From: Richard Kettlewell Date: Tue, 17 Feb 2009 20:29:50 +0000 (+0000) Subject: Merge playlist branch against trunk to date. X-Git-Tag: 5.0~86^2~4^2~1 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/812b526d127c6657e571db8b33a58137af6709cd?hp=-c Merge playlist branch against trunk to date. --- 812b526d127c6657e571db8b33a58137af6709cd diff --combined clients/disorder.c index 8dc6d7f,b57a8de..4161d93 --- a/clients/disorder.c +++ b/clients/disorder.c @@@ -2,21 -2,20 +2,21 @@@ * This file is part of DisOrder. * Copyright (C) 2004-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 . + */ +/** @file clients/disorder.c + * @brief Command-line client */ #include "common.h" @@@ -32,6 -31,7 +32,7 @@@ #include #include #include + #include #include "configuration.h" #include "syscalls.h" @@@ -50,6 -50,7 +51,7 @@@ #include "vector.h" #include "version.h" #include "dateparse.h" + #include "inputline.h" static disorder_client *client; @@@ -99,17 -100,8 +101,17 @@@ static void cf_version(char attribute(( static void print_queue_entry(const struct queue_entry *q) { if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track))); if(q->id) xprintf(" id %s\n", nullcheck(utf82mb(q->id))); - if(q->submitter) xprintf(" submitted by %s at %s", - nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + switch(q->origin) { + case origin_adopted: + case origin_picked: + case origin_scheduled: + xprintf(" %s by %s at %s", + track_origins[q->origin], + nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + break; + default: + break; + } if(q->played) xprintf(" played at %s", ctime(&q->played)); if(q->state == playing_started || q->state == playing_paused) xprintf(" %lds so far", q->sofar); @@@ -185,15 -177,34 +187,34 @@@ static void cf_queue(char attribute((un } 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, @@@ -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; @@@ -591,8 -652,6 +667,8 @@@ } commands[] = { { "adduser", 2, 3, cf_adduser, isarg_rights, "USERNAME PASSWORD [RIGHTS]", "Create a new user" }, + { "adopt", 1, 1, cf_adopt, 0, "ID", + "Adopt a randomly picked track" }, { "allfiles", 1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]", "List all files and directories in DIR" }, { "authorize", 1, 2, cf_authorize, isarg_rights, "USERNAME [RIGHTS]", @@@ -635,6 -694,14 +711,14 @@@ "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 }, diff --combined disobedience/Makefile.am index e7072c7,db74563..94a4c78 --- a/disobedience/Makefile.am +++ b/disobedience/Makefile.am @@@ -2,18 -2,20 +2,18 @@@ # This file is part of DisOrder. # 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 . # bin_PROGRAMS=disobedience @@@ -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) @@@ -66,7 -67,6 +66,7 @@@ check-help: al unset DISPLAY;./disobedience --version > /dev/null unset DISPLAY;./disobedience --help > /dev/null -CLEANFILES=disobedience.html images.h +CLEANFILES=disobedience.html images.h \ + *.gcda *.gcov *.gcno *.c.html index.html export GNUSED diff --combined disobedience/disobedience.c index ab51bb3,dd052ad..1f9ee43 --- a/disobedience/disobedience.c +++ b/disobedience/disobedience.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * Copyright (C) 2006, 2007, 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 . */ /** @file disobedience/disobedience.c * @brief Main Disobedience program @@@ -240,6 -242,7 +240,7 @@@ static gboolean periodic_slow(gpointer /* 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 */ @@@ -282,6 -285,7 +283,7 @@@ static gboolean periodic_fast(gpointer recheck_rights = 0; if(recheck_rights) check_rights(); + event_raise("periodic-fast", 0); return TRUE; } @@@ -479,6 -483,7 +481,7 @@@ int main(int argc, char **argv) disorder_eclient_version(client, version_completed, 0); event_register("log-connected", check_rtp_address, 0); suppress_actions = 0; + playlists_init(); /* If no password is set yet pop up a login box */ if(!config->password) login_box(); diff --combined disobedience/disobedience.h index db4bff0,547cfc8..4687724 --- a/disobedience/disobedience.h +++ b/disobedience/disobedience.h @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * 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 . */ /** @file disobedience/disobedience.h * @brief Header file for Disobedience, the DisOrder GTK+ client @@@ -250,6 -252,18 +250,18 @@@ void load_settings(void) void set_tool_colors(GtkWidget *w); void popup_settings(void); + /* Playlists */ + + void playlists_init(void); + void edit_playlists(gpointer callback_data, + guint callback_action, + GtkWidget *menu_item); + extern char **playlists; + extern int nplaylists; + extern GtkWidget *playlists_widget; + extern GtkWidget *playlists_menu; + extern GtkWidget *editplaylists_widget; + #endif /* DISOBEDIENCE_H */ /* diff --combined disobedience/log.c index 652c4e9,9a94e06..f1c4f79 --- a/disobedience/log.c +++ b/disobedience/log.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * Copyright (C) 2006, 2007 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 . */ /** @file disobedience/log.c * @brief State monitoring @@@ -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 = { @@@ -59,7 -66,9 +65,10 @@@ .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 diff --combined disobedience/menu.c index 283c0d1,b8ba439..1d50223 --- a/disobedience/menu.c +++ b/disobedience/menu.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * 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 . */ /** @file disobedience/menu.c * @brief Main menu @@@ -24,6 -26,9 +24,9 @@@ static GtkWidget *selectall_widget; static GtkWidget *selectnone_widget; static GtkWidget *properties_widget; + GtkWidget *playlists_widget; + GtkWidget *playlists_menu; + GtkWidget *editplaylists_widget; /** @brief Main menu widgets */ GtkItemFactory *mainmenufactory; @@@ -113,7 -118,7 +116,7 @@@ static void edit_menu_show(GtkWidget at && 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,15 +296,15 @@@ GtkWidget *menubar(GtkWidget *w) 0, /* item_type */ 0 /* extra_data */ }, + { + (char *)"/Edit/Edit playlists", /* path */ + 0, /* accelerator */ + edit_playlists, /* callback */ + 0, /* callback_action */ + 0, /* item_type */ + 0 /* extra_data */ + }, + { (char *)"/Control", /* path */ @@@ -334,6 -348,14 +346,14 @@@ (char *)"", /* item_type */ 0 /* extra_data */ }, + { + (char *)"/Control/Activate playlist", /* path */ + 0, /* accelerator */ + 0, /* callback */ + 0, /* callback_action */ + (char *)"", /* item_type */ + 0 /* extra_data */ + }, { (char *)"/Help", /* path */ @@@ -378,15 -400,23 +398,23 @@@ "/Edit/Deselect all tracks"); properties_widget = gtk_item_factory_get_widget(mainmenufactory, "/Edit/Track properties"); + playlists_widget = gtk_item_factory_get_item(mainmenufactory, + "/Control/Activate playlist"); + playlists_menu = gtk_item_factory_get_widget(mainmenufactory, + "/Control/Activate playlist"); + editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory, + "/Edit/Edit playlists"); assert(selectall_widget != 0); assert(selectnone_widget != 0); assert(properties_widget != 0); + assert(playlists_widget != 0); + assert(playlists_menu != 0); + assert(editplaylists_widget != 0); - GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory, "/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 --combined disobedience/queue-generic.c index 636f10c,daf5a6b..60ff794 --- a/disobedience/queue-generic.c +++ b/disobedience/queue-generic.c @@@ -2,21 -2,23 +2,21 @@@ * This file is part of DisOrder * 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 . */ /** @file disobedience/queue-generic.c - * @brief Queue widgets + * @brief Disobedience queue widgets * * This file provides contains code shared between all the queue-like * widgets - the queue, the recent list and the added tracks list. @@@ -127,8 -129,6 +127,8 @@@ const char *column_length(const struct if(last_state & DISORDER_TRACK_PAUSED) l = playing_track->sofar; else { + if(!last_playing) + return NULL; time(&now); l = playing_track->sofar + (now - last_playing); } @@@ -143,7 -143,7 +143,7 @@@ /** @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) { @@@ -178,14 -178,11 +178,14 @@@ void ql_update_row(struct queue_entry * iter = my_iter; } /* Update all the columns */ - for(int col = 0; col < ql->ncolumns; ++col) - gtk_list_store_set(ql->store, iter, - col, ql->columns[col].value(q, - ql->columns[col].data), - -1); + for(int col = 0; col < ql->ncolumns; ++col) { + const char *const v = ql->columns[col].value(q, + ql->columns[col].data); + if(v) + gtk_list_store_set(ql->store, iter, + col, v, + -1); + } gtk_list_store_set(ql->store, iter, ql->ncolumns + QUEUEPOINTER_COLUMN, q, -1); @@@ -402,6 -399,120 +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 -558,17 +561,17 @@@ 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(); diff --combined disobedience/queue-generic.h index 4b31fe9,2bf33e7..8dd9fdb --- a/disobedience/queue-generic.h +++ b/disobedience/queue-generic.h @@@ -2,21 -2,20 +2,21 @@@ * This file is part of DisOrder * 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 . + */ +/** @file disobedience/queue-generic.h + * @brief Disobedience queue widgets */ #ifndef QUEUE_GENERIC_H #define QUEUE_GENERIC_H @@@ -90,6 -89,18 +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 { @@@ -134,9 -145,6 +146,9 @@@ void ql_remove_activate(GtkMenuItem *me int ql_play_sensitive(void *extra); void ql_play_activate(GtkMenuItem *menuitem, gpointer user_data); +int ql_adopt_sensitive(void *extra); +void ql_adopt_activate(GtkMenuItem *menuitem, + gpointer user_data); gboolean ql_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data); diff --combined disobedience/queue.c index 6dac960,c35b90b..b6eae61 --- a/disobedience/queue.c +++ b/disobedience/queue.c @@@ -2,21 -2,20 +2,21 @@@ * This file is part of DisOrder * 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 . + */ +/** @file disobedience/queue.c + * @brief Disobedience queue widget */ #include "disobedience.h" #include "popup.h" @@@ -29,10 -28,7 +29,10 @@@ static struct queue_entry *actual_playi /** @brief The playing track */ struct queue_entry *playing_track; -/** @brief When we last got the playing track */ +/** @brief When we last got the playing track + * + * Set to 0 if the timings are currently off due to having just unpaused. + */ time_t last_playing; static void queue_completed(void *v, @@@ -44,6 -40,7 +44,6 @@@ static void playing_completed(void *v /** @brief Called when either the actual queue or the playing track change */ static void queue_playing_changed(void) { - /* Check that the playing track isn't in the queue. There's a race here due * to the fact that we issue the two commands at slightly different times. * If it goes wrong we re-issue and try again, so that we never offer up an @@@ -69,6 -66,7 +69,6 @@@ playing_track = NULL; q = actual_queue; } - time(&last_playing); /* for column_length() */ ql_new_queue(&ql_queue, q); /* Tell anyone who cares */ event_raise("queue-list-changed", q); @@@ -97,7 -95,6 +97,7 @@@ static void playing_completed(void attr } actual_playing_track = q; queue_playing_changed(); + time(&last_playing); } /** @brief Schedule an update to the queue @@@ -121,9 -118,6 +121,9 @@@ static void playing_changed(const char void attribute((unused)) *callbackdata) { D(("playing_changed")); gtk_label_set_text(GTK_LABEL(report_label), "updating playing track"); + /* Setting last_playing=0 means that we don't know what the correct value + * is right now, e.g. because things have been deranged by a pause. */ + last_playing = 0; disorder_eclient_playing(client, playing_completed, 0); } @@@ -152,6 -146,61 +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 }, @@@ -169,7 -218,6 +224,7 @@@ static struct menuitem queue_menuitems[ { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 }, { "Scratch playing track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 }, { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 }, + { "Adopt track", ql_adopt_activate, ql_adopt_sensitive, 0, 0 }, }; struct queuelike ql_queue = { @@@ -178,161 -226,10 +233,10 @@@ .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 -250,6 +257,6 @@@ 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); diff --combined doc/disorder.1.in index 784083c,b045f10..784b37a --- a/doc/disorder.1.in +++ b/doc/disorder.1.in @@@ -1,18 -1,20 +1,18 @@@ .\" .\" Copyright (C) 2004-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 . .\" .TH disorder 1 .SH NAME @@@ -62,10 -64,6 +62,10 @@@ Create a new user If \fIRIGHTS\fR is not specified then the \fBdefault_rights\fR setting from the server's configuration file applies. .TP +.B adopt \fIID\fR +Adopts track \fIID\fR (in the queue). +The track will show up as submitted by the calling user. +.TP .B authorize \fIUSERNAME\fR [\fIRIGHTS\fR] Create user \fIUSERNAME\fR with a random password. User \fIUSERNAME\fR must be a UNIX login user (not just any old string). @@@ -152,6 -150,23 +152,23 @@@ Add \fITRACKS\fR to the end of the queu .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). diff --combined doc/disorder_protocol.5.in index abd0caa,5a8a481..4ad1d42 --- a/doc/disorder_protocol.5.in +++ b/doc/disorder_protocol.5.in @@@ -1,18 -1,20 +1,18 @@@ .\" .\" Copyright (C) 2004-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 . .\" .TH disorder_protocol 5 .SH NAME @@@ -38,6 -40,15 +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 -58,6 +56,6 @@@ All commands require the connection to 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. @@@ -57,10 -66,6 +64,10 @@@ then the \fBdefault_rights\fR setting a Requires the \fBadmin\fR right, and only works on local connections. .TP +.B adopt \fIID\fR +Adopts a randomly picked track, leaving it in a similar state to if it was +picked by this user. Requires the \fBplay\fR right. +.TP .B allfiles \fIDIRECTORY\fR [\fIREGEXP\fR] List all the files and directories in \fIDIRECTORY\fR in a response body. If \fIREGEXP\fR is present only matching files and directories are returned. @@@ -208,6 -213,43 +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 @@@ -499,26 -541,6 +543,26 @@@ The time the track was played at .B scratched The user that scratched the track. .TP +.B origin +The origin of the track. Valid origins are: +.RS +.TP 12 +.B adopted +The track was originally randomly picked but has been adopted by a user. +.TP +.B picked +The track was picked by a user. +.TP +.B random +The track was randomly picked. +.TP +.B scheduled +The track was played from a scheduled action. +.TP +.B scratch +The track is a scratch sound. +.RE +.TP .B state The current track state. Valid states are: @@@ -527,6 -549,12 +571,6 @@@ .B failed The player failed (exited with nonzero status but wasn't scratched). .TP -.B isscratch -The track is actually a scratch. -.TP -.B no_player -No player could be found for the track. -.TP .B ok The track was played without any problems. .TP @@@ -536,9 -564,6 +580,9 @@@ The track was scratched .B started The track is currently playing. .TP +.B paused +Track is playing but paused. +.TP .B unplayed In the queue, hasn't been played yet. .TP @@@ -557,9 -582,6 +601,9 @@@ The time the track was added to the que .TP .B wstat The wait status of the player in decimal. +.PP +Note that \fBorigin\fR is new with DisOrder 4.3, and obsoletes some old +\fBstate\fR values. .SH NOTES Times are decimal integers using the server's \fBtime_t\fR. .PP @@@ -577,9 -599,6 +621,9 @@@ keyword followed by (optionally) parame The parameters are quoted in the usual DisOrder way. Currently the following keywords are used: .TP +.B adopted \fIID\fR \fIUSERNAME\fR +\fIUSERNAME\fR adopted track \fIID\fR. +.TP .B completed \fITRACK\fR Completed playing \fITRACK\fR .TP @@@ -593,6 -612,21 +637,21 @@@ Further details aren't included any mor .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 diff --combined lib/Makefile.am index f7d99ac,9df944f..ca0e82d --- a/lib/Makefile.am +++ b/lib/Makefile.am @@@ -2,18 -2,20 +2,18 @@@ # This file is part of DisOrder. # Copyright (C) 2004-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 . # noinst_LIBRARIES=libdisorder.a @@@ -73,12 -75,12 +73,13 @@@ libdisorder_a_SOURCES=charset.c charset sink.c sink.h \ speaker-protocol.c speaker-protocol.h \ split.c split.h \ + strptime.c strptime.h \ syscalls.c syscalls.h \ common.h \ table.c table.h \ timeval.h \ $(TRACKDB) trackdb.h trackdb-int.h \ + trackdb-playlists.c \ trackname.c trackorder.c trackname.h \ tracksort.c \ url.h url.c \ @@@ -107,12 -109,7 +108,12 @@@ versionstring.h: version-string ${top_s definitions.h: Makefile rm -f $@.new - echo "#define PKGLIBDIR \"${pkglibdir}\"" > $@.new + echo "/** @file lib/definitions.h" >> $@.new + echo " * @brief Definitions exported from makefile" >> $@.new + echo " *" >> $@.new + echo " * DO NOT EDIT." >> $@.new + echo " */" >> $@.new + echo "#define PKGLIBDIR \"${pkglibdir}\"" >> $@.new echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new @@@ -134,6 -131,6 +135,6 @@@ rebuild-unicode mv $@.new $@ CLEANFILES=definitions.h definitions.h.new version-string versionstring.h \ - *.gcda *.gcov *.gcno + *.gcda *.gcov *.gcno *.c.html index.html EXTRA_DIST=trackdb.c trackdb-stub.c diff --combined lib/client.c index 00d7cf5,5c4cac5..f8a2c9e --- a/lib/client.c +++ b/lib/client.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * Copyright (C) 2004-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 . */ /** @file lib/client.c * @brief Simple C client @@@ -153,6 -155,8 +153,8 @@@ static int check_response(disorder_clie * @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 -167,21 +165,21 @@@ * * 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 -200,32 +198,32 @@@ 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 -252,30 +250,30 @@@ static int disorder_simple(disorder_cli 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 -727,8 +725,8 @@@ static int readlist(disorder_client *c * *)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 -737,7 +735,7 @@@ 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); @@@ -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 --combined lib/client.h index 89f3037,37920ba..f7ff728 --- a/lib/client.h +++ b/lib/client.h @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * Copyright (C) 2004-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 . */ /** @file lib/client.h * @brief Simple C client @@@ -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 */ diff --combined lib/configuration.c index 2a0b89c,934e52b..cd83224 --- a/lib/configuration.c +++ b/lib/configuration.c @@@ -3,18 -3,20 +3,18 @@@ * Copyright (C) 2004-2008 Richard Kettlewell * Portions copyright (C) 2007 Mark Wooding * - * 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 . */ /** @file lib/configuration.c * @brief Configuration file support @@@ -950,6 -952,8 +950,8 @@@ static const struct conf conf[] = { C(noticed_history), &type_integer, validate_positive }, { C(password), &type_string, validate_any }, { C(player), &type_stringlist_accum, validate_player }, + { C(playlist_lock_timeout), &type_integer, validate_positive }, + { C(playlist_max) , &type_integer, validate_positive }, { C(plugins), &type_string_accum, validate_isdir }, { C(prefsync), &type_integer, validate_positive }, { C(queue_pad), &type_integer, validate_positive }, @@@ -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); @@@ -1247,7 -1252,7 +1251,7 @@@ static void config_postdefaults(struct int n; static const char *namepart[][4] = { - { "title", "/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" }, + { "title", "/([0-9]+ *[-:]? *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" }, { "title", "/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort" }, { "album", "/([^/]+)/[^/]+$", "$1", "*" }, { "artist", "/([^/]+)/[^/]+/[^/]+$", "$1", "*" }, @@@ -1256,7 -1261,7 +1260,7 @@@ #define NNAMEPART (int)(sizeof namepart / sizeof *namepart) static const char *transform[][5] = { - { "track", "^.*/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" }, + { "track", "^.*/([0-9]+ *[-:]? *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" }, { "track", "^.*/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort", "" }, { "dir", "^.*/([^/]+)$", "$1", "*", "" }, { "dir", "^(the) ([^/]*)", "$2, $1", "sort", "i", }, diff --combined lib/configuration.h index 624e495,b94b684..54fb4f8 --- a/lib/configuration.h +++ b/lib/configuration.h @@@ -3,18 -3,20 +3,18 @@@ * This file is part of DisOrder. * Copyright (C) 2004-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 . */ /** @file lib/configuration.h * @brief Configuration file support @@@ -183,6 -185,12 +183,12 @@@ struct config */ int api; + /** @brief Maximum size of a playlist */ + long playlist_max; + + /** @brief Maximum lifetime of a playlist lock */ + long playlist_lock_timeout; + /* These values had better be non-negative */ #define BACKEND_ALSA 0 /**< Use ALSA (Linux only) */ #define BACKEND_COMMAND 1 /**< Execute a command */ diff --combined lib/eclient.c index 40639f3,8e89a31..676aa06 --- a/lib/eclient.c +++ b/lib/eclient.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * 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 . */ /** @file lib/eclient.c * @brief Client code for event-driven programs @@@ -92,6 -94,7 +92,7 @@@ typedef void operation_callback(disorde 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 -168,8 +166,8 @@@ static void stash_command(disorder_ecli operation_callback *opcallback, void (*completed)(), void *v, + int nbody, + char **body, const char *cmd, ...); static void log_opcallback(disorder_eclient *c, struct operation *op); @@@ -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 ********************************************************************/ @@@ -203,11 -210,13 +209,14 @@@ struct logentry_handler /** @brief Table for parsing log entries */ static const struct logentry_handler logentry_handlers[] = { #define LE(X, MIN, MAX) { #X, MIN, MAX, logentry_##X } + LE(adopted, 2, 2), LE(completed, 1, 1), 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 +335,24 @@@ static int protocol_error(disorder_ecli /* 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 +406,7 @@@ void disorder_eclient_polled(disorder_e 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 +422,13 @@@ 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 +624,7 @@@ static void authbanner_opcallback(disor 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 +649,7 @@@ static void authuser_opcallback(disorde 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 +812,8 @@@ static void stash_command_vector(disord 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 +832,13 @@@ 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 +864,8 @@@ static void vstash_command(disorder_ecl 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 +877,11 @@@ 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 +889,14 @@@ 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 +1048,8 @@@ static void list_response_opcallback(di 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 +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 +1183,7 @@@ int disorder_eclient_moveafter(disorder 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; } @@@ -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 @@@ -1444,7 -1606,7 +1620,7 @@@ int disorder_eclient_log(disorder_eclie 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 -1774,27 +1788,27 @@@ static void logentry_rights_changed(dis } } + 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; @@@ -1694,12 -1877,6 +1891,12 @@@ char *disorder_eclient_interpret_state( return d->vec; } +static void logentry_adopted(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(c->log_callbacks->adopted) + c->log_callbacks->adopted(c->log_v, vec[0], vec[1]); +} + /* Local Variables: c-basic-offset:2 diff --combined lib/eclient.h index ce5c582,2bbc367..fae610c --- a/lib/eclient.h +++ b/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 . */ /** @file lib/eclient.h * @brief Client code for event-driven programs @@@ -125,7 -127,7 +125,7 @@@ typedef struct disorder_eclient_log_cal /** @brief Called when @p id is removed from the recent list */ void (*recent_removed)(void *v, const char *id); - /** @brief Called when @id is removed from the queue + /** @brief Called when @p id is removed from the queue * * @p user might be 0. */ @@@ -166,8 -168,14 +166,17 @@@ /** @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 */ @@@ -222,7 -230,8 +231,8 @@@ typedef void disorder_eclient_no_respon * * @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 -290,8 +291,8 @@@ typedef void disorder_eclient_queue_res * @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. @@@ -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 /* diff --combined lib/trackdb-int.h index 6d5234e,9431406..b21b5d9 --- a/lib/trackdb-int.h +++ b/lib/trackdb-int.h @@@ -2,26 -2,28 +2,27 @@@ * This file is part of DisOrder * Copyright (C) 2005, 2007 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 . */ - +/** @file lib/trackdb-int.h + * @brief Track database internals */ #ifndef TRACKDB_INT_H #define TRACKDB_INT_H #include + #include "trackdb.h" #include "kvp.h" struct vector; /* forward declaration */ @@@ -36,6 -38,7 +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 -154,7 +153,7 @@@ int trackdb_get_global_tid(const char * char **parsetags(const char *s); int tag_intersection(char **a, char **b); + int valid_username(const char *user); #endif /* TRACKDB_INT_H */ diff --combined lib/trackdb.c index e1bbfc8,a364446..b98752f --- a/lib/trackdb.c +++ b/lib/trackdb.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder * Copyright (C) 2005-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 . */ /** @file lib/trackdb.c * @brief Track database @@@ -157,6 -159,13 +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 */ @@@ -354,7 -363,7 +361,7 @@@ static DB *open_db(const char *path DBTYPE dbtype, u_int32_t openflags, int mode) { - int err; + int err, err2; DB *db; D(("open %s", path)); @@@ -369,14 -378,8 +376,14 @@@ fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err)); if((err = db->open(db, 0, path, 0, dbtype, openflags | DB_AUTO_COMMIT, mode))) { - if((openflags & DB_CREATE) || errno != ENOENT) + if((openflags & DB_CREATE) || errno != ENOENT) { + if((err2 = db->close(db, 0))) + error(0, "db->close: %s", db_strerror(err2)); + trackdb_close(); + trackdb_env->close(trackdb_env,0); + trackdb_env = 0; fatal(0, "db->open %s: %s", path, db_strerror(err)); + } db->close(db, 0); db = 0; } @@@ -472,6 -475,7 +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) { /* Stash the database version */ char buf[32]; @@@ -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")); } @@@ -1400,9 -1411,7 +1409,9 @@@ void trackdb_stats_subprocess(ev_sourc pid = subprogram(ev, p[1], "disorder-stats", (char *)0); xclose(p[1]); ev_child(ev, pid, 0, stats_finished, d); - ev_reader_new(ev, p[0], stats_read, stats_error, d, "disorder-stats reader"); + if(!ev_reader_new(ev, p[0], stats_read, stats_error, d, + "disorder-stats reader")) + fatal(0, "ev_reader_new for disorder-stats reader failed"); } /** @brief Parse a track name part preference @@@ -1758,9 -1767,8 +1767,9 @@@ int trackdb_request_random(ev_source *e choose_callback = callback; choose_output.nvec = 0; choose_complete = 0; - ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0, - "disorder-choose reader"); /* owns p[0] */ + if(!ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0, + "disorder-choose reader")) /* owns p[0] */ + fatal(0, "ev_reader_new for disorder-choose reader failed"); ev_child(ev, choose_pid, 0, choose_exited, 0); /* owns the subprocess */ return 0; } @@@ -2250,7 -2258,7 +2259,7 @@@ static int reap_rescan(ev_source attrib * @param ev Event loop or 0 to block * @param recheck 1 to recheck lengths, 0 to suppress check * @param rescanned Called on completion (if not NULL) - * @param u Passed to @p rescanned + * @param ru Passed to @p rescanned */ void trackdb_rescan(ev_source *ev, int recheck, void (*rescanned)(void *ru), @@@ -2553,8 -2561,10 +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) { diff --combined lib/trackdb.h index 0774579,97c761e..6b86651 --- a/lib/trackdb.h +++ b/lib/trackdb.h @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder * Copyright (C) 2005-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 . */ /** @file lib/trackdb.h * @brief Track database public interface */ @@@ -184,6 -186,25 +184,25 @@@ void trackdb_add_rescanned(void (*resca 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 */ /* diff --combined python/disorder.py.in index e873e49,47d7090..3e1541e --- a/python/disorder.py.in +++ b/python/disorder.py.in @@@ -1,18 -1,20 +1,18 @@@ # # Copyright (C) 2004, 2005, 2007, 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 . # """Python support for DisOrder @@@ -113,8 -115,8 +113,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. """ @@@ -422,8 -424,8 +422,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 @@@ -528,8 -530,8 +528,8 @@@ 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): @@@ -764,7 -766,8 +764,8 @@@ 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. @@@ -891,7 -894,8 +892,8 @@@ 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 @@@ -905,10 -909,54 +907,58 @@@ """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 @@@ -938,8 -986,8 +988,8 @@@ 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) @@@ -948,6 -996,13 +998,13 @@@ 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: @@@ -958,7 -1013,7 +1015,7 @@@ 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. @@@ -966,10 -1021,20 +1023,20 @@@ # 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() @@@ -1050,8 -1115,8 +1117,8 @@@ 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 @@@ -1067,8 -1132,8 +1134,8 @@@ 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): diff --combined scripts/completion.bash index 89eb63d,d18a31d..2766b17 --- a/scripts/completion.bash +++ b/scripts/completion.bash @@@ -2,18 -2,20 +2,18 @@@ # This file is part of DisOrder. # Copyright (C) 2005-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 . # complete -r disorder 2>/dev/null || true @@@ -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 --combined server/dump.c index e08b32f,f8be9cd..ab48e48 --- a/server/dump.c +++ b/server/dump.c @@@ -2,22 -2,22 +2,22 @@@ * This file is part of DisOrder. * Copyright (C) 2004, 2005, 2007, 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 . + */ +/** @file server/dump.c + * @brief Dump and restore database contents */ - #include "disorder-server.h" static const struct option options[] = { @@@ -29,8 -29,6 +29,6 @@@ { "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 +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 +126,18 @@@ 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 +224,6 @@@ static int undump_dbt(FILE *fp, const c /* 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 +236,28 @@@ 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 +266,15 @@@ 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 +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 +377,6 @@@ 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 +386,6 @@@ } 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 +408,7 @@@ 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); diff --combined server/server.c index 2874357,64d29e0..320cbb1 --- a/server/server.c +++ b/server/server.c @@@ -2,18 -2,20 +2,18 @@@ * This file is part of DisOrder. * Copyright (C) 2004-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 . */ #include "disorder-server.h" @@@ -39,6 -41,34 +39,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 */ @@@ -72,6 -102,18 +100,18 @@@ 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 */ @@@ -83,6 -125,15 +123,15 @@@ static int reader_callback(ev_source *e 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" }; @@@ -188,7 -239,7 +237,7 @@@ static int c_play(struct conn *c, char sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n"); return 1; } - q = queue_add(track, c->who, WHERE_BEFORE_RANDOM); + q = queue_add(track, c->who, WHERE_BEFORE_RANDOM, origin_picked); queue_write(); /* If we added the first track, and something is playing, then prepare the * new track. If nothing is playing then we don't bother as it wouldn't gain @@@ -1024,21 -1075,25 +1073,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) { @@@ -1305,17 -1360,7 +1358,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()); } /** @brief Base64 mapping table for confirmation strings @@@ -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; @@@ -1619,7 -1785,6 +1808,7 @@@ rights_type rights; } commands[] = { { "adduser", 2, 3, c_adduser, RIGHT_ADMIN|RIGHT__LOCAL }, + { "adopt", 1, 1, c_adopt, RIGHT_PLAY }, { "allfiles", 0, 2, c_allfiles, RIGHT_READ }, { "confirm", 1, 1, c_confirm, 0 }, { "cookie", 1, 1, c_cookie, 0 }, @@@ -1644,6 -1809,14 +1833,14 @@@ { "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 }, @@@ -1679,13 -1852,58 +1876,58 @@@ { "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; @@@ -1756,7 -1974,7 +1998,7 @@@ static int reader_callback(ev_source at 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) { @@@ -1802,23 -2020,14 +2044,24 @@@ static int listen_callback(ev_source *e c->ev = ev; c->w = ev_writer_new(ev, fd, writer_error, c, "client writer"); + if(!c->w) { + error(0, "ev_writer_new for file inbound connection (fd=%d) failed", + fd); + close(fd); + return 0; + } c->r = ev_reader_new(ev, fd, redirect_reader_callback, reader_error, c, "client reader"); + if(!c->r) + /* Main reason for failure is the FD is too big and that will already have + * been handled */ + fatal(0, "ev_reader_new for file inbound connection (fd=%d) failed", fd); ev_tie(c->r, c->w); c->fd = fd; 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", diff --combined tests/Makefile.am index 2508499,1ee9af9..be3d73b --- a/tests/Makefile.am +++ b/tests/Makefile.am @@@ -2,18 -2,20 +2,18 @@@ # This file is part of DisOrder. # Copyright (C) 2004, 2005, 2007, 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 . # noinst_PROGRAMS=disorder-udplog @@@ -26,7 -28,7 +26,7 @@@ disorder_udplog_DEPENDENCIES=../lib/lib TESTS=cookie.py dbversion.py dump.py files.py play.py queue.py \ recode.py search.py user-upgrade.py user.py aliases.py \ - schedule.py + schedule.py playlists.py TESTS_ENVIRONMENT=${PYTHON} -u @@@ -34,4 -36,3 +34,4 @@@ clean-local rm -rf testroot *.log *.pyc EXTRA_DIST=dtest.py ${TESTS} +CLEANFILES=*.gcda *.gcov *.gcno *.c.html index.html