* 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 <http://www.gnu.org/licenses/>.
+ */
+/** @file clients/disorder.c
+ * @brief Command-line client
*/
#include "common.h"
#include <unistd.h>
#include <pcre.h>
#include <ctype.h>
+ #include <langinfo.h>
#include "configuration.h"
#include "syscalls.h"
#include "vector.h"
#include "version.h"
#include "dateparse.h"
+ #include "inputline.h"
static disorder_client *client;
static void 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);
}
static void cf_quack(char attribute((unused)) **argv) {
- xprintf("\n"
- " .------------------.\n"
- " | Naath is a babe! |\n"
- " `---------+--------'\n"
- " \\\n"
- " >0\n"
- " (<)'\n"
- "~~~~~~~~~~~~~~~~~~~~~~\n"
- "\n");
+ if(!strcasecmp(nl_langinfo(CODESET), "utf-8")) {
+ #define TL "\xE2\x95\xAD"
+ #define TR "\xE2\x95\xAE"
+ #define BR "\xE2\x95\xAF"
+ #define BL "\xE2\x95\xB0"
+ #define H "\xE2\x94\x80"
+ #define V "\xE2\x94\x82"
+ #define T "\xE2\x94\xAC"
+ xprintf("\n"
+ " "TL H H H H H H H H H H H H H H H H H H TR"\n"
+ " "V" Naath is a babe! "V"\n"
+ " "BL H H H H H H H H H T H H H H H H H H BR"\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ } else {
+ xprintf("\n"
+ " .------------------.\n"
+ " | Naath is a babe! |\n"
+ " `---------+--------'\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ }
}
static void cf_somelist(char **argv,
exit(EXIT_FAILURE);
}
+static void cf_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;
} 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]",
"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 },
# 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 <http://www.gnu.org/licenses/>.
#
bin_PROGRAMS=disobedience
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)
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
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file disobedience/disobedience.c
* @brief Main Disobedience program
/* Update everything to be sure that the connection to the server hasn't
* mysteriously gone stale on us. */
all_update();
+ event_raise("periodic-slow", 0);
/* Recheck RTP status too */
check_rtp_address(0, 0, 0);
return TRUE; /* don't remove me */
recheck_rights = 0;
if(recheck_rights)
check_rights();
+ event_raise("periodic-fast", 0);
return TRUE;
}
disorder_eclient_version(client, version_completed, 0);
event_register("log-connected", check_rtp_address, 0);
suppress_actions = 0;
+ playlists_init();
/* If no password is set yet pop up a login box */
if(!config->password)
login_box();
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file disobedience/disobedience.h
* @brief Header file for Disobedience, the DisOrder GTK+ client
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 */
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file disobedience/log.c
* @brief State monitoring
static void log_volume(void *v, int l, int r);
static void log_rescanned(void *v);
static void log_rights_changed(void *v, rights_type r);
+static void log_adopted(void *v, const char *id, const char *user);
+ static void log_playlist_created(void *v,
+ const char *playlist, const char *sharing);
+ static void log_playlist_modified(void *v,
+ const char *playlist, const char *sharing);
+ static void log_playlist_deleted(void *v,
+ const char *playlist);
/** @brief Callbacks for server state monitoring */
const disorder_eclient_log_callbacks log_callbacks = {
.volume = log_volume,
.rescanned = log_rescanned,
.rights_changed = log_rights_changed,
- .adopted = log_adopted
++ .adopted = log_adopted,
+ .playlist_created = log_playlist_created,
+ .playlist_modified = log_playlist_modified,
+ .playlist_deleted = log_playlist_deleted,
};
/** @brief Update everything */
--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
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file disobedience/menu.c
* @brief Main menu
static GtkWidget *selectall_widget;
static GtkWidget *selectnone_widget;
static GtkWidget *properties_widget;
+ GtkWidget *playlists_widget;
+ GtkWidget *playlists_menu;
+ GtkWidget *editplaylists_widget;
/** @brief Main menu widgets */
GtkItemFactory *mainmenufactory;
&& t->selectnone_sensitive(t->extra));
}
}
-
+
/** @brief Fetch version in order to display the about... popup */
static void about_popup(gpointer attribute((unused)) callback_data,
guint attribute((unused)) callback_action,
0, /* item_type */
0 /* extra_data */
},
+ {
+ (char *)"/Edit/Edit playlists", /* path */
+ 0, /* accelerator */
+ edit_playlists, /* callback */
+ 0, /* callback_action */
+ 0, /* item_type */
+ 0 /* extra_data */
+ },
+
{
(char *)"/Control", /* path */
(char *)"<CheckItem>", /* item_type */
0 /* extra_data */
},
+ {
+ (char *)"/Control/Activate playlist", /* path */
+ 0, /* accelerator */
+ 0, /* callback */
+ 0, /* callback_action */
+ (char *)"<Branch>", /* item_type */
+ 0 /* extra_data */
+ },
{
(char *)"/Help", /* path */
"<GdisorderMain>/Edit/Deselect all tracks");
properties_widget = gtk_item_factory_get_widget(mainmenufactory,
"<GdisorderMain>/Edit/Track properties");
+ playlists_widget = gtk_item_factory_get_item(mainmenufactory,
+ "<GdisorderMain>/Control/Activate playlist");
+ playlists_menu = gtk_item_factory_get_widget(mainmenufactory,
+ "<GdisorderMain>/Control/Activate playlist");
+ editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
+ "<GdisorderMain>/Edit/Edit playlists");
assert(selectall_widget != 0);
assert(selectnone_widget != 0);
assert(properties_widget != 0);
+ assert(playlists_widget != 0);
+ assert(playlists_menu != 0);
+ assert(editplaylists_widget != 0);
-
GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory,
"<GdisorderMain>/Edit");
g_signal_connect(edit_widget, "show", G_CALLBACK(edit_menu_show), 0);
-
+
event_register("rights-changed", menu_rights_changed, 0);
users_set_sensitive(0);
m = gtk_item_factory_get_widget(mainmenufactory,
* 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 <http://www.gnu.org/licenses/>.
*/
/** @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.
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);
}
/** @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) {
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);
--suppress_actions;
}
+ /* Drag and drop has to be figured out experimentally, because it is not well
+ * documented.
+ *
+ * First you get a row-inserted. The path argument points to the destination
+ * row but this will not yet have had its values set. The source row is still
+ * present. AFAICT the iter argument points to the same place.
+ *
+ * Then you get a row-deleted. The path argument identifies the row that was
+ * deleted. By this stage the row inserted above has acquired its values.
+ *
+ * A complication is that the deletion will move the inserted row. For
+ * instance, if you do a drag that moves row 1 down to after the track that was
+ * formerly on row 9, in the row-inserted call it will show up as row 10, but
+ * in the row-deleted call, row 1 will have been deleted thus making the
+ * inserted row be row 9.
+ *
+ * So when we see the row-inserted we have no idea what track to move.
+ * Therefore we stash it until we see a row-deleted.
+ */
+
+ /** @brief row-inserted callback */
+ static void ql_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
+ GtkTreePath *path,
+ GtkTreeIter attribute((unused)) *iter,
+ gpointer user_data) {
+ struct queuelike *const ql = user_data;
+ if(!suppress_actions) {
+ #if 0
+ char *ps = gtk_tree_path_to_string(path);
+ GtkTreeIter piter[1];
+ gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
+ struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
+ struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
+
+ fprintf(stderr, "row-inserted %s path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
+ ql->name,
+ ps,
+ pi,
+ pq,
+ (pi
+ ? (pq ? pq->track : "(pq=0)")
+ : "(pi=FALSE)"),
+ iq,
+ iq ? iq->track : "(iq=0)");
+
+ GtkTreeIter j[1];
+ gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+ int row = 0;
+ while(jt) {
+ struct queue_entry *q = ql_iter_to_q(treemodel, j);
+ fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+ jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j);
+ }
+ g_free(ps);
+ #endif
+ /* Remember an iterator pointing at the insertion target */
+ if(ql->drag_target)
+ gtk_tree_path_free(ql->drag_target);
+ ql->drag_target = gtk_tree_path_copy(path);
+ }
+ }
+
+ /** @brief row-deleted callback */
+ static void ql_row_deleted(GtkTreeModel attribute((unused)) *treemodel,
+ GtkTreePath *path,
+ gpointer user_data) {
+ struct queuelike *const ql = user_data;
+
+ if(!suppress_actions) {
+ #if 0
+ char *ps = gtk_tree_path_to_string(path);
+ fprintf(stderr, "row-deleted %s path=%s ql->drag_target=%s\n",
+ ql->name, ps, gtk_tree_path_to_string(ql->drag_target));
+ GtkTreeIter j[1];
+ gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+ int row = 0;
+ while(jt) {
+ struct queue_entry *q = ql_iter_to_q(treemodel, j);
+ fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+ jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j);
+ }
+ g_free(ps);
+ #endif
+ if(!ql->drag_target) {
+ error(0, "%s: unsuppressed row-deleted with no row-inserted",
+ ql->name);
+ return;
+ }
+
+ /* Get the source and destination row numbers. */
+ int srcrow = gtk_tree_path_get_indices(path)[0];
+ int dstrow = gtk_tree_path_get_indices(ql->drag_target)[0];
+ //fprintf(stderr, "srcrow=%d dstrow=%d\n", srcrow, dstrow);
+
+ /* Note that the source row is computed AFTER the destination has been
+ * inserted, since GTK+ does the insert before the delete. Therefore if
+ * the source row is south (higher row number) of the destination, it will
+ * be one higher than expected.
+ *
+ * For instance if we drag row 1 to before row 0 we will see row-inserted
+ * for row 0 but then a row-deleted for row 2.
+ */
+ if(srcrow > dstrow)
+ --srcrow;
+
+ /* Tell the queue implementation */
+ ql->drop(srcrow, dstrow);
+
+ /* Dispose of stashed data */
+ gtk_tree_path_free(ql->drag_target);
+ ql->drag_target = 0;
+ }
+ }
+
/** @brief Initialize a @ref queuelike */
GtkWidget *init_queuelike(struct queuelike *ql) {
D(("init_queuelike"));
g_signal_connect(ql->view, "button-press-event",
G_CALLBACK(ql_button_release), ql);
+ /* Drag+drop*/
+ if(ql->drop) {
+ gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql->view), TRUE);
+ g_signal_connect(ql->store,
+ "row-inserted",
+ G_CALLBACK(ql_row_inserted), ql);
+ g_signal_connect(ql->store,
+ "row-deleted",
+ G_CALLBACK(ql_row_deleted), ql);
+ }
+
/* TODO style? */
ql->init();
* 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 <http://www.gnu.org/licenses/>.
+ */
+/** @file disobedience/queue-generic.h
+ * @brief Disobedience queue widgets
*/
#ifndef QUEUE_GENERIC_H
#define QUEUE_GENERIC_H
/** @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 {
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);
* 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 <http://www.gnu.org/licenses/>.
+ */
+/** @file disobedience/queue.c
+ * @brief Disobedience queue widget
*/
#include "disobedience.h"
#include "popup.h"
/** @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,
/** @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
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);
}
actual_playing_track = q;
queue_playing_changed();
+ time(&last_playing);
}
/** @brief Schedule an update to the queue
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);
}
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 },
{ "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 = {
.columns = queue_columns,
.ncolumns = sizeof queue_columns / sizeof *queue_columns,
.menuitems = queue_menuitems,
- .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems
+ .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems,
+ .drop = queue_drop
};
- /* Drag and drop has to be figured out experimentally, because it is not well
- * documented.
- *
- * First you get a row-inserted. The path argument points to the destination
- * row but this will not yet have had its values set. The source row is still
- * present. AFAICT the iter argument points to the same place.
- *
- * Then you get a row-deleted. The path argument identifies the row that was
- * deleted. By this stage the row inserted above has acquired its values.
- *
- * A complication is that the deletion will move the inserted row. For
- * instance, if you do a drag that moves row 1 down to after the track that was
- * formerly on row 9, in the row-inserted call it will show up as row 10, but
- * in the row-deleted call, row 1 will have been deleted thus making the
- * inserted row be row 9.
- *
- * So when we see the row-inserted we have no idea what track to move.
- * Therefore we stash it until we see a row-deleted.
- */
-
- /** @brief Target row for drag */
- static int queue_drag_target = -1;
-
- static void queue_move_completed(void attribute((unused)) *v,
- const char *err) {
- if(err) {
- popup_protocol_error(0, err);
- return;
- }
- /* The log should tell us the queue changed so we do no more here */
- }
-
- static void queue_row_deleted(GtkTreeModel *treemodel,
- GtkTreePath *path,
- gpointer attribute((unused)) user_data) {
- if(!suppress_actions) {
- #if 0
- char *ps = gtk_tree_path_to_string(path);
- fprintf(stderr, "row-deleted path=%s queue_drag_target=%d\n",
- ps, queue_drag_target);
- GtkTreeIter j[1];
- gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
- int row = 0;
- while(jt) {
- struct queue_entry *q = ql_iter_to_q(treemodel, j);
- fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
- jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
- }
- g_free(ps);
- #endif
- if(queue_drag_target < 0) {
- error(0, "unsuppressed row-deleted with no row-inserted");
- return;
- }
- int drag_source = gtk_tree_path_get_indices(path)[0];
-
- /* If the drag is downwards (=towards higher row numbers) then the target
- * will have been moved upwards (=towards lower row numbers) by one row. */
- if(drag_source < queue_drag_target)
- --queue_drag_target;
-
- /* Find the track to move */
- GtkTreeIter src[1];
- gboolean srcv = gtk_tree_model_iter_nth_child(treemodel, src, NULL,
- queue_drag_target);
- if(!srcv) {
- error(0, "cannot get iterator to drag target %d", queue_drag_target);
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
- struct queue_entry *srcq = ql_iter_to_q(treemodel, src);
- assert(srcq);
- //fprintf(stderr, "move %s %s\n", srcq->id, srcq->track);
-
- /* Don't allow the currently playing track to be moved. As above, we put
- * the queue back into the right order straight away. */
- if(srcq == playing_track) {
- //fprintf(stderr, "cannot move currently playing track\n");
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
-
- /* Find the destination */
- struct queue_entry *dstq;
- if(queue_drag_target) {
- GtkTreeIter dst[1];
- gboolean dstv = gtk_tree_model_iter_nth_child(treemodel, dst, NULL,
- queue_drag_target - 1);
- if(!dstv) {
- error(0, "cannot get iterator to drag target predecessor %d",
- queue_drag_target - 1);
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
- dstq = ql_iter_to_q(treemodel, dst);
- assert(dstq);
- if(dstq == playing_track)
- dstq = 0;
- } else
- dstq = 0;
- /* NB if the user attempts to move a queued track before the currently
- * playing track we assume they just missed a bit, and put it after. */
- //fprintf(stderr, " target %s %s\n", dstq ? dstq->id : "(none)", dstq ? dstq->track : "(none)");
- /* Now we know what is to be moved. We need to know the preceding queue
- * entry so we can move it. */
- disorder_eclient_moveafter(client,
- dstq ? dstq->id : "",
- 1, &srcq->id,
- queue_move_completed, NULL);
- queue_drag_target = -1;
- }
- }
-
- static void queue_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
- GtkTreePath *path,
- GtkTreeIter attribute((unused)) *iter,
- gpointer attribute((unused)) user_data) {
- if(!suppress_actions) {
- #if 0
- char *ps = gtk_tree_path_to_string(path);
- GtkTreeIter piter[1];
- gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
- struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
- struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
-
- fprintf(stderr, "row-inserted path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
- ps,
- pi,
- pq,
- (pi
- ? (pq ? pq->track : "(pq=0)")
- : "(pi=FALSE)"),
- iq,
- iq ? iq->track : "(iq=0)");
-
- GtkTreeIter j[1];
- gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
- int row = 0;
- while(jt) {
- struct queue_entry *q = ql_iter_to_q(treemodel, j);
- fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
- jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
- }
- g_free(ps);
- #endif
- queue_drag_target = gtk_tree_path_get_indices(path)[0];
- }
- }
-
/** @brief Called when a key is pressed in the queue tree view */
static gboolean queue_key_press(GtkWidget attribute((unused)) *widget,
GdkEventKey *event,
GtkWidget *queue_widget(void) {
GtkWidget *const w = init_queuelike(&ql_queue);
- /* Enable drag+drop */
- gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql_queue.view), TRUE);
- g_signal_connect(ql_queue.store,
- "row-inserted",
- G_CALLBACK(queue_row_inserted), &ql_queue);
- g_signal_connect(ql_queue.store,
- "row-deleted",
- G_CALLBACK(queue_row_deleted), &ql_queue);
/* Catch keypresses */
g_signal_connect(ql_queue.view, "key-press-event",
G_CALLBACK(queue_key_press), &ql_queue);
.\"
.\" 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 <http://www.gnu.org/licenses/>.
.\"
.TH disorder 1
.SH NAME
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).
.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).
.\"
.\" 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 <http://www.gnu.org/licenses/>.
.\"
.TH disorder_protocol 5
.SH NAME
Bodies borrow their syntax from RFC821; they consist of zero or more ordinary
lines, with any initial full stop doubled up, and are terminated by a line
consisting of a full stop and a line feed.
+ .PP
+ Commands only have a body if explicitly stated below.
+ If they do have a body then the body should always be sent immediately;
+ unlike (for instance) the SMTP "DATA" command there is no intermediate step
+ where the server asks for the body to be sent.
+ .PP
+ Replies also only have a body if stated below.
+ The presence of a reply body can always be inferred from the response code;
+ if the last digit is a 3 then a body is present, otherwise it is not.
.SH COMMANDS
Commands always have a command name as the first field of the line; responses
always have a 3-digit response code as the first field.
stated otherwise.
If not stated otherwise, the \fBread\fR right is sufficient to execute
the command.
- .PP
- Neither commands nor responses have a body unless stated otherwise.
.TP
.B adduser \fIUSERNAME PASSWORD \fR[\fIRIGHTS\fR]
Create a new user with the given username and password.
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.
.IP
If the response is \fB259\fR then nothing is playing.
.TP
+ .B playlist-delete \fIPLAYLIST\fR
+ Delete a playlist.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ .TP
+ .B playlist-get \fIPLAYLIST\fR
+ Get the contents of a playlist, in a response body.
+ Requires permission to read that playlist and the \fBread\fR right.
+ .TP
+ .B playlist-get-share \fIPLAYLIST\fR
+ Get the sharing status of a playlist.
+ The result will be \fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+ Requires permission to read that playlist and the \fBread\fR right.
+ .TP
+ .B playlist-lock \fIPLAYLIST\fR
+ Lock a playlist.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ Only one playlist may be locked at a time on a given connection and the lock
+ automatically expires when the connection is closed.
+ .TP
+ .B playlist-set \fIPLAYLIST\fR
+ Set the contents of a playlist.
+ The new contents should be supplied in a command body.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ The playlist must be locked.
+ .TP
+ .B playlist-set-share \fIPLAYLIST\fR \fISHARE\fR
+ Set the sharing status of a playlist to
+ \fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ .TP
+ .B playlist-unlock\fR
+ Unlock the locked playlist.
+ .TP
+ .B playlists
+ List all playlists that this connection has permission to read.
+ Requires the \fBread\fR right.
+ .TP
.B prefs \fBTRACK\fR
Send back the preferences for \fITRACK\fR in a response body.
Each line of the response has the usual line syntax, the first field being the
.B 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:
.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
.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
.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
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
.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
# 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 <http://www.gnu.org/licenses/>.
#
noinst_LIBRARIES=libdisorder.a
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 \
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
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
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/client.c
* @brief Simple C client
* @param c Client
* @param rp Where to store result, or NULL
* @param cmd Command
+ * @param body Body or NULL
+ * @param nbody Length of body or -1
* @param ap Arguments (UTF-8), terminated by (char *)0
* @return 0 on success, non-0 on error
*
*
* NB that the response will NOT be converted to the local encoding
* nor will quotes be stripped. See dequote().
+ *
+ * If @p body is not NULL then the body is sent immediately after the
+ * command. @p nbody should be the number of lines or @c -1 to count
+ * them if @p body is NULL-terminated.
+ *
+ * Usually you would call this via one of the following interfaces:
+ * - disorder_simple()
+ * - disorder_simple_body()
+ * - disorder_simple_list()
*/
static int disorder_simple_v(disorder_client *c,
char **rp,
- const char *cmd, va_list ap) {
+ const char *cmd,
+ char **body, int nbody,
+ va_list ap) {
const char *arg;
struct dynstr d;
dynstr_append(&d, '\n');
dynstr_terminate(&d);
D(("command: %s", d.vec));
- if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) {
- byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
- error(errno, "error writing to %s", c->ident);
- return -1;
+ if(fputs(d.vec, c->fpout) < 0)
+ goto write_error;
+ if(body) {
+ if(nbody < 0)
+ for(nbody = 0; body[nbody]; ++nbody)
+ ;
+ for(int n = 0; n < nbody; ++n) {
+ if(body[n][0] == '.')
+ if(fputc('.', c->fpout) < 0)
+ goto write_error;
+ if(fputs(body[n], c->fpout) < 0)
+ goto write_error;
+ if(fputc('\n', c->fpout) < 0)
+ goto write_error;
+ }
+ if(fputs(".\n", c->fpout) < 0)
+ goto write_error;
}
+ if(fflush(c->fpout))
+ goto write_error;
}
return check_response(c, rp);
+ write_error:
+ byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
+ error(errno, "error writing to %s", c->ident);
+ return -1;
}
/** @brief Issue a command and parse a simple response
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, rp, cmd, ap);
+ ret = disorder_simple_v(c, rp, cmd, 0, 0, ap);
+ va_end(ap);
+ return ret;
+ }
+
+ /** @brief Issue a command with a body and parse a simple response
+ * @param c Client
+ * @param rp Where to store result, or NULL (UTF-8)
+ * @param body Pointer to body
+ * @param nbody Size of body
+ * @param cmd Command
+ * @return 0 on success, non-0 on error
+ *
+ * See disorder_simple().
+ */
+ static int disorder_simple_body(disorder_client *c,
+ char **rp,
+ char **body, int nbody,
+ const char *cmd, ...) {
+ va_list ap;
+ int ret;
+
+ va_start(ap, cmd);
+ ret = disorder_simple_v(c, rp, cmd, body, nbody, ap);
va_end(ap);
return ret;
}
* *)0. They should be in UTF-8.
*
* 5xx responses count as errors.
+ *
+ * See disorder_simple().
*/
static int disorder_simple_list(disorder_client *c,
char ***vecp, int *nvecp,
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, 0, cmd, ap);
+ ret = disorder_simple_v(c, 0, cmd, 0, 0, ap);
va_end(ap);
if(ret) return ret;
return readlist(c, vecp, nvecp);
return 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
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/client.h
* @brief Simple C client
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 */
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/configuration.c
* @brief Configuration file support
{ C(noticed_history), &type_integer, validate_positive },
{ C(password), &type_string, validate_any },
{ C(player), &type_stringlist_accum, validate_player },
+ { C(playlist_lock_timeout), &type_integer, validate_positive },
+ { C(playlist_max) , &type_integer, validate_positive },
{ C(plugins), &type_string_accum, validate_isdir },
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
c->new_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);
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", "*" },
#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", },
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/configuration.h
* @brief Configuration file support
*/
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 */
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/eclient.c
* @brief Client code for event-driven programs
struct operation {
struct operation *next; /**< @brief next operation */
char *cmd; /**< @brief command to send or 0 */
+ char **body; /**< @brief command body */
operation_callback *opcallback; /**< @brief internal completion callback */
void (*completed)(); /**< @brief user completion callback or 0 */
void *v; /**< @brief data for COMPLETED */
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd,
...);
static void log_opcallback(disorder_eclient *c, struct operation *op);
static void logentry_user_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 ********************************************************************/
/** @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),
/* State machine *************************************************************/
+ /** @brief Send an operation (into the output buffer)
+ * @param op Operation to send
+ */
+ static void op_send(struct operation *op) {
+ disorder_eclient *const c = op->client;
+ put(c, op->cmd, strlen(op->cmd));
+ if(op->body) {
+ for(int n = 0; op->body[n]; ++n) {
+ if(op->body[n][0] == '.')
+ put(c, ".", 1);
+ put(c, op->body[n], strlen(op->body[n]));
+ put(c, "\n", 1);
+ }
+ put(c, ".\n", 2);
+ }
+ op->sent = 1;
+ }
+
/** @brief Called when there's something to do
* @param c Client
* @param mode bitmap of @ref DISORDER_POLL_READ and/or @ref DISORDER_POLL_WRITE.
D(("state_connected"));
/* We just connected. Initiate the authentication protocol. */
stash_command(c, 1/*queuejump*/, authbanner_opcallback,
- 0/*completed*/, 0/*v*/, 0/*cmd*/);
+ 0/*completed*/, 0/*v*/, -1/*nbody*/, 0/*body*/, 0/*cmd*/);
/* We never stay is state_connected very long. We could in principle jump
* straight to state_cmdresponse since there's actually no command to
* send, but that would arguably be cheating. */
if(c->authenticated) {
/* Transmit all unsent operations */
for(op = c->ops; op; op = op->next) {
- if(!op->sent) {
- put(c, op->cmd, strlen(op->cmd));
- op->sent = 1;
- }
+ if(!op->sent)
+ op_send(op);
}
} else {
/* Just send the head operation */
- if(c->ops->cmd && !c->ops->sent) {
- put(c, c->ops->cmd, strlen(c->ops->cmd));
- c->ops->sent = 1;
- }
+ if(c->ops->cmd && !c->ops->sent)
+ op_send(c->ops);
}
/* Awaiting response for the operation at the head of the list */
c->state = state_cmdresponse;
return;
}
stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
+ -1/*nbody*/, 0/*body*/,
"user", quoteutf8(config->username), quoteutf8(res),
(char *)0);
}
if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback))
/* We are a log client, switch to logging mode */
stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v,
+ -1/*nbody*/, 0/*body*/,
"log", (char *)0);
}
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
int ncmd,
char **cmd) {
struct operation *op = xmalloc(sizeof *op);
op->cmd = d.vec;
} else
op->cmd = 0; /* usually, awaiting challenge */
+ if(nbody >= 0) {
+ op->body = xcalloc(nbody + 1, sizeof (char *));
+ for(n = 0; n < nbody; ++n)
+ op->body[n] = xstrdup(body[n]);
+ op->body[n] = 0;
+ } else
+ op->body = NULL;
op->opcallback = opcallback;
op->completed = completed;
op->v = v;
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd, va_list ap) {
char *arg;
struct vector vec;
while((arg = va_arg(ap, char *)))
vector_append(&vec, arg);
stash_command_vector(c, queuejump, opcallback, completed, v,
- vec.nvec, vec.vec);
+ nbody, body, vec.nvec, vec.vec);
} else
- stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0);
+ stash_command_vector(c, queuejump, opcallback, completed, v,
+ nbody, body,
+ 0, 0);
}
static void stash_command(disorder_eclient *c,
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd,
...) {
va_list ap;
va_start(ap, cmd);
- vstash_command(c, queuejump, opcallback, completed, v, cmd, ap);
+ vstash_command(c, queuejump, opcallback, completed, v, nbody, body, cmd, ap);
va_end(ap);
}
D(("list_response_callback"));
if(c->rc / 100 == 2)
completed(op->v, NULL, c->vec.nvec, c->vec.vec);
+ else if(c->rc == 555)
+ completed(op->v, NULL, -1, NULL);
else
completed(op->v, errorstring(c), 0, 0);
}
va_list ap;
va_start(ap, cmd);
- vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap);
+ vstash_command(c, 0/*queuejump*/, opcallback, completed, v, -1, 0, cmd, ap);
+ va_end(ap);
+ /* Give the state machine a kick, since we might be in state_idle */
+ disorder_eclient_polled(c, 0);
+ return 0;
+ }
+
+ static int simple_body(disorder_eclient *c,
+ operation_callback *opcallback,
+ void (*completed)(),
+ void *v,
+ int nbody,
+ char **body,
+ const char *cmd, ...) {
+ va_list ap;
+
+ va_start(ap, cmd);
+ vstash_command(c, 0/*queuejump*/, opcallback, completed, v, nbody, body, cmd, ap);
va_end(ap);
/* Give the state machine a kick, since we might be in state_idle */
disorder_eclient_polled(c, 0);
for(n = 0; n < nids; ++n)
vector_append(&vec, (char *)ids[n]);
stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v,
- vec.nvec, vec.vec);
+ -1, 0, vec.nvec, vec.vec);
disorder_eclient_polled(c, 0);
return 0;
}
"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
if(c->log_callbacks->state)
c->log_callbacks->state(c->log_v, c->statebits);
stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v,
- "log", (char *)0);
+ -1, 0, "log", (char *)0);
disorder_eclient_polled(c, 0);
return 0;
}
}
}
+ static void logentry_playlist_created(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_created)
+ c->log_callbacks->playlist_created(c->log_v, vec[0], vec[1]);
+ }
+
+ static void logentry_playlist_deleted(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_deleted)
+ c->log_callbacks->playlist_deleted(c->log_v, vec[0]);
+ }
+
+ static void logentry_playlist_modified(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_modified)
+ c->log_callbacks->playlist_modified(c->log_v, vec[0], vec[1]);
+ }
+
static const struct {
unsigned long bit;
const char *enable;
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
/*
* This file is part of DisOrder.
- * Copyright (C) 2006, 2007 Richard Kettlewell
+ * Copyright (C) 2006-2008 Richard Kettlewell
*
- * This program is free software; you can redistribute it and/or modify
+ * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
+ * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
* You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file lib/eclient.h
* @brief Client code for event-driven programs
/** @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.
*/
/** @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 */
*
* @p error will be NULL on success. In this case @p value will be the result
* (which might be NULL for disorder_eclient_get(),
- * disorder_eclient_get_global() and disorder_eclient_userinfo()).
+ * disorder_eclient_get_global(), disorder_eclient_userinfo() and
+ * disorder_eclient_playlist_get_share()).
*
* @p error will be non-NULL on failure. In this case @p value is always NULL.
*/
* @param vec Pointer to response list
*
* @p error will be NULL on success. In this case @p nvec and @p vec will give
- * the result.
+ * the result, or be -1 and NULL respectively e.g. from
+ * disorder_eclient_playlist_get() if there is no such playlist.
*
* @p error will be non-NULL on failure. In this case @p nvec and @p vec will
* be 0 and NULL.
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
/*
* 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 <http://www.gnu.org/licenses/>.
*/
-
+/** @file lib/trackdb-int.h
+ * @brief Track database internals */
#ifndef TRACKDB_INT_H
#define TRACKDB_INT_H
#include <db.h>
+ #include "trackdb.h"
#include "kvp.h"
struct vector; /* forward declaration */
extern DB *trackdb_globaldb;
extern DB *trackdb_usersdb;
extern DB *trackdb_scheduledb;
+ extern DB *trackdb_playlistsdb;
DBC *trackdb_opencursor(DB *db, DB_TXN *tid);
/* open a transaction */
char **parsetags(const char *s);
int tag_intersection(char **a, char **b);
+ int valid_username(const char *user);
#endif /* TRACKDB_INT_H */
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/trackdb.c
* @brief Track database
*/
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 */
DBTYPE dbtype,
u_int32_t openflags,
int mode) {
- int err;
+ int err, err2;
DB *db;
D(("open %s", path));
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;
}
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];
/* 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"));
}
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
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;
}
* @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),
* 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) {
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file lib/trackdb.h
* @brief Track database public interface */
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 */
/*
#
# 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 <http://www.gnu.org/licenses/>.
#
"""Python support for DisOrder
self.cmd_ = cmd
self.details_ = details
def __str__(self):
- """Return the complete response string from the server, with the command
- if available.
+ """Return the complete response string from the server, with the
+ command if available.
Excludes the final newline.
"""
Returns the ID of the new queue entry.
- Note that queue IDs are unicode strings (because all track information
- values are unicode strings).
+ Note that queue IDs are unicode strings (because all track
+ information values are unicode strings).
"""
res, details = self._simple("play", track)
return unicode(details) # because it's unicode in queue() output
The return value is a list of dictionaries corresponding to
recently played tracks. The next track to be played comes first.
- See disorder_protocol(5) for the meanings of the keys. All keys are
- plain strings but the values will be unicode strings."""
+ See disorder_protocol(5) for the meanings of the keys.
+ All keys are plain strings but the values will be unicode strings."""
return self._somequeue("queue")
def _somedir(self, command, dir, re):
The callback should return True to continue or False to stop (don't
forget this, or your program will mysteriously misbehave). Once you
- stop reading the log the connection is useless and should be deleted.
+ stop reading the log the connection is useless and should be
+ deleted.
It is suggested that you use the disorder.monitor class instead of
calling this method directly, but this is not mandatory.
self._simple("schedule-del", event)
def schedule_get(self, event):
- """Get the details for an event as a dict (returns None if event not found)"""
+ """Get the details for an event as a dict (returns None if
+ event not found)"""
res, details = self._simple("schedule-get", event)
if res == 555:
return None
"""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
else:
raise protocolError(self.who, "invalid response %s")
- def _send(self, *command):
- # Quote and send a command
+ def _send(self, body, *command):
+ # Quote and send a command and optional body
#
# Returns the encoded command.
quoted = _quote(command)
try:
self.w.write(encoded)
self.w.write("\n")
+ if body != None:
+ for l in body:
+ if l[0] == ".":
+ self.w.write(".")
+ self.w.write(l)
+ self.w.write("\n")
+ self.w.write(".\n")
self.w.flush()
return encoded
except IOError, e:
self._disconnect()
raise
- def _simple(self, *command):
+ def _simple(self, *command):
# Issue a simple command, throw an exception on error
#
# If an I/O error occurs, disconnect from the server.
# On success or 'normal' errors returns response as a (code, details) tuple
#
# On error raise operationError
+ return self._simple_body(None, *command)
+
+ def _simple_body(self, body, *command):
+ # Issue a simple command with optional body, throw an exception on error
+ #
+ # If an I/O error occurs, disconnect from the server.
+ #
+ # On success or 'normal' errors returns response as a (code, details) tuple
+ #
+ # On error raise operationError
if self.state == 'disconnected':
self.connect()
if command:
- cmd = self._send(*command)
+ cmd = self._send(body, *command)
else:
cmd = None
res, details = self._response()
class monitor:
"""DisOrder event log monitor class
- Intended to be subclassed with methods corresponding to event log messages
- the implementor cares about over-ridden."""
+ Intended to be subclassed with methods corresponding to event log
+ messages the implementor cares about over-ridden."""
def __init__(self, c=None):
"""Constructor for the monitor class
def run(self):
"""Start monitoring logs. Continues monitoring until one of the
- message-specific methods returns False. Can be called more than once
- (but not recursively!)"""
+ message-specific methods returns False. Can be called more than
+ once (but not recursively!)"""
self.c.log(self._callback)
def when(self):
# 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 <http://www.gnu.org/licenses/>.
#
complete -r disorder 2>/dev/null || true
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
* 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 <http://www.gnu.org/licenses/>.
+ */
+/** @file server/dump.c
+ * @brief Dump and restore database contents
*/
-
#include "disorder-server.h"
static const struct option options[] = {
{ "debug", no_argument, 0, 'D' },
{ "recover", no_argument, 0, 'r' },
{ "recover-fatal", no_argument, 0, 'R' },
- { "trackdb", no_argument, 0, 't' },
- { "searchdb", no_argument, 0, 's' },
{ "recompute-aliases", no_argument, 0, 'a' },
{ "remove-pathless", no_argument, 0, 'P' },
{ 0, 0, 0, 0 }
exit(0);
}
+ /** @brief Dump one record
+ * @param s Output stream
+ * @param tag Tag for error messages
+ * @param letter Prefix leter for dumped record
+ * @param dbname Database name
+ * @param db Database handle
+ * @param tid Transaction handle
+ * @return 0 or @c DB_LOCK_DEADLOCK
+ */
+ static int dump_one(struct sink *s,
+ const char *tag,
+ int letter,
+ const char *dbname,
+ DB *db,
+ DB_TXN *tid) {
+ int err;
+ DBC *cursor;
+ DBT k, d;
+
+ /* dump the preferences */
+ cursor = trackdb_opencursor(db, tid);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST);
+ while(err == 0) {
+ if(sink_writec(s, letter) < 0
+ || urlencode(s, k.data, k.size)
+ || sink_writec(s, '\n') < 0
+ || urlencode(s, d.data, d.size)
+ || sink_writec(s, '\n') < 0)
+ fatal(errno, "error writing to %s", tag);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ switch(err) {
+ case DB_LOCK_DEADLOCK:
+ trackdb_closecursor(cursor);
+ return err;
+ case DB_NOTFOUND:
+ return trackdb_closecursor(cursor);
+ case 0:
+ assert(!"cannot happen");
+ default:
+ fatal(0, "error reading %s: %s", dbname, db_strerror(err));
+ }
+ }
+
+ static struct {
+ int letter;
+ const char *dbname;
+ DB **db;
+ } dbtable[] = {
+ { 'P', "prefs.db", &trackdb_prefsdb },
+ { 'G', "global.db", &trackdb_globaldb },
+ { 'U', "users.db", &trackdb_usersdb },
+ { 'W', "schedule.db", &trackdb_scheduledb },
+ { 'L', "playlists.db", &trackdb_playlistsdb },
+ /* avoid 'T' and 'S' for now */
+ };
+ #define NDBTABLE (sizeof dbtable / sizeof *dbtable)
+
/* dump prefs to FP, return nonzero on error */
- static void do_dump(FILE *fp, const char *tag,
- int tracksdb, int searchdb) {
- DBC *cursor = 0;
+ static void do_dump(FILE *fp, const char *tag) {
DB_TXN *tid;
struct sink *s = sink_stdio(tag, fp);
- int err;
- DBT k, d;
for(;;) {
tid = trackdb_begin_transaction();
fatal(errno, "error calling fflush");
if(ftruncate(fileno(fp), 0) < 0)
fatal(errno, "error calling ftruncate");
- if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
+ if(fprintf(fp, "V0") < 0)
fatal(errno, "error writing to %s", tag);
- /* dump the preferences */
- cursor = trackdb_opencursor(trackdb_prefsdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('P', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
- /* dump the global preferences */
- cursor = trackdb_opencursor(trackdb_globaldb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('G', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
+ for(size_t n = 0; n < NDBTABLE; ++n)
+ if(dump_one(s, tag,
+ dbtable[n].letter, dbtable[n].dbname, *dbtable[n].db,
+ tid))
+ goto fail;
- /* dump the users */
- cursor = trackdb_opencursor(trackdb_usersdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('U', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
- /* dump the schedule */
- cursor = trackdb_opencursor(trackdb_scheduledb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('W', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
-
- if(tracksdb) {
- cursor = trackdb_opencursor(trackdb_tracksdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('T', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
- }
-
- if(searchdb) {
- cursor = trackdb_opencursor(trackdb_searchdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('S', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } cursor = 0;
- }
-
- if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag);
- if(err == DB_LOCK_DEADLOCK) {
- error(0, "c->c_get: %s", db_strerror(err));
- goto fail;
- }
- if(err && err != DB_NOTFOUND)
- fatal(0, "cursor->c_get: %s", db_strerror(err));
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+ if(fputs("E\n", fp) < 0)
+ fatal(errno, "error writing to %s", tag);
break;
fail:
- trackdb_closecursor(cursor);
- cursor = 0;
info("aborting transaction and retrying dump");
trackdb_abort_transaction(tid);
}
/* undump from FP, return 0 or DB_LOCK_DEADLOCK */
static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
int err, c;
- DBT k, d;
- const char *which_name;
- DB *which_db;
info("undumping");
if(fseek(fp, 0, SEEK_SET) < 0)
if((err = truncdb(tid, trackdb_scheduledb))) return err;
c = getc(fp);
while(!ferror(fp) && !feof(fp)) {
+ for(size_t n = 0; n < NDBTABLE; ++n) {
+ if(dbtable[n].letter == c) {
+ DB *db = *dbtable[n].db;
+ const char *dbname = dbtable[n].dbname;
+ DBT k, d;
+
+ if(undump_dbt(fp, tag, prepare_data(&k))
+ || undump_dbt(fp, tag, prepare_data(&d)))
+ break;
+ switch(err = db->put(db, tid, &k, &d, 0)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating %s: %s", dbname, db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating %s: %s", dbname, db_strerror(err));
+ }
+ goto next;
+ }
+ }
+
switch(c) {
case 'V':
c = getc(fp);
break;
case 'E':
return 0;
- case 'P':
- case 'G':
- case 'U':
- case 'W':
- switch(c) {
- case 'P':
- which_db = trackdb_prefsdb;
- which_name = "prefs.db";
- break;
- case 'G':
- which_db = trackdb_globaldb;
- which_name = "global.db";
- break;
- case 'U':
- which_db = trackdb_usersdb;
- which_name = "users.db";
- break;
- case 'W': /* for 'when' */
- which_db = trackdb_scheduledb;
- which_name = "scheduledb.db";
- break;
- default:
- abort();
- }
- if(undump_dbt(fp, tag, prepare_data(&k))
- || undump_dbt(fp, tag, prepare_data(&d)))
- break;
- switch(err = which_db->put(which_db, tid, &k, &d, 0)) {
- case 0:
- break;
- case DB_LOCK_DEADLOCK:
- error(0, "error updating %s: %s", which_name, db_strerror(err));
- return err;
- default:
- fatal(0, "error updating %s: %s", which_name, db_strerror(err));
- }
- break;
- case 'T':
- case 'S':
- if(undump_dbt(fp, tag, prepare_data(&k))
- || undump_dbt(fp, tag, prepare_data(&d)))
- break;
- /* We don't restore the tracks.db or search.db entries, instead
- * we recompute them */
- break;
case '\n':
break;
+ default:
+ if(c >= 32 && c <= 126)
+ fatal(0, "unexpected character '%c'", c);
+ else
+ fatal(0, "unexpected character 0x%02X", c);
}
+ next:
c = getc(fp);
}
if(ferror(fp))
int main(int argc, char **argv) {
int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
- int tracksdb = 0, searchdb = 0, remove_pathless = 0, fd;
+ int remove_pathless = 0, fd;
const char *path;
char *tmp;
FILE *fp;
mem_init();
- while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
+ while((n = getopt_long(argc, argv, "hVc:dDurRaP", options, 0)) >= 0) {
switch(n) {
case 'h': help();
case 'V': version("disorder-dump");
case 'd': dump = 1; break;
case 'u': undump = 1; break;
case 'D': debugging = 1; break;
- case 't': tracksdb = 1; break;
- case 's': searchdb = 1; break;
case 'r': recover = TRACKDB_NORMAL_RECOVER;
case 'R': recover = TRACKDB_FATAL_RECOVER;
case 'a': recompute = 1; break;
}
if(dump + undump + recompute != 1)
fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
- if((undump || recompute) && (tracksdb || searchdb))
- fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases");
if(recompute) {
if(optind != argc)
fatal(0, "--recompute-aliases does not take a filename");
fatal(errno, "error opening %s", tmp);
if(!(fp = fdopen(fd, "w")))
fatal(errno, "fdopen on %s", tmp);
- do_dump(fp, tmp, tracksdb, searchdb);
+ do_dump(fp, tmp);
if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
if(rename(tmp, path) < 0)
fatal(errno, "error renaming %s to %s", tmp, path);
* 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 <http://www.gnu.org/licenses/>.
*/
#include "disorder-server.h"
int pf;
};
+ struct conn;
+
+ /** @brief Signature for line reader callback
+ * @param c Connection
+ * @param line Line
+ * @return 0 if incomplete, 1 if complete
+ *
+ * @p line is 0-terminated and excludes the newline. It points into the
+ * input buffer so will become invalid shortly.
+ */
+ typedef int line_reader_type(struct conn *c,
+ char *line);
+
+ /** @brief Signature for with-body command callbacks
+ * @param c Connection
+ * @param body List of body lines
+ * @param nbody Number of body lines
+ * @param u As passed to fetch_body()
+ * @return 0 to suspend input, 1 if complete
+ *
+ * The body strings are allocated (so survive indefinitely) and don't include
+ * newlines.
+ */
+ typedef int body_callback_type(struct conn *c,
+ char **body,
+ int nbody,
+ void *u);
+
/** @brief One client connection */
struct conn {
/** @brief Read commands from here */
struct conn *next;
/** @brief True if pending rescan had 'wait' set */
int rescan_wait;
+ /** @brief Playlist that this connection locks */
+ const char *locked_playlist;
+ /** @brief When that playlist was locked */
+ time_t locked_when;
+ /** @brief Line reader function */
+ line_reader_type *line_reader;
+ /** @brief Called when command body has been read */
+ body_callback_type *body_callback;
+ /** @brief Passed to @c body_callback */
+ void *body_u;
+ /** @brief Accumulating body */
+ struct vector body[1];
};
/** @brief Linked list of connections */
size_t bytes,
int eof,
void *u);
+ static int c_playlist_set_body(struct conn *c,
+ char **body,
+ int nbody,
+ void *u);
+ static int fetch_body(struct conn *c,
+ body_callback_type body_callback,
+ void *u);
+ static int body_line(struct conn *c, char *line);
+ static int command(struct conn *c, char *line);
static const char *noyes[] = { "no", "yes" };
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
return 1;
}
- static int c_tags(struct conn *c,
- char attribute((unused)) **vec,
- int attribute((unused)) nvec) {
- char **tags = trackdb_alltags();
-
- sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n");
- while(*tags) {
+ static int list_response(struct conn *c,
+ const char *reply,
+ char **list) {
+ sink_printf(ev_writer_sink(c->w), "253 %s\n", reply);
+ while(*list) {
sink_printf(ev_writer_sink(c->w), "%s%s\n",
- **tags == '.' ? "." : "", *tags);
- ++tags;
+ **list == '.' ? "." : "", *list);
+ ++list;
}
sink_writes(ev_writer_sink(c->w), ".\n");
return 1; /* completed */
}
+ static int c_tags(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ return list_response(c, "Tag list follows", trackdb_alltags());
+ }
+
static int c_set_global(struct conn *c,
char **vec,
int attribute((unused)) nvec) {
static int c_users(struct conn *c,
char attribute((unused)) **vec,
int attribute((unused)) nvec) {
- /* TODO de-dupe with c_tags */
- char **users = trackdb_listusers();
-
- sink_writes(ev_writer_sink(c->w), "253 User list follows\n");
- while(*users) {
- sink_printf(ev_writer_sink(c->w), "%s%s\n",
- **users == '.' ? "." : "", *users);
- ++users;
- }
- sink_writes(ev_writer_sink(c->w), ".\n");
- return 1; /* completed */
+ return list_response(c, "User list follows", trackdb_listusers());
}
/** @brief Base64 mapping table for confirmation strings
return 1;
}
+static int 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;
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 },
{ "pause", 0, 0, c_pause, RIGHT_PAUSE },
{ "play", 1, 1, c_play, RIGHT_PLAY },
{ "playing", 0, 0, c_playing, RIGHT_READ },
+ { "playlist-delete", 1, 1, c_playlist_delete, RIGHT_PLAY },
+ { "playlist-get", 1, 1, c_playlist_get, RIGHT_READ },
+ { "playlist-get-share", 1, 1, c_playlist_get_share, RIGHT_READ },
+ { "playlist-lock", 1, 1, c_playlist_lock, RIGHT_PLAY },
+ { "playlist-set", 1, 1, c_playlist_set, RIGHT_PLAY },
+ { "playlist-set-share", 2, 2, c_playlist_set_share, RIGHT_PLAY },
+ { "playlist-unlock", 0, 0, c_playlist_unlock, RIGHT_PLAY },
+ { "playlists", 0, 0, c_playlists, RIGHT_READ },
{ "prefs", 1, 1, c_prefs, RIGHT_READ },
{ "queue", 0, 0, c_queue, RIGHT_READ },
{ "random-disable", 0, 0, c_random_disable, RIGHT_GLOBAL_PREFS },
{ "volume", 0, 2, c_volume, RIGHT_READ|RIGHT_VOLUME }
};
+ /** @brief Fetch a command body
+ * @param c Connection
+ * @param body_callback Called with body
+ * @param u Passed to body_callback
+ * @return 1
+ */
+ static int fetch_body(struct conn *c,
+ body_callback_type body_callback,
+ void *u) {
+ assert(c->line_reader == command);
+ c->line_reader = body_line;
+ c->body_callback = body_callback;
+ c->body_u = u;
+ vector_init(c->body);
+ return 1;
+ }
+
+ /** @brief @ref line_reader_type callback for command body lines
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
+ static int body_line(struct conn *c,
+ char *line) {
+ if(*line == '.') {
+ ++line;
+ if(!*line) {
+ /* That's the lot */
+ c->line_reader = command;
+ vector_terminate(c->body);
+ return c->body_callback(c, c->body->vec, c->body->nvec, c->body_u);
+ }
+ }
+ vector_append(c->body, xstrdup(line));
+ return 1; /* completed */
+ }
+
static void command_error(const char *msg, void *u) {
struct conn *c = u;
sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg);
}
- /* process a command. Return 1 if complete, 0 if incomplete. */
+ /** @brief @ref line_reader_type callback for commands
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
static int command(struct conn *c, char *line) {
char **vec;
int nvec, n;
while((eol = memchr(ptr, '\n', bytes))) {
*eol++ = 0;
ev_reader_consume(reader, eol - (char *)ptr);
- complete = command(c, ptr);
+ complete = c->line_reader(c, ptr); /* usually command() */
bytes -= (eol - (char *)ptr);
ptr = eol;
if(!complete) {
c->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",
# 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 <http://www.gnu.org/licenses/>.
#
noinst_PROGRAMS=disorder-udplog
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
rm -rf testroot *.log *.pyc
EXTRA_DIST=dtest.py ${TESTS}
+CLEANFILES=*.gcda *.gcov *.gcno *.c.html index.html