chiark / gitweb /
Merge from disorder.dev.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 Oct 2009 21:54:18 +0000 (22:54 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 Oct 2009 21:54:18 +0000 (22:54 +0100)
29 files changed:
clients/disorder.c
disobedience/Makefile.am
disobedience/disobedience.c
disobedience/disobedience.h
disobedience/log.c
disobedience/menu.c
disobedience/playlists.c [new file with mode: 0644]
disobedience/queue-generic.c
disobedience/queue-generic.h
disobedience/queue.c
doc/disorder.1.in
doc/disorder_protocol.5.in
lib/Makefile.am
lib/client.c
lib/client.h
lib/configuration.c
lib/configuration.h
lib/eclient.c
lib/eclient.h
lib/trackdb-int.h
lib/trackdb-playlists.c [new file with mode: 0644]
lib/trackdb.c
lib/trackdb.h
python/disorder.py.in
scripts/completion.bash
server/dump.c
server/server.c
tests/Makefile.am
tests/playlists.py [new file with mode: 0755]

index ccfaf20..a246eee 100644 (file)
@@ -33,6 +33,7 @@
 #include <pcre.h>
 #include <ctype.h>
 #include <gcrypt.h>
+#include <langinfo.h>
 
 #include "configuration.h"
 #include "syscalls.h"
@@ -52,6 +53,7 @@
 #include "version.h"
 #include "dateparse.h"
 #include "trackdb.h"
+#include "inputline.h"
 
 static disorder_client *client;
 
@@ -188,15 +190,34 @@ static void cf_queue(char attribute((unused)) **argv) {
 }
 
 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,
@@ -585,6 +606,61 @@ static void cf_adopt(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;
@@ -638,6 +714,14 @@ static const struct command {
                       "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 },
index c7b702b..f8bdb14 100644 (file)
@@ -28,7 +28,7 @@ disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c   \
        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) $(LIBICONV)
 disobedience_LDFLAGS=$(GTK_LIBS)
index 38fc6eb..7fad2eb 100644 (file)
@@ -243,6 +243,7 @@ static gboolean periodic_slow(gpointer attribute((unused)) data) {
   /* 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 */
@@ -285,6 +286,7 @@ static gboolean periodic_fast(gpointer attribute((unused)) data) {
     recheck_rights = 0;
   if(recheck_rights)
     check_rights();
+  event_raise("periodic-fast", 0);
   return TRUE;
 }
 
@@ -493,6 +495,9 @@ int main(int argc, char **argv) {
   disorder_eclient_version(client, version_completed, 0);
   event_register("log-connected", check_rtp_address, 0);
   suppress_actions = 0;
+#if PLAYLISTS
+  playlists_init();
+#endif
   /* If no password is set yet pop up a login box */
   if(!config->password)
     login_box();
index ca5f7ef..f4678c7 100644 (file)
@@ -252,6 +252,20 @@ void load_settings(void);
 void set_tool_colors(GtkWidget *w);
 void popup_settings(void);
 
+/* Playlists */
+
+#if 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
+
 #endif /* DISOBEDIENCE_H */
 
 /*
index 652c4e9..f1c4f79 100644 (file)
@@ -42,6 +42,12 @@ static void log_volume(void *v, int l, int r);
 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 = {
@@ -59,7 +65,10 @@ 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 */
@@ -211,6 +220,23 @@ static void log_adopted(void attribute((unused)) *v,
   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
index 15fb4fb..0243139 100644 (file)
 static GtkWidget *selectall_widget;
 static GtkWidget *selectnone_widget;
 static GtkWidget *properties_widget;
+#if PLAYLISTS
+GtkWidget *playlists_widget;
+GtkWidget *playlists_menu;
+GtkWidget *editplaylists_widget;
+#endif
 
 /** @brief Main menu widgets */
 GtkItemFactory *mainmenufactory;
@@ -113,7 +118,7 @@ static void edit_menu_show(GtkWidget attribute((unused)) *widget,
                              && 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,
@@ -293,6 +298,17 @@ GtkWidget *menubar(GtkWidget *w) {
       0,                                /* item_type */
       0                                 /* extra_data */
     },
+#if PLAYLISTS
+    {
+      (char *)"/Edit/Edit playlists",   /* path */
+      0,                                /* accelerator */
+      edit_playlists,                   /* callback */
+      0,                                /* callback_action */
+      0,                                /* item_type */
+      0                                 /* extra_data */
+    },
+#endif
+    
     
     {
       (char *)"/Control",               /* path */
@@ -334,6 +350,16 @@ GtkWidget *menubar(GtkWidget *w) {
       (char *)"<CheckItem>",            /* item_type */
       0                                 /* extra_data */
     },
+#if PLAYLISTS
+    {
+      (char *)"/Control/Activate playlist", /* path */
+      0,                                /* accelerator */
+      0,                                /* callback */
+      0,                                /* callback_action */
+      (char *)"<Branch>",               /* item_type */
+      0                                 /* extra_data */
+    },
+#endif
     
     {
       (char *)"/Help",                  /* path */
@@ -378,15 +404,27 @@ GtkWidget *menubar(GtkWidget *w) {
                                                 "<GdisorderMain>/Edit/Deselect all tracks");
   properties_widget = gtk_item_factory_get_widget(mainmenufactory,
                                                  "<GdisorderMain>/Edit/Track properties");
+#if PLAYLISTS
+  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");
+#endif
   assert(selectall_widget != 0);
   assert(selectnone_widget != 0);
   assert(properties_widget != 0);
+#if PLAYLISTS
+  assert(playlists_widget != 0);
+  assert(playlists_menu != 0);
+  assert(editplaylists_widget != 0);
+#endif
 
-  
   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,
diff --git a/disobedience/playlists.c b/disobedience/playlists.c
new file mode 100644 (file)
index 0000000..cd8979d
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+ * 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
+ *
+ * The playlists management window contains:
+ * - a list of all playlists
+ * - an add button
+ * - a delete button
+ * - a drag+drop capable view of the playlist
+ * - a close button
+ */
+#include "disobedience.h"
+
+#if PLAYLISTS
+
+static void playlists_updated(void *v,
+                              const char *err,
+                              int nvec, char **vec);
+
+/** @brief Playlist editing window */
+static GtkWidget *playlists_window;
+
+/** @brief Tree model for list of playlists */
+static GtkListStore *playlists_list;
+
+/** @brief Selection for list of playlists */
+static GtkTreeSelection *playlists_selection;
+
+/** @brief Currently selected playlist */
+static const char *playlists_selected;
+
+/** @brief Delete button */
+static GtkWidget *playlists_delete_button;
+
+/** @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);
+}
+
+/** @brief (Re-)populate the playlist tree model */
+static void playlists_fill(void) {
+  GtkTreeIter iter[1];
+
+  if(!playlists_list)
+    playlists_list = gtk_list_store_new(1, G_TYPE_STRING);
+  gtk_list_store_clear(playlists_list);
+  for(int n = 0; n < nplaylists; ++n)
+    gtk_list_store_insert_with_values(playlists_list, iter, n/*position*/,
+                                      0, playlists[n],        /* column 0 */
+                                      -1);                    /* no more cols */
+  // TODO reselect whatever was formerly selected if possible, if not then
+  // zap the contents view
+}
+
+/** @brief Called when the selection might have changed */
+static void playlists_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
+                                        gpointer attribute((unused)) user_data) {
+  GtkTreeIter iter;
+  char *gselected, *selected;
+  
+  /* Identify the current selection */
+  if(gtk_tree_selection_get_selected(playlists_selection, 0, &iter)) {
+    gtk_tree_model_get(GTK_TREE_MODEL(playlists_list), &iter,
+                       0, &gselected, -1);
+    selected = xstrdup(gselected);
+    g_free(gselected);
+  } else
+    selected = 0;
+  /* Eliminate no-change cases */
+  if(!selected && !playlists_selected)
+    return;
+  if(selected && playlists_selected && !strcmp(selected, playlists_selected))
+    return;
+  /* There's been a change */
+  playlists_selected = selected;
+  if(playlists_selected) {
+    fprintf(stderr, "playlists selection changed\n'"); /* TODO */
+    gtk_widget_set_sensitive(playlists_delete_button, 1);
+  } else
+    gtk_widget_set_sensitive(playlists_delete_button, 0);
+}
+
+/** @brief Called when the 'add' button is pressed */
+static void playlists_add(GtkButton attribute((unused)) *button,
+                          gpointer attribute((unused)) userdata) {
+  /* Unselect whatever is selected */
+  gtk_tree_selection_unselect_all(playlists_selection);
+  fprintf(stderr, "playlists_add\n");/* TODO */
+}
+
+/** @brief Called when the 'Delete' button is pressed */
+static void playlists_delete(GtkButton attribute((unused)) *button,
+                        gpointer attribute((unused)) userdata) {
+  GtkWidget *yesno;
+  int res;
+
+  if(!playlists_selected)
+    return;                             /* shouldn't happen */
+  yesno = gtk_message_dialog_new(GTK_WINDOW(playlists_window),
+                                 GTK_DIALOG_MODAL,
+                                 GTK_MESSAGE_QUESTION,
+                                 GTK_BUTTONS_YES_NO,
+                                 "Do you really want to delete user %s?"
+                                 " This action cannot be undone.",
+                                 playlists_selected);
+  res = gtk_dialog_run(GTK_DIALOG(yesno));
+  gtk_widget_destroy(yesno);
+  if(res == GTK_RESPONSE_YES) {
+    disorder_eclient_playlist_delete(client,
+                                     NULL/*playlists_delete_completed*/,
+                                     playlists_selected,
+                                     NULL);
+  }
+}
+
+/** @brief Table of buttons below the playlist list */
+static struct button playlists_buttons[] = {
+  {
+    GTK_STOCK_ADD,
+    playlists_add,
+    "Create a new playlist",
+    0
+  },
+  {
+    GTK_STOCK_REMOVE,
+    playlists_delete,
+    "Delete a playlist",
+    0
+  },
+};
+#define NPLAYLISTS_BUTTONS (sizeof playlists_buttons / sizeof *playlists_buttons)
+
+/** @brief Keypress handler */
+static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget,
+                                   GdkEventKey *event,
+                                   gpointer attribute((unused)) user_data) {
+  if(event->state)
+    return FALSE;
+  switch(event->keyval) {
+  case GDK_Escape:
+    gtk_widget_destroy(playlists_window);
+    return TRUE;
+  default:
+    return FALSE;
+  }
+}
+
+void edit_playlists(gpointer attribute((unused)) callback_data,
+                     guint attribute((unused)) callback_action,
+                     GtkWidget attribute((unused)) *menu_item) {
+  GtkWidget *tree, *hbox, *vbox, *buttons;
+  GtkCellRenderer *cr;
+  GtkTreeViewColumn *col;
+
+  /* If the window already exists, raise it */
+  if(playlists_window) {
+    gtk_window_present(GTK_WINDOW(playlists_window));
+    return;
+  }
+  /* Create the window */
+  playlists_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  gtk_widget_set_style(playlists_window, tool_style);
+  g_signal_connect(playlists_window, "destroy",
+                  G_CALLBACK(gtk_widget_destroyed), &playlists_window);
+  gtk_window_set_title(GTK_WINDOW(playlists_window), "Playlists Management");
+  /* TODO loads of this is very similar to (copied from!) users.c - can we
+   * de-dupe? */
+  /* Keyboard shortcuts */
+  g_signal_connect(playlists_window, "key-press-event",
+                   G_CALLBACK(playlists_keypress), 0);
+  /* default size is too small */
+  gtk_window_set_default_size(GTK_WINDOW(playlists_window), 240, 240);
+  /* Create the list of playlist and populate it */
+  playlists_fill();
+  /* Create the tree view */
+  tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlists_list));
+  /* ...and the renderers for it */
+  cr = gtk_cell_renderer_text_new();
+  col = gtk_tree_view_column_new_with_attributes("Playlist",
+                                                cr,
+                                                "text", 0,
+                                                NULL);
+  gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
+  /* Get the selection for the view; set its mode; arrange for a callback when
+   * it changes */
+  playlists_selected = NULL;
+  playlists_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+  gtk_tree_selection_set_mode(playlists_selection, GTK_SELECTION_BROWSE);
+  g_signal_connect(playlists_selection, "changed",
+                   G_CALLBACK(playlists_selection_changed), NULL);
+
+  /* Create the control buttons */
+  buttons = create_buttons_box(playlists_buttons,
+                              NPLAYLISTS_BUTTONS,
+                              gtk_hbox_new(FALSE, 1));
+  playlists_delete_button = playlists_buttons[1].widget;
+
+  /* Buttons live below the list */
+  vbox = gtk_vbox_new(FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+
+  hbox = gtk_hbox_new(FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE/*expand*/, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(), FALSE/*expand*/, FALSE, 2);
+  // TODO something to edit the playlist in
+  //gtk_box_pack_start(GTK_BOX(hbox), vbox2, TRUE/*expand*/, TRUE/*fill*/, 0);
+  gtk_container_add(GTK_CONTAINER(playlists_window), frame_widget(hbox, NULL));
+  gtk_widget_show_all(playlists_window);
+}
+
+/** @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);
+}
+
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index aa29c28..f14f424 100644 (file)
@@ -143,7 +143,7 @@ const char *column_length(const struct queue_entry *q,
 /** @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) {
@@ -402,6 +402,120 @@ void ql_new_queue(struct queuelike *ql,
   --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"));
@@ -447,6 +561,17 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
   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();
index 4b31fe9..8dd9fdb 100644 (file)
@@ -90,6 +90,18 @@ struct queuelike {
 
   /** @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 {
index 7779c0f..a051090 100644 (file)
@@ -152,6 +152,61 @@ static void queue_init(void) {
   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 },
@@ -178,161 +233,10 @@ struct queuelike ql_queue = {
   .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,
@@ -353,14 +257,6 @@ static gboolean queue_key_press(GtkWidget attribute((unused)) *widget,
 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);
index afa1eaa..b61a825 100644 (file)
@@ -152,6 +152,23 @@ Add \fITRACKS\fR to the end of the 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).
index b42ebd5..a0baadb 100644 (file)
@@ -38,6 +38,15 @@ that comments are prohibited.
 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.
@@ -47,8 +56,6 @@ All commands require the connection to have been already authenticated unless
 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.
@@ -208,6 +215,43 @@ track information (see below).
 .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
@@ -598,6 +642,21 @@ Further details aren't included any more.
 .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
index 4717fd6..d0890e1 100644 (file)
@@ -78,6 +78,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        table.c table.h                                 \
        timeval.h                                       \
        $(TRACKDB) trackdb.h trackdb-int.h              \
+       trackdb-playlists.c                             \
        trackname.c trackorder.c trackname.h            \
        tracksort.c                                     \
        uaudio.c uaudio-thread.c uaudio.h uaudio-apis.c \
index 20db675..a0502eb 100644 (file)
@@ -153,6 +153,8 @@ static int check_response(disorder_client *c, char **rp) {
  * @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
  *
@@ -163,10 +165,21 @@ static int check_response(disorder_client *c, char **rp) {
  *
  * 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;
 
@@ -185,13 +198,32 @@ static int disorder_simple_v(disorder_client *c,
     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
@@ -218,7 +250,30 @@ static int disorder_simple(disorder_client *c,
   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;
 }
@@ -670,6 +725,8 @@ static int readlist(disorder_client *c, char ***vecp, int *nvecp) {
  * *)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,
@@ -678,7 +735,7 @@ static int disorder_simple_list(disorder_client *c,
   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);
@@ -1311,6 +1368,103 @@ int disorder_adopt(disorder_client *c, const char *id) {
   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
index 89f3037..f7ff728 100644 (file)
@@ -132,6 +132,22 @@ int disorder_schedule_add(disorder_client *c,
                          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 */
 
index dc3d009..ff59968 100644 (file)
@@ -947,6 +947,8 @@ static const struct conf conf[] = {
   { C(password),         &type_string,           validate_any },
   { C(pause_mode),       &type_string,           validate_pausemode },
   { 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 },
@@ -1195,6 +1197,8 @@ static struct config *config_default(void) {
   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);
index 875b9d6..9170a2f 100644 (file)
@@ -185,6 +185,12 @@ struct config {
   /** @brief API used to play sound */
   const char *api;
 
+  /** @brief Maximum size of a playlist */
+  long playlist_max;
+
+  /** @brief Maximum lifetime of a playlist lock */
+  long playlist_lock_timeout;
+
   /** @brief Home directory for state files */
   const char *home;
 
index 101a7bf..fad9e1b 100644 (file)
@@ -92,6 +92,7 @@ typedef void operation_callback(disorder_eclient *c, struct operation *op);
 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 */
@@ -165,6 +166,8 @@ static void stash_command(disorder_eclient *c,
                           operation_callback *opcallback,
                           void (*completed)(),
                           void *v,
+                          int nbody,
+                          char **body,
                           const char *cmd,
                           ...);
 static void log_opcallback(disorder_eclient *c, struct operation *op);
@@ -187,6 +190,9 @@ static void logentry_user_delete(disorder_eclient *c, int nvec, char **vec);
 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 ********************************************************************/
 
@@ -208,6 +214,9 @@ static const struct logentry_handler logentry_handlers[] = {
   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),
@@ -326,6 +335,24 @@ static int protocol_error(disorder_eclient *c, struct operation *op,
 
 /* 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.
@@ -379,7 +406,7 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
     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. */
@@ -395,17 +422,13 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
       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;
@@ -601,6 +624,7 @@ static void authbanner_opcallback(disorder_eclient *c,
     return;
   }
   stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
+                -1/*nbody*/, 0/*body*/,
                 "user", quoteutf8(config->username), quoteutf8(res),
                 (char *)0);
 }
@@ -625,6 +649,7 @@ static void authuser_opcallback(disorder_eclient *c,
   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);
 }
 
@@ -787,6 +812,8 @@ static void stash_command_vector(disorder_eclient *c,
                                  operation_callback *opcallback,
                                  void (*completed)(),
                                  void *v,
+                                 int nbody,
+                                 char **body,
                                  int ncmd,
                                  char **cmd) {
   struct operation *op = xmalloc(sizeof *op);
@@ -805,6 +832,13 @@ static void stash_command_vector(disorder_eclient *c,
     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;
@@ -830,6 +864,8 @@ static void vstash_command(disorder_eclient *c,
                            operation_callback *opcallback,
                            void (*completed)(),
                            void *v,
+                           int nbody,
+                           char **body,
                            const char *cmd, va_list ap) {
   char *arg;
   struct vector vec;
@@ -841,9 +877,11 @@ static void vstash_command(disorder_eclient *c,
     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,
@@ -851,12 +889,14 @@ 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);
 }
 
@@ -1008,6 +1048,8 @@ static void list_response_opcallback(disorder_eclient *c,
   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);
 }
@@ -1039,7 +1081,24 @@ static int simple(disorder_eclient *c,
   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);
@@ -1124,7 +1183,7 @@ int disorder_eclient_moveafter(disorder_eclient *c,
   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;
 }
@@ -1420,6 +1479,123 @@ int disorder_eclient_adopt(disorder_eclient *c,
                 "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
@@ -1444,7 +1620,7 @@ int disorder_eclient_log(disorder_eclient *c,
   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;
 }
@@ -1612,6 +1788,27 @@ static void logentry_rights_changed(disorder_eclient *c,
   }
 }
 
+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;
index ce5c582..fae610c 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * 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
@@ -168,6 +168,15 @@ typedef struct disorder_eclient_log_callbacks {
 
   /** @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 */
@@ -222,7 +231,8 @@ typedef void disorder_eclient_no_response(void *v,
  *
  * @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.
  */
@@ -281,7 +291,8 @@ typedef void disorder_eclient_queue_response(void *v,
  * @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.
@@ -490,6 +501,40 @@ int disorder_eclient_adopt(disorder_eclient *c,
                            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
 
 /*
index 6d5234e..b21b5d9 100644 (file)
@@ -22,6 +22,7 @@
 
 #include <db.h>
 
+#include "trackdb.h"
 #include "kvp.h"
 
 struct vector;                          /* forward declaration */
@@ -36,6 +37,7 @@ extern DB *trackdb_noticeddb;
 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 */
@@ -151,6 +153,7 @@ int trackdb_get_global_tid(const char *name,
 
 char **parsetags(const char *s);
 int tag_intersection(char **a, char **b);
+int valid_username(const char *user);
 
 #endif /* TRACKDB_INT_H */
 
diff --git a/lib/trackdb-playlists.c b/lib/trackdb-playlists.c
new file mode 100644 (file)
index 0000000..bef7107
--- /dev/null
@@ -0,0 +1,491 @@
+/*
+ * 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:
+*/
index 6984b21..d724472 100644 (file)
@@ -157,6 +157,13 @@ DB *trackdb_scheduledb;
  */
 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 */
@@ -472,6 +479,7 @@ void trackdb_open(int flags) {
   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 && !(flags & TRACKDB_READ_ONLY)) {
     /* Stash the database version */
     char buf[32];
@@ -503,6 +511,7 @@ void trackdb_close(void) {
   CLOSE("noticed.db", trackdb_noticeddb);
   CLOSE("schedule.db", trackdb_scheduledb);
   CLOSE("users.db", trackdb_usersdb);
+  CLOSE("playlists.db", trackdb_playlistsdb);
   D(("closed databases"));
 }
 
@@ -2553,8 +2562,10 @@ static int trusted(const char *user) {
  * 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) {
index 0774579..6b86651 100644 (file)
@@ -184,6 +184,25 @@ void trackdb_add_rescanned(void (*rescanned)(void *ru),
                            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 */
 
 /*
index f6fe1a4..d06c7ee 100644 (file)
@@ -125,8 +125,8 @@ class operationError(Error):
     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.
     """
@@ -433,8 +433,8 @@ class client:
 
     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
@@ -539,8 +539,8 @@ class client:
     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):
@@ -775,7 +775,8 @@ class client:
     
     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.
@@ -902,7 +903,8 @@ class client:
     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
@@ -920,6 +922,54 @@ class client:
     """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
 
@@ -949,8 +999,8 @@ class client:
     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)
@@ -959,6 +1009,13 @@ class client:
     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:
@@ -969,7 +1026,7 @@ class client:
       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.
@@ -977,10 +1034,20 @@ class client:
     # 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()
@@ -1061,8 +1128,8 @@ class client:
 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
@@ -1078,8 +1145,8 @@ class monitor:
 
   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):
index 89eb63d..2766b17 100644 (file)
@@ -33,6 +33,7 @@ complete -o default \
              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
index 4b023aa..1cee366 100644 (file)
@@ -29,8 +29,6 @@ static const struct option options[] = {
   { "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 }
@@ -55,14 +53,70 @@ static void help(void) {
   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();
@@ -72,124 +126,18 @@ static void do_dump(FILE *fp, const char *tag,
       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);
   }
@@ -276,9 +224,6 @@ static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
 /* 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)
@@ -291,6 +236,28 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
   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);
@@ -299,54 +266,15 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
       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))
@@ -435,13 +363,13 @@ fail:
 
 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");
@@ -449,8 +377,6 @@ int main(int argc, char **argv) {
     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;
@@ -460,8 +386,6 @@ int main(int argc, char **argv) {
   }
   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");
@@ -484,7 +408,7 @@ int main(int argc, char **argv) {
       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);
index feaac7e..781c71a 100644 (file)
@@ -44,6 +44,34 @@ struct listener {
   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 */
@@ -77,6 +105,18 @@ struct conn {
   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 */
@@ -88,6 +128,15 @@ static int reader_callback(ev_source *ev,
                           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" };
 
@@ -1030,21 +1079,25 @@ static int c_resolve(struct conn *c,
   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) {
@@ -1314,17 +1367,7 @@ static int c_userinfo(struct conn *c,
 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());
 }
 
 static int c_register(struct conn *c,
@@ -1599,6 +1642,152 @@ static int c_adopt(struct conn *c,
   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;
@@ -1645,6 +1834,14 @@ static const struct command {
   { "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 },
@@ -1680,13 +1877,58 @@ static const struct command {
   { "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;
@@ -1757,7 +1999,7 @@ static int reader_callback(ev_source attribute((unused)) *ev,
   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) {
@@ -1820,6 +2062,7 @@ static int listen_callback(ev_source *ev,
   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",
index dfe7424..5563f7f 100644 (file)
@@ -26,7 +26,7 @@ disorder_udplog_DEPENDENCIES=../lib/libdisorder.a
 
 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 hashes.py
+       schedule.py hashes.py playlists.py
 
 TESTS_ENVIRONMENT=${PYTHON} -u
 
diff --git a/tests/playlists.py b/tests/playlists.py
new file mode 100755 (executable)
index 0000000..0932632
--- /dev/null
@@ -0,0 +1,151 @@
+#! /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()