chiark / gitweb /
Merge playlist branch against trunk to date.
authorRichard Kettlewell <rjk@greenend.org.uk>
Tue, 17 Feb 2009 20:29:50 +0000 (20:29 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Tue, 17 Feb 2009 20:29:50 +0000 (20:29 +0000)
26 files changed:
1  2 
clients/disorder.c
disobedience/Makefile.am
disobedience/disobedience.c
disobedience/disobedience.h
disobedience/log.c
disobedience/menu.c
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.c
lib/trackdb.h
python/disorder.py.in
scripts/completion.bash
server/dump.c
server/server.c
tests/Makefile.am

@@@ -577,11 -588,61 +598,66 @@@ static void cf_schedule_unset_global(ch
      exit(EXIT_FAILURE);
  }
  
 +static void cf_adopt(char **argv) {
 +  if(disorder_adopt(getclient(), argv[0]))
 +    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;
@@@ -27,8 -29,7 +27,8 @@@ disobedience_SOURCES=disobedience.h dis
        recent.c added.c queue-generic.c queue-generic.h queue-menu.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 playlists.c
 +      help.c ../lib/memgc.c settings.c users.c lookup.c choose.h      \
-       popup.h
++      popup.h playlists.c
  disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
        $(LIBASOUND) $(COREAUDIO) $(LIBDB)
  disobedience_LDFLAGS=$(GTK_LIBS)
Simple merge
Simple merge
@@@ -41,7 -43,12 +41,13 @@@ static void log_state(void *v, unsigne
  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 = {
    .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 */
@@@ -204,13 -213,23 +213,30 @@@ static void log_rights_changed(void att
    --suppress_actions;
  }
  
 +/** @brief Called when a track is adopted */
 +static void log_adopted(void attribute((unused)) *v,
 +                        const char attribute((unused)) *id,
 +                        const char attribute((unused)) *who) {
 +  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
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
diff --cc lib/Makefile.am
Simple merge
diff --cc lib/client.c
@@@ -1302,15 -1361,103 +1359,112 @@@ int disorder_schedule_add(disorder_clie
    return rc;
  }
  
 +/** @brief Adopt a track
 + * @param c Client
 + * @param id Track ID to adopt
 + * @return 0 on success, non-0 on error
 + */
 +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
diff --cc lib/client.h
@@@ -131,7 -133,22 +131,23 @@@ int disorder_schedule_add(disorder_clie
                          const char *priority,
                          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 */
  
@@@ -1195,8 -1199,9 +1197,10 @@@ static struct config *config_default(vo
    c->new_max = 100;
    c->reminder_interval = 600;         /* 10m */
    c->new_bias_age = 7 * 86400;                /* 1 week */
 -  c->new_bias = 9000000;              /* 100 times the base weight */
 +  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);
Simple merge
diff --cc lib/eclient.c
@@@ -186,7 -191,9 +189,10 @@@ static void logentry_user_confirm(disor
  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 ********************************************************************/
  
@@@ -1406,20 -1465,123 +1465,137 @@@ int disorder_eclient_adduser(disorder_e
                  "adduser", user, password, rights, (char *)0);
  }
  
 +/** @brief Adopt a track
 + * @param c Client
 + * @param completed Called on completion
 + * @param id Track ID
 + * @param v Passed to @p completed
 + */
 +int disorder_eclient_adopt(disorder_eclient *c,
 +                           disorder_eclient_no_response *completed,
 +                           const char *id,
 +                           void *v) {
 +  return simple(c, no_response_opcallback, (void (*)())completed, v, 
 +                "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
diff --cc lib/eclient.h
@@@ -1,19 -1,21 +1,19 @@@
  /*
   * 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
 + * 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
 + * the Free Software Foundation, either version 3 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.
 - *
 + * 
 + * 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
 + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  /** @file lib/eclient.h
   * @brief Client code for event-driven programs
@@@ -166,8 -168,14 +166,17 @@@ typedef struct disorder_eclient_log_cal
    /** @brief Called when your rights change */
    void (*rights_changed)(void *v, rights_type new_rights);
  
 +  /** @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 */
@@@ -486,10 -496,40 +497,44 @@@ int disorder_eclient_adduser(disorder_e
                               void *v);
  void disorder_eclient_enable_connect(disorder_eclient *c);
  void disorder_eclient_disable_connect(disorder_eclient *c);
 +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
  
  /*
Simple merge
diff --cc lib/trackdb.c
@@@ -490,19 -494,26 +498,20 @@@ void trackdb_close(void) 
    /* sanity checks */
    assert(opened == 1);
    --opened;
 -  if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0)))
 -    fatal(0, "error closing tracks.db: %s", db_strerror(err));
 -  if((err = trackdb_searchdb->close(trackdb_searchdb, 0)))
 -    fatal(0, "error closing search.db: %s", db_strerror(err));
 -  if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0)))
 -    fatal(0, "error closing tags.db: %s", db_strerror(err));
 -  if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0)))
 -    fatal(0, "error closing prefs.db: %s", db_strerror(err));
 -  if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
 -    fatal(0, "error closing global.db: %s", db_strerror(err));
 -  if((err = trackdb_noticeddb->close(trackdb_noticeddb, 0)))
 -    fatal(0, "error closing noticed.db: %s", db_strerror(err));
 -  if((err = trackdb_scheduledb->close(trackdb_scheduledb, 0)))
 -    fatal(0, "error closing schedule.db: %s", db_strerror(err));
 -  if((err = trackdb_usersdb->close(trackdb_usersdb, 0)))
 -    fatal(0, "error closing users.db: %s", db_strerror(err));
 -  if((err = trackdb_playlistsdb->close(trackdb_playlistsdb, 0)))
 -    fatal(0, "error closing playlists.db: %s", db_strerror(err));
 -  trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
 -  trackdb_tagsdb = trackdb_globaldb = 0;
 +#define CLOSE(N, V) do {                                        \
 +  if(V && (err = V->close(V, 0)))                               \
 +    fatal(0, "error closing %s: %s", N, db_strerror(err));      \
 +  V = 0;                                                        \
 +} while(0)
 +  CLOSE("tracks.db", trackdb_tracksdb);
 +  CLOSE("search.db", trackdb_searchdb);
 +  CLOSE("tags.db", trackdb_tagsdb);
 +  CLOSE("prefs.db", trackdb_prefsdb);
 +  CLOSE("global.db", trackdb_globaldb);
 +  CLOSE("noticed.db", trackdb_noticeddb);
 +  CLOSE("schedule.db", trackdb_scheduledb);
 +  CLOSE("users.db", trackdb_usersdb);
++  CLOSE("playlists.db", trackdb_playlistsdb);
    D(("closed databases"));
  }
  
diff --cc lib/trackdb.h
Simple merge
@@@ -905,10 -909,54 +907,58 @@@ class client
      """Add a scheduled event"""
      self._simple("schedule-add", str(when), priority, action, *rest)
  
 +  def adopt(self, id):
 +    """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
  
@@@ -32,7 -34,7 +32,8 @@@ complete -o default 
               tags new rtp-address adduser users edituser deluser userinfo
               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
diff --cc server/dump.c
Simple merge
diff --cc server/server.c
@@@ -1573,31 -1618,152 +1616,177 @@@ static int c_schedule_add(struct conn *
    return 1;
  }
  
 +static int c_adopt(struct conn *c,
 +                 char **vec,
 +                 int attribute((unused)) nvec) {
 +  struct queue_entry *q;
 +
 +  if(!c->who) {
 +    sink_writes(ev_writer_sink(c->w), "550 no identity\n");
 +    return 1;
 +  }
 +  if(!(q = queue_find(vec[0]))) {
 +    sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
 +    return 1;
 +  }
 +  if(q->origin != origin_random) {
 +    sink_writes(ev_writer_sink(c->w), "550 not a random track\n");
 +    return 1;
 +  }
 +  q->origin = origin_adopted;
 +  q->submitter = xstrdup(c->who);
 +  eventlog("adopted", q->id, q->submitter, (char *)0);
 +  queue_write();
 +  sink_writes(ev_writer_sink(c->w), "250 OK\n");
 +  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;
Simple merge