#include <unistd.h>
#include <pcre.h>
#include <ctype.h>
+#include <gcrypt.h>
+ #include <langinfo.h>
#include "configuration.h"
#include "syscalls.h"
#include "vector.h"
#include "version.h"
#include "dateparse.h"
+#include "trackdb.h"
+ #include "inputline.h"
static disorder_client *client;
{ "help-commands", no_argument, 0, 'H' },
{ "user", required_argument, 0, 'u' },
{ "password", required_argument, 0, 'p' },
+ { "wait-for-root", no_argument, 0, 'W' },
{ 0, 0, 0, 0 }
};
static void cf_reconfigure(char attribute((unused)) **argv) {
/* Re-check configuration for server */
- if(config_read(1)) fatal(0, "cannot read configuration");
+ if(config_read(1, NULL)) fatal(0, "cannot read configuration");
if(disorder_reconfigure(getclient())) exit(EXIT_FAILURE);
}
}
static void cf_quack(char attribute((unused)) **argv) {
- xprintf("\n"
- " .------------------.\n"
- " | Naath is a babe! |\n"
- " `---------+--------'\n"
- " \\\n"
- " >0\n"
- " (<)'\n"
- "~~~~~~~~~~~~~~~~~~~~~~\n"
- "\n");
+ if(!strcasecmp(nl_langinfo(CODESET), "utf-8")) {
+ #define TL "\xE2\x95\xAD"
+ #define TR "\xE2\x95\xAE"
+ #define BR "\xE2\x95\xAF"
+ #define BL "\xE2\x95\xB0"
+ #define H "\xE2\x94\x80"
+ #define V "\xE2\x94\x82"
+ #define T "\xE2\x94\xAC"
+ xprintf("\n"
+ " "TL H H H H H H H H H H H H H H H H H H TR"\n"
+ " "V" Naath is a babe! "V"\n"
+ " "BL H H H H H H H H H T H H H H H H H H BR"\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ } else {
+ xprintf("\n"
+ " .------------------.\n"
+ " | Naath is a babe! |\n"
+ " `---------+--------'\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ }
}
static void cf_somelist(char **argv,
exit(EXIT_FAILURE);
}
+ static void cf_playlists(char attribute((unused)) **argv) {
+ char **vec;
+
+ if(disorder_playlists(getclient(), &vec, 0))
+ exit(EXIT_FAILURE);
+ while(*vec)
+ xprintf("%s\n", nullcheck(utf82mb(*vec++)));
+ }
+
+ static void cf_playlist_del(char **argv) {
+ if(disorder_playlist_delete(getclient(), argv[0]))
+ exit(EXIT_FAILURE);
+ }
+
+ static void cf_playlist_get(char **argv) {
+ char **vec;
+
+ if(disorder_playlist_get(getclient(), argv[0], &vec, 0))
+ exit(EXIT_FAILURE);
+ while(*vec)
+ xprintf("%s\n", nullcheck(utf82mb(*vec++)));
+ }
+
+ static void cf_playlist_set(char **argv) {
+ struct vector v[1];
+ FILE *input;
+ const char *tag;
+ char *l;
+
+ if(argv[1]) {
+ // Read track list from file
+ if(!(input = fopen(argv[1], "r")))
+ fatal(errno, "opening %s", argv[1]);
+ tag = argv[1];
+ } else {
+ // Read track list from standard input
+ input = stdin;
+ tag = "stdin";
+ }
+ vector_init(v);
+ while(!inputline(tag, input, &l, '\n')) {
+ if(!strcmp(l, "."))
+ break;
+ vector_append(v, l);
+ }
+ if(ferror(input))
+ fatal(errno, "reading %s", tag);
+ if(input != stdin)
+ fclose(input);
+ if(disorder_playlist_lock(getclient(), argv[0])
+ || disorder_playlist_set(getclient(), argv[0], v->vec, v->nvec)
+ || disorder_playlist_unlock(getclient()))
+ exit(EXIT_FAILURE);
+ }
+
static const struct command {
const char *name;
int min, max;
"Add TRACKS to the end of the queue" },
{ "playing", 0, 0, cf_playing, 0, "",
"Report the playing track" },
+ { "playlist-del", 1, 1, cf_playlist_del, 0, "PLAYLIST",
+ "Delete a playlist" },
+ { "playlist-get", 1, 1, cf_playlist_get, 0, "PLAYLIST",
+ "Get the contents of a playlist" },
+ { "playlist-set", 1, 2, cf_playlist_set, isarg_filename, "PLAYLIST [PATH]",
+ "Set the contents of a playlist" },
+ { "playlists", 0, 0, cf_playlists, 0, "",
+ "List playlists" },
{ "prefs", 1, 1, cf_prefs, 0, "TRACK",
"Display all the preferences for TRACK" },
{ "quack", 0, 0, cf_quack, 0, 0, 0 },
exit(0);
}
+static void wait_for_root(void) {
+ const char *password;
+
+ while(!trackdb_readable()) {
+ info("waiting for trackdb...");
+ sleep(1);
+ }
+ trackdb_init(TRACKDB_NO_RECOVER|TRACKDB_NO_UPGRADE);
+ for(;;) {
+ trackdb_open(TRACKDB_READ_ONLY);
+ password = trackdb_get_password("root");
+ trackdb_close();
+ if(password)
+ break;
+ info("waiting for root user to be created...");
+ sleep(1);
+ }
+ trackdb_deinit();
+}
+
int main(int argc, char **argv) {
- int n, i, j, local = 0;
+ int n, i, j, local = 0, wfr = 0;
int status = 0;
struct vector args;
const char *user = 0, *password = 0;
pcre_free = xfree;
if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
if(!setlocale(LC_TIME, "")) fatal(errno, "error calling setlocale");
- while((n = getopt_long(argc, argv, "+hVc:dHlNu:p:", options, 0)) >= 0) {
+ while((n = getopt_long(argc, argv, "+hVc:dHlNu:p:W", options, 0)) >= 0) {
switch(n) {
case 'h': help();
case 'H': help_commands();
case 'N': config_per_user = 0; break;
case 'u': user = optarg; break;
case 'p': password = optarg; break;
+ case 'W': wfr = 1; break;
default: fatal(0, "invalid option");
}
}
- if(config_read(0)) fatal(0, "cannot read configuration");
+ if(config_read(0, NULL)) fatal(0, "cannot read configuration");
if(user) {
config->username = user;
config->password = 0;
if(password)
config->password = password;
if(local)
- config->connect.n = 0;
+ config->connect.af = -1;
+ if(wfr)
+ wait_for_root();
n = optind;
optind = 1; /* for subsequent getopt calls */
+ /* gcrypt initialization */
+ if(!gcry_check_version(NULL))
+ disorder_fatal(0, "gcry_check_version failed");
+ gcry_control(GCRYCTL_INIT_SECMEM, 0);
+ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
/* accumulate command args */
while(n < argc) {
if((i = TABLE_FIND(commands, name, argv[n])) < 0)
#
# This file is part of DisOrder.
-# Copyright (C) 2006-2008 Richard Kettlewell
+# Copyright (C) 2006-2009 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
choose.c choose-menu.c choose-search.c popup.c misc.c \
control.c properties.c menu.c log.c progress.c login.c rtp.c \
help.c ../lib/memgc.c settings.c users.c lookup.c choose.h \
- popup.h
+ popup.h playlists.c
disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
- $(LIBASOUND) $(COREAUDIO) $(LIBDB)
+ $(LIBASOUND) $(COREAUDIO) $(LIBDB) $(LIBICONV)
disobedience_LDFLAGS=$(GTK_LIBS)
install-exec-hook:
/*
* This file is part of DisOrder.
- * Copyright (C) 2006, 2007, 2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
*/
#include "disobedience.h"
-#include "mixer.h"
#include "version.h"
#include <getopt.h>
#include <locale.h>
#include <pcre.h>
+#include <gcrypt.h>
/* Apologies for the numerous de-consting casts, but GLib et al do not seem to
* have heard of const. */
/** @brief Right channel volume */
int volume_r;
+/** @brief Audio backend */
+const struct uaudio *backend;
+
double goesupto = 10; /* volume upper bound */
/** @brief True if a NOP is in flight */
/* 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 */
}
last = now;
#endif
- if(rtp_supported && mixer_supported(DEFAULT_BACKEND)) {
+ if(rtp_supported && backend && backend->get_volume) {
int nl, nr;
- if(!mixer_control(DEFAULT_BACKEND, &nl, &nr, 0)
- && (nl != volume_l || nr != volume_r)) {
+ backend->get_volume(&nl, &nr);
+ if(nl != volume_l || nr != volume_r) {
volume_l = nl;
volume_r = nr;
event_raise("volume-changed", 0);
recheck_rights = 0;
if(recheck_rights)
check_rights();
+ event_raise("periodic-fast", 0);
return TRUE;
}
}
if(!gtkok)
fatal(0, "failed to initialize GTK+");
+ /* gcrypt initialization */
+ if(!gcry_check_version(NULL))
+ disorder_fatal(0, "gcry_check_version failed");
+ gcry_control(GCRYCTL_INIT_SECMEM, 0);
+ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
signal(SIGPIPE, SIG_IGN);
init_styles();
load_settings();
/* create the event loop */
D(("create main loop"));
mainloop = g_main_loop_new(0, 0);
- if(config_read(0)) fatal(0, "cannot read configuration");
+ if(config_read(0, NULL)) fatal(0, "cannot read configuration");
+ /* we'll need mixer support */
+ backend = uaudio_apis[0];
+ if(backend->configure)
+ backend->configure();
+ if(backend->open_mixer)
+ backend->open_mixer();
/* create the clients */
if(!(client = gtkclient())
|| !(logclient = gtkclient()))
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();
#include "eventdist.h"
#include "split.h"
#include "timeval.h"
+#include "uaudio.h"
#include <glib.h>
#include <gtk/gtk.h>
extern int rtp_supported;
extern int rtp_is_running;
extern GtkItemFactory *mainmenufactory;
+extern const struct uaudio *backend;
extern const disorder_eclient_log_callbacks log_callbacks;
void set_tool_colors(GtkWidget *w);
void popup_settings(void);
+ /* Playlists */
+
+ void playlists_init(void);
+ void edit_playlists(gpointer callback_data,
+ guint callback_action,
+ GtkWidget *menu_item);
+ extern char **playlists;
+ extern int nplaylists;
+ extern GtkWidget *playlists_widget;
+ extern GtkWidget *playlists_menu;
+ extern GtkWidget *editplaylists_widget;
+
#endif /* DISOBEDIENCE_H */
/*
static 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,
FALSE/*fill*/,
1/*padding*/);
gtk_box_pack_start(GTK_BOX(vbox),
- gtk_label_new("\xC2\xA9 2004-2008 Richard Kettlewell"),
+ gtk_label_new("\xC2\xA9 2004-2009 Richard Kettlewell"),
FALSE/*expand*/,
FALSE/*fill*/,
1/*padding*/);
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,
.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).
.TP
.B reconfigure
Make the daemon reload its configuration file.
+.IP
+Not all configuration options can be modified during the lifetime of the
+server; of those that can't, some will just be ignored if they change while
+others will cause the new configuration to be rejected.
+See \fBdisorder_config\fR(5) for details.
.TP
.B remove \fITRACK\fR
Remove a track from the queue.
Bodies borrow their syntax from RFC821; they consist of zero or more ordinary
lines, with any initial full stop doubled up, and are terminated by a line
consisting of a full stop and a line feed.
+ .PP
+ Commands only have a body if explicitly stated below.
+ If they do have a body then the body should always be sent immediately;
+ unlike (for instance) the SMTP "DATA" command there is no intermediate step
+ where the server asks for the body to be sent.
+ .PP
+ Replies also only have a body if stated below.
+ The presence of a reply body can always be inferred from the response code;
+ if the last digit is a 3 then a body is present, otherwise it is not.
.SH COMMANDS
Commands always have a command name as the first field of the line; responses
always have a 3-digit response code as the first field.
stated otherwise.
If not stated otherwise, the \fBread\fR right is sufficient to execute
the command.
- .PP
- Neither commands nor responses have a body unless stated otherwise.
.TP
.B adduser \fIUSERNAME PASSWORD \fR[\fIRIGHTS\fR]
Create a new user with the given username and password.
.IP
If the response is \fB259\fR then nothing is playing.
.TP
+ .B playlist-delete \fIPLAYLIST\fR
+ Delete a playlist.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ .TP
+ .B playlist-get \fIPLAYLIST\fR
+ Get the contents of a playlist, in a response body.
+ Requires permission to read that playlist and the \fBread\fR right.
+ .TP
+ .B playlist-get-share \fIPLAYLIST\fR
+ Get the sharing status of a playlist.
+ The result will be \fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+ Requires permission to read that playlist and the \fBread\fR right.
+ .TP
+ .B playlist-lock \fIPLAYLIST\fR
+ Lock a playlist.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ Only one playlist may be locked at a time on a given connection and the lock
+ automatically expires when the connection is closed.
+ .TP
+ .B playlist-set \fIPLAYLIST\fR
+ Set the contents of a playlist.
+ The new contents should be supplied in a command body.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ The playlist must be locked.
+ .TP
+ .B playlist-set-share \fIPLAYLIST\fR \fISHARE\fR
+ Set the sharing status of a playlist to
+ \fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+ Requires permission to modify that playlist and the \fBplay\fR right.
+ .TP
+ .B playlist-unlock\fR
+ Unlock the locked playlist.
+ .TP
+ .B playlists
+ List all playlists that this connection has permission to read.
+ Requires the \fBread\fR right.
+ .TP
.B prefs \fBTRACK\fR
Send back the preferences for \fITRACK\fR in a response body.
Each line of the response has the usual line syntax, the first field being the
.B reconfigure
Request that DisOrder reconfigure itself.
Requires the \fBadmin\fR right.
+.IP
+Not all configuration options can be modified during the lifetime of the
+server; of those that can't, some will just be ignored if they change while
+others will cause the new configuration to be rejected.
+See \fBdisorder_config\fR(5) for details.
.TP
.B register \fIUSERNAME PASSWORD EMAIL
Register a new user.
.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
+# Copyright (C) 2004-2009 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
libdisorder_a_SOURCES=charset.c charset.h \
addr.c addr.h \
- alsabg.c alsabg.h \
arcfour.c arcfour.h \
authhash.c authhash.h \
basen.c basen.h \
client-common.c client-common.h \
configuration.c configuration.h \
cookies.c cookies.h \
+ coreaudio.c coreaudio.h \
dateparse.c dateparse.h xgetdate.c \
defs.c defs.h \
eclient.c eclient.h \
ifreq.c ifreq.h \
inputline.c inputline.h \
kvp.c kvp.h \
- log.c log.h log-impl.h \
+ log.c log.h \
logfd.c logfd.h \
macros.c macros-builtin.c macros.h \
- mem.c mem.h mem-impl.h \
+ mem.c mem.h \
mime.h mime.c \
- mixer.c mixer.h mixer-oss.c mixer-alsa.c \
printf.c printf.h \
asprintf.c fprintf.c snprintf.c \
queue.c queue.h \
table.c table.h \
timeval.h \
$(TRACKDB) trackdb.h trackdb-int.h \
+ trackdb-playlists.c \
trackname.c trackorder.c trackname.h \
tracksort.c \
+ uaudio.c uaudio-thread.c uaudio.h uaudio-apis.c \
+ uaudio-oss.c uaudio-alsa.c \
+ uaudio-coreaudio.c \
+ uaudio-rtp.c uaudio-command.c uaudio-schedule.c \
url.h url.c \
user.h user.c \
unicode.h unicode.c \
* @param c Client
* @param rp Where to store result, or NULL
* @param cmd Command
+ * @param body Body or NULL
+ * @param nbody Length of body or -1
* @param ap Arguments (UTF-8), terminated by (char *)0
* @return 0 on success, non-0 on error
*
*
* NB that the response will NOT be converted to the local encoding
* nor will quotes be stripped. See dequote().
+ *
+ * If @p body is not NULL then the body is sent immediately after the
+ * command. @p nbody should be the number of lines or @c -1 to count
+ * them if @p body is NULL-terminated.
+ *
+ * Usually you would call this via one of the following interfaces:
+ * - disorder_simple()
+ * - disorder_simple_body()
+ * - disorder_simple_list()
*/
static int disorder_simple_v(disorder_client *c,
char **rp,
- const char *cmd, va_list ap) {
+ const char *cmd,
+ char **body, int nbody,
+ va_list ap) {
const char *arg;
struct dynstr d;
dynstr_append(&d, '\n');
dynstr_terminate(&d);
D(("command: %s", d.vec));
- if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) {
- byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
- error(errno, "error writing to %s", c->ident);
- return -1;
+ if(fputs(d.vec, c->fpout) < 0)
+ goto write_error;
+ if(body) {
+ if(nbody < 0)
+ for(nbody = 0; body[nbody]; ++nbody)
+ ;
+ for(int n = 0; n < nbody; ++n) {
+ if(body[n][0] == '.')
+ if(fputc('.', c->fpout) < 0)
+ goto write_error;
+ if(fputs(body[n], c->fpout) < 0)
+ goto write_error;
+ if(fputc('\n', c->fpout) < 0)
+ goto write_error;
+ }
+ if(fputs(".\n", c->fpout) < 0)
+ goto write_error;
}
+ if(fflush(c->fpout))
+ goto write_error;
}
return check_response(c, rp);
+ write_error:
+ byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
+ error(errno, "error writing to %s", c->ident);
+ return -1;
}
/** @brief Issue a command and parse a simple response
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, rp, cmd, ap);
+ ret = disorder_simple_v(c, rp, cmd, 0, 0, ap);
+ va_end(ap);
+ return ret;
+ }
+
+ /** @brief Issue a command with a body and parse a simple response
+ * @param c Client
+ * @param rp Where to store result, or NULL (UTF-8)
+ * @param body Pointer to body
+ * @param nbody Size of body
+ * @param cmd Command
+ * @return 0 on success, non-0 on error
+ *
+ * See disorder_simple().
+ */
+ static int disorder_simple_body(disorder_client *c,
+ char **rp,
+ char **body, int nbody,
+ const char *cmd, ...) {
+ va_list ap;
+ int ret;
+
+ va_start(ap, cmd);
+ ret = disorder_simple_v(c, rp, cmd, body, nbody, ap);
va_end(ap);
return ret;
}
if(!password) {
/* Oh well */
c->last = "no password";
- error(0, "no password configured");
+ error(0, "no password configured for user '%s'", username);
return -1;
}
return disorder_connect_generic(config,
* *)0. They should be in UTF-8.
*
* 5xx responses count as errors.
+ *
+ * See disorder_simple().
*/
static int disorder_simple_list(disorder_client *c,
char ***vecp, int *nvecp,
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, 0, cmd, ap);
+ ret = disorder_simple_v(c, 0, cmd, 0, 0, ap);
va_end(ap);
if(ret) return ret;
return readlist(c, vecp, nvecp);
return disorder_simple(c, 0, "adopt", id, (char *)0);
}
+ /** @brief Delete a playlist
+ * @param c Client
+ * @param playlist Playlist to delete
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlist_delete(disorder_client *c,
+ const char *playlist) {
+ return disorder_simple(c, 0, "playlist-delete", playlist, (char *)0);
+ }
+
+ /** @brief Get the contents of a playlist
+ * @param c Client
+ * @param playlist Playlist to get
+ * @param tracksp Where to put list of tracks
+ * @param ntracksp Where to put count of tracks
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlist_get(disorder_client *c, const char *playlist,
+ char ***tracksp, int *ntracksp) {
+ return disorder_simple_list(c, tracksp, ntracksp,
+ "playlist-get", playlist, (char *)0);
+ }
+
+ /** @brief List all readable playlists
+ * @param c Client
+ * @param playlistsp Where to put list of playlists
+ * @param nplaylistsp Where to put count of playlists
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlists(disorder_client *c,
+ char ***playlistsp, int *nplaylistsp) {
+ return disorder_simple_list(c, playlistsp, nplaylistsp,
+ "playlists", (char *)0);
+ }
+
+ /** @brief Get the sharing status of a playlist
+ * @param c Client
+ * @param playlist Playlist to inspect
+ * @param sharep Where to put sharing status
+ * @return 0 on success, non-0 on error
+ *
+ * Possible @p sharep values are @c public, @c private and @c shared.
+ */
+ int disorder_playlist_get_share(disorder_client *c, const char *playlist,
+ char **sharep) {
+ return disorder_simple(c, sharep,
+ "playlist-get-share", playlist, (char *)0);
+ }
+
+ /** @brief Get the sharing status of a playlist
+ * @param c Client
+ * @param playlist Playlist to modify
+ * @param share New sharing status
+ * @return 0 on success, non-0 on error
+ *
+ * Possible @p share values are @c public, @c private and @c shared.
+ */
+ int disorder_playlist_set_share(disorder_client *c, const char *playlist,
+ const char *share) {
+ return disorder_simple(c, 0,
+ "playlist-set-share", playlist, share, (char *)0);
+ }
+
+ /** @brief Lock a playlist for modifications
+ * @param c Client
+ * @param playlist Playlist to lock
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlist_lock(disorder_client *c, const char *playlist) {
+ return disorder_simple(c, 0,
+ "playlist-lock", playlist, (char *)0);
+ }
+
+ /** @brief Unlock the locked playlist
+ * @param c Client
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlist_unlock(disorder_client *c) {
+ return disorder_simple(c, 0,
+ "playlist-unlock", (char *)0);
+ }
+
+ /** @brief Set the contents of a playlst
+ * @param c Client
+ * @param playlist Playlist to modify
+ * @param tracks List of tracks
+ * @param ntracks Length of @p tracks (or -1 to count up to the first NULL)
+ * @return 0 on success, non-0 on error
+ */
+ int disorder_playlist_set(disorder_client *c,
+ const char *playlist,
+ char **tracks,
+ int ntracks) {
+ return disorder_simple_body(c, 0, tracks, ntracks,
+ "playlist-set", playlist, (char *)0);
+ }
+
/*
Local Variables:
c-basic-offset:2
/*
* This file is part of DisOrder.
- * Copyright (C) 2004-2008 Richard Kettlewell
+ * Copyright (C) 2004-2009 Richard Kettlewell
* Portions copyright (C) 2007 Mark Wooding
*
* This program is free software: you can redistribute it and/or modify
#include "inputline.h"
#include "charset.h"
#include "defs.h"
-#include "mixer.h"
#include "printf.h"
#include "regsub.h"
#include "signame.h"
#include "authhash.h"
#include "vector.h"
+#include "uaudio.h"
/** @brief Path to config file
*
*/
int config_per_user = 1;
+/** @brief Table of audio APIs
+ *
+ * Only set in server processes.
+ */
+const struct uaudio *const *config_uaudio_apis;
+
/** @brief Config file parser state */
struct config_state {
/** @brief Filename */
/** @brief Return the value of an item */
#define VALUE(C, TYPE) (*ADDRESS(C, TYPE))
+static int stringlist_compare(const struct stringlist *a,
+ const struct stringlist *b);
+static int namepartlist_compare(const struct namepartlist *a,
+ const struct namepartlist *b);
+
static int set_signal(const struct config_state *cs,
const struct conf *whoami,
int nvec, char **vec) {
npl->s = xrealloc(npl->s, (npl->n + 1) * sizeof (struct namepart));
npl->s[npl->n].part = xstrdup(vec[0]);
npl->s[npl->n].re = re;
+ npl->s[npl->n].res = xstrdup(vec[1]);
npl->s[npl->n].replace = xstrdup(vec[2]);
npl->s[npl->n].context = xstrdup(vec[3]);
npl->s[npl->n].reflags = reflags;
return 0;
}
-static int set_backend(const struct config_state *cs,
- const struct conf *whoami,
- int nvec, char **vec) {
- int *const valuep = ADDRESS(cs->config, int);
-
- if(nvec != 1) {
- error(0, "%s:%d: '%s' requires one argument",
- cs->path, cs->line, whoami->name);
- return -1;
- }
- if(!strcmp(vec[0], "alsa")) {
-#if HAVE_ALSA_ASOUNDLIB_H
- *valuep = BACKEND_ALSA;
-#else
- error(0, "%s:%d: ALSA is not available on this platform",
- cs->path, cs->line);
- return -1;
-#endif
- } else if(!strcmp(vec[0], "command"))
- *valuep = BACKEND_COMMAND;
- else if(!strcmp(vec[0], "network"))
- *valuep = BACKEND_NETWORK;
- else if(!strcmp(vec[0], "coreaudio")) {
-#if HAVE_COREAUDIO_AUDIOHARDWARE_H
- *valuep = BACKEND_COREAUDIO;
-#else
- error(0, "%s:%d: Core Audio is not available on this platform",
- cs->path, cs->line);
- return -1;
-#endif
- } else if(!strcmp(vec[0], "oss")) {
-#if HAVE_SYS_SOUNDCARD_H
- *valuep = BACKEND_OSS;
-#else
- error(0, "%s:%d: OSS is not available on this platform",
- cs->path, cs->line);
- return -1;
-#endif
- } else {
- error(0, "%s:%d: invalid '%s' value '%s'",
- cs->path, cs->line, whoami->name, vec[0]);
- return -1;
- }
- return 0;
-}
-
static int set_rights(const struct config_state *cs,
const struct conf *whoami,
int nvec, char **vec) {
return 0;
}
+static int set_netaddress(const struct config_state *cs,
+ const struct conf *whoami,
+ int nvec, char **vec) {
+ struct netaddress *na = ADDRESS(cs->config, struct netaddress);
+
+ if(netaddress_parse(na, nvec, vec)) {
+ error(0, "%s:%d: invalid network address", cs->path, cs->line);
+ return -1;
+ }
+ return 0;
+}
+
/* free functions */
static void free_none(struct config attribute((unused)) *c,
static void free_string(struct config *c,
const struct conf *whoami) {
xfree(VALUE(c, char *));
+ VALUE(c, char *) = 0;
}
static void free_stringlist(struct config *c,
np = &npl->s[n];
xfree(np->part);
pcre_free(np->re); /* ...whatever pcre_free is set to. */
+ xfree(np->res);
xfree(np->replace);
xfree(np->context);
}
xfree(tl->t);
}
+static void free_netaddress(struct config *c,
+ const struct conf *whoami) {
+ struct netaddress *na = ADDRESS(c, struct netaddress);
+
+ xfree(na->address);
+}
+
/* configuration types */
static const struct conftype
type_restrict = { set_restrict, free_none },
type_namepart = { set_namepart, free_namepartlist },
type_transform = { set_transform, free_transformlist },
- type_rights = { set_rights, free_none },
- type_backend = { set_backend, free_none };
+ type_netaddress = { set_netaddress, free_netaddress },
+ type_rights = { set_rights, free_none };
/* specific validation routine */
return 0;
}
-static int validate_addrport(const struct config_state attribute((unused)) *cs,
- int nvec,
- char attribute((unused)) **vec) {
- switch(nvec) {
- case 0:
- error(0, "%s:%d: missing address",
- cs->path, cs->line);
- return -1;
- case 1:
- error(0, "%s:%d: missing port name/number",
- cs->path, cs->line);
+static int validate_algo(const struct config_state attribute((unused)) *cs,
+ int nvec,
+ char **vec) {
+ if(nvec != 1) {
+ error(0, "%s:%d: invalid algorithm specification", cs->path, cs->line);
return -1;
- case 2:
- return 0;
- default:
- error(0, "%s:%d: expected ADDRESS PORT",
- cs->path, cs->line);
+ }
+ if(!valid_authhash(vec[0])) {
+ error(0, "%s:%d: unsuported algorithm '%s'", cs->path, cs->line, vec[0]);
return -1;
}
+ return 0;
}
-static int validate_port(const struct config_state attribute((unused)) *cs,
- int nvec,
- char attribute((unused)) **vec) {
- switch(nvec) {
- case 0:
- error(0, "%s:%d: missing address",
- cs->path, cs->line);
+static int validate_backend(const struct config_state attribute((unused)) *cs,
+ int nvec,
+ char **vec) {
+ int n;
+ if(nvec != 1) {
+ error(0, "%s:%d: invalid sound API specification", cs->path, cs->line);
return -1;
- case 1:
- case 2:
+ }
+ if(!strcmp(vec[0], "network")) {
+ error(0, "'api network' is deprecated; use 'api rtp'");
return 0;
- default:
- error(0, "%s:%d: expected [ADDRESS] PORT",
- cs->path, cs->line);
+ }
+ if(config_uaudio_apis) {
+ for(n = 0; config_uaudio_apis[n]; ++n)
+ if(!strcmp(vec[0], config_uaudio_apis[n]->name))
+ return 0;
+ error(0, "%s:%d: unrecognized sound API '%s'", cs->path, cs->line, vec[0]);
return -1;
}
+ /* In non-server processes we have no idea what's valid */
+ return 0;
}
-static int validate_algo(const struct config_state attribute((unused)) *cs,
- int nvec,
- char **vec) {
- if(nvec != 1) {
- error(0, "%s:%d: invalid algorithm specification", cs->path, cs->line);
+static int validate_pausemode(const struct config_state attribute((unused)) *cs,
+ int nvec,
+ char **vec) {
+ if(nvec == 1 && (!strcmp(vec[0], "silence") || !strcmp(vec[0], "suspend")))
+ return 0;
+ error(0, "%s:%d: invalid pause mode", cs->path, cs->line);
+ return -1;
+}
+
+static int validate_destaddr(const struct config_state attribute((unused)) *cs,
+ int nvec,
+ char **vec) {
+ struct netaddress na[1];
+
+ if(netaddress_parse(na, nvec, vec)) {
+ error(0, "%s:%d: invalid network address", cs->path, cs->line);
return -1;
}
- if(!valid_authhash(vec[0])) {
- error(0, "%s:%d: unsuported algorithm '%s'", cs->path, cs->line, vec[0]);
+ if(!na->address) {
+ error(0, "%s:%d: destination address required", cs->path, cs->line);
return -1;
}
return 0;
static const struct conf conf[] = {
{ C(alias), &type_string, validate_alias },
{ C(allow), &type_stringlist_accum, validate_allow },
- { C(api), &type_backend, validate_any },
+ { C(api), &type_string, validate_backend },
{ C(authorization_algorithm), &type_string, validate_algo },
- { C(broadcast), &type_stringlist, validate_addrport },
- { C(broadcast_from), &type_stringlist, validate_addrport },
+ { C(broadcast), &type_netaddress, validate_destaddr },
+ { C(broadcast_from), &type_netaddress, validate_any },
{ C(channel), &type_string, validate_any },
{ C(checkpoint_kbyte), &type_integer, validate_non_negative },
{ C(checkpoint_min), &type_integer, validate_non_negative },
{ C(collection), &type_collections, validate_any },
- { C(connect), &type_stringlist, validate_addrport },
+ { C(connect), &type_netaddress, validate_destaddr },
{ C(cookie_login_lifetime), &type_integer, validate_positive },
{ C(cookie_key_lifetime), &type_integer, validate_positive },
{ C(dbversion), &type_integer, validate_positive },
{ C(gap), &type_integer, validate_non_negative },
{ C(history), &type_integer, validate_positive },
{ C(home), &type_string, validate_isabspath },
- { C(listen), &type_stringlist, validate_port },
+ { C(listen), &type_netaddress, validate_any },
{ C(lock), &type_boolean, validate_any },
{ C(mail_sender), &type_string, validate_any },
{ C(mixer), &type_string, validate_any },
{ C(nice_speaker), &type_integer, validate_any },
{ C(noticed_history), &type_integer, validate_positive },
{ C(password), &type_string, validate_any },
+ { C(pause_mode), &type_string, validate_pausemode },
{ C(player), &type_stringlist_accum, validate_player },
+ { C(playlist_lock_timeout), &type_integer, validate_positive },
+ { C(playlist_max) , &type_integer, validate_positive },
{ C(plugins), &type_string_accum, validate_isdir },
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
{ C(reminder_interval), &type_integer, validate_positive },
{ C(remote_userman), &type_boolean, validate_any },
{ C2(restrict, restrictions), &type_restrict, validate_any },
+ { C(rtp_delay_threshold), &type_integer, validate_positive },
{ C(sample_format), &type_sample_format, validate_sample_format },
{ C(scratch), &type_string_accum, validate_isreg },
{ C(sendmail), &type_string, validate_isabspath },
{ C(signal), &type_signal, validate_any },
{ C(smtp_server), &type_string, validate_any },
{ C(sox_generation), &type_integer, validate_non_negative },
- { C2(speaker_backend, api), &type_backend, validate_any },
+ { C2(speaker_backend, api), &type_string, validate_backend },
{ C(speaker_command), &type_string, validate_any },
{ C(stopword), &type_string_accum, validate_any },
{ C(templates), &type_string_accum, validate_isdir },
logname = pw->pw_name;
c->username = xstrdup(logname);
c->refresh = 15;
- c->prefsync = 3600;
+ c->prefsync = 0;
c->signal = SIGKILL;
c->alias = xstrdup("{/artist}{/album}{/title}{ext}");
c->lock = 1;
c->sample_format.endian = ENDIAN_NATIVE;
c->queue_pad = 10;
c->replay_min = 8 * 3600;
- c->api = -1;
+ c->api = NULL;
c->multicast_ttl = 1;
c->multicast_loop = 1;
c->authorization_algorithm = xstrdup("sha1");
c->new_bias_age = 7 * 86400; /* 1 week */
c->new_bias = 4500000; /* 50 times the base weight */
c->sox_generation = DEFAULT_SOX_GENERATION;
+ c->playlist_max = INT_MAX; /* effectively no limit */
+ c->playlist_lock_timeout = 10; /* 10s */
/* Default stopwords */
if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
exit(1);
default_players[n], "disorder-tracklength", (char *)0))
exit(1);
}
+ c->broadcast.af = -1;
+ c->broadcast_from.af = -1;
+ c->listen.af = -1;
+ c->connect.af = -1;
return c;
}
for(n = 0; n < NTRANSFORM; ++n)
set_transform(&cs, whoami, 5, (char **)transform[n]);
}
- if(c->api == -1) {
+ if(!c->api) {
if(c->speaker_command)
- c->api = BACKEND_COMMAND;
- else if(c->broadcast.n)
- c->api = BACKEND_NETWORK;
+ c->api = xstrdup("command");
+ else if(c->broadcast.af != -1)
+ c->api = xstrdup("rtp");
+ else if(config_uaudio_apis)
+ c->api = xstrdup(config_uaudio_apis[0]->name);
else
- c->api = DEFAULT_BACKEND;
+ c->api = xstrdup("<none>");
}
+ if(!strcmp(c->api, "network"))
+ c->api = xstrdup("rtp");
if(server) {
- if(c->api == BACKEND_COMMAND && !c->speaker_command)
+ if(!strcmp(c->api, "command") && !c->speaker_command)
fatal(0, "'api command' but speaker_command is not set");
- if(c->api == BACKEND_NETWORK && !c->broadcast.n)
- fatal(0, "'api network' but broadcast is not set");
+ if((!strcmp(c->api, "rtp")) && c->broadcast.af == -1)
+ fatal(0, "'api rtp' but broadcast is not set");
}
/* Override sample format */
- switch(c->api) {
- case BACKEND_NETWORK:
+ if(!strcmp(c->api, "rtp")) {
c->sample_format.rate = 44100;
c->sample_format.channels = 2;
c->sample_format.bits = 16;
- c->sample_format.endian = ENDIAN_BIG;
- break;
- case BACKEND_COREAUDIO:
+ c->sample_format.endian = ENDIAN_NATIVE;
+ }
+ if(!strcmp(c->api, "coreaudio")) {
c->sample_format.rate = 44100;
c->sample_format.channels = 2;
c->sample_format.bits = 16;
c->sample_format.endian = ENDIAN_NATIVE;
- break;
}
if(!c->default_rights) {
rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER
/** @brief (Re-)read the config file
* @param server If set, do extra checking
+ * @param oldconfig Old configuration for compatibility check
+ * @return 0 on success, non-0 on error
+ *
+ * If @p oldconfig is set, then certain compatibility checks are done between
+ * the old and new configurations.
*/
-int config_read(int server) {
+int config_read(int server,
+ const struct config *oldconfig) {
struct config *c;
char *privconf;
struct passwd *pw;
}
/* install default namepart and transform settings */
config_postdefaults(c, server);
+ if(oldconfig) {
+ int failed = 0;
+ if(strcmp(c->home, oldconfig->home)) {
+ error(0, "'home' cannot be changed without a restart");
+ failed = 1;
+ }
+ if(strcmp(c->alias, oldconfig->alias)) {
+ error(0, "'alias' cannot be changed without a restart");
+ failed = 1;
+ }
+ if(strcmp(c->user, oldconfig->user)) {
+ error(0, "'user' cannot be changed without a restart");
+ failed = 1;
+ }
+ if(c->nice_speaker != oldconfig->nice_speaker) {
+ error(0, "'nice_speaker' cannot be changed without a restart");
+ /* ...but we accept the new config anyway */
+ }
+ if(c->nice_server != oldconfig->nice_server) {
+ error(0, "'nice_server' cannot be changed without a restart");
+ /* ...but we accept the new config anyway */
+ }
+ if(namepartlist_compare(&c->namepart, &oldconfig->namepart)) {
+ error(0, "'namepart' settings cannot be changed without a restart");
+ failed = 1;
+ }
+ if(stringlist_compare(&c->stopword, &oldconfig->stopword)) {
+ error(0, "'stopword' settings cannot be changed without a restart");
+ failed = 1;
+ }
+ if(failed) {
+ error(0, "not installing incompatible new configuration");
+ return -1;
+ }
+ }
/* everything is good so we shall use the new config */
config_free(config);
/* warn about obsolete directives */
error(0, "'allow' will be removed in a future version");
if(c->trust.n)
error(0, "'trust' will be removed in a future version");
+ if(!c->lock)
+ error(0, "'lock' will be removed in a future version");
+ if(c->gap)
+ error(0, "'gap' will be removed in a future version");
+ if(c->prefsync)
+ error(0, "'prefsync' will be removed in a future version");
config = c;
return 0;
}
return config_get_file2(config, name);
}
+static int stringlist_compare(const struct stringlist *a,
+ const struct stringlist *b) {
+ int n = 0, c;
+
+ while(n < a->n && n < b->n) {
+ if((c = strcmp(a->s[n], b->s[n])))
+ return c;
+ ++n;
+ }
+ if(a->n < b->n)
+ return -1;
+ else if(a->n > b->n)
+ return 1;
+ else
+ return 0;
+}
+
+static int namepart_compare(const struct namepart *a,
+ const struct namepart *b) {
+ int c;
+
+ if((c = strcmp(a->part, b->part)))
+ return c;
+ if((c = strcmp(a->res, b->res)))
+ return c;
+ if((c = strcmp(a->replace, b->replace)))
+ return c;
+ if((c = strcmp(a->context, b->context)))
+ return c;
+ if(a->reflags > b->reflags)
+ return 1;
+ if(a->reflags < b->reflags)
+ return -1;
+ return 0;
+}
+
+static int namepartlist_compare(const struct namepartlist *a,
+ const struct namepartlist *b) {
+ int n = 0, c;
+
+ while(n < a->n && n < b->n) {
+ if((c = namepart_compare(&a->s[n], &b->s[n])))
+ return c;
+ ++n;
+ }
+ if(a->n > b->n)
+ return 1;
+ else if(a->n < b->n)
+ return -1;
+ else
+ return 0;
+}
+
/*
Local Variables:
c-basic-offset:2
-
/*
* This file is part of DisOrder.
- * Copyright (C) 2004-2008 Richard Kettlewell
+ * Copyright (C) 2004-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
#include "speaker-protocol.h"
#include "rights.h"
+#include "addr.h"
+
+struct uaudio;
/* Configuration is kept in a @struct config@; the live configuration
* is always pointed to by @config@. Values in @config@ are UTF-8 encoded.
struct namepart {
char *part; /* part */
- pcre *re; /* regexp */
+ pcre *re; /* compiled regexp */
+ char *res; /* regexp as a string */
char *replace; /* replacement string */
char *context; /* context glob */
unsigned reflags; /* regexp flags */
long prefsync; /* preflog sync interval */
/** @brief Secondary listen address */
- struct stringlist listen;
+ struct netaddress listen;
/** @brief Alias format string */
const char *alias;
/** @brief Command execute by speaker to play audio */
const char *speaker_command;
+ /** @brief Pause mode for command backend */
+ const char *pause_mode;
+
/** @brief Target sample format */
struct stream_header sample_format;
/** @brief Sox syntax generation */
long sox_generation;
- /** @brief API used to play sound
- *
- * Choices are @ref BACKEND_ALSA, @ref BACKEND_COMMAND or @ref
- * BACKEND_NETWORK.
- */
- int api;
+ /** @brief API used to play sound */
+ const char *api;
-/* These values had better be non-negative */
-#define BACKEND_ALSA 0 /**< Use ALSA (Linux only) */
-#define BACKEND_COMMAND 1 /**< Execute a command */
-#define BACKEND_NETWORK 2 /**< Transmit RTP */
-#define BACKEND_COREAUDIO 3 /**< Use Core Audio (Mac only) */
-#define BACKEND_OSS 4 /**< Use OSS */
-
-#if HAVE_ALSA_ASOUNDLIB_H
-# define DEFAULT_BACKEND BACKEND_ALSA
-#elif HAVE_SYS_SOUNDCARD_H || EMPEG_HOST
-# define DEFAULT_BACKEND BACKEND_OSS
-#elif HAVE_COREAUDIO_AUDIOHARDWARE_H
-# define DEFAULT_BACKEND BACKEND_COREAUDIO
-#else
-# error Cannot choose a default backend
-#endif
-
+ /** @brief Maximum size of a playlist */
+ long playlist_max;
+
+ /** @brief Maximum lifetime of a playlist lock */
+ long playlist_lock_timeout;
+
/** @brief Home directory for state files */
const char *home;
const char *password;
/** @brief Address to connect to */
- struct stringlist connect;
+ struct netaddress connect;
/** @brief Directories to search for web templates */
struct stringlist templates;
struct transformlist transform; /* path name transformations */
/** @brief Address to send audio data to */
- struct stringlist broadcast;
+ struct netaddress broadcast;
/** @brief Source address for network audio transmission */
- struct stringlist broadcast_from;
+ struct netaddress broadcast_from;
+ /** @brief RTP delay threshold */
+ long rtp_delay_threshold;
+
/** @brief TTL for multicast packets */
long multicast_ttl;
extern struct config *config;
/* the current configuration */
-int config_read(int server);
+int config_read(int server,
+ const struct config *oldconfig);
/* re-read config, return 0 on success or non-0 on error.
* Only updates @config@ if the new configuration is valid. */
extern char *configfile;
extern int config_per_user;
+extern const struct uaudio *const *config_uaudio_apis;
+
#endif /* CONFIGURATION_H */
/*
*/
DB *trackdb_usersdb;
+ /** @brief The playlists database
+ * - Keys are playlist names
+ * - Values are encoded key-value pairs
+ * - Data is user data and cannot be reconstructed
+ */
+ DB *trackdb_playlistsdb;
+
static pid_t db_deadlock_pid = -1; /* deadlock manager PID */
static pid_t rescan_pid = -1; /* rescanner PID */
static int initialized, opened; /* state */
trackdb_noticeddb = open_db("noticed.db",
DB_DUPSORT, DB_BTREE, dbflags, 0666);
trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666);
- if(!trackdb_existing_database) {
+ if(!trackdb_existing_database && !(flags & TRACKDB_READ_ONLY)) {
+ trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666);
/* Stash the database version */
char buf[32];
CLOSE("noticed.db", trackdb_noticeddb);
CLOSE("schedule.db", trackdb_scheduledb);
CLOSE("users.db", trackdb_usersdb);
+ CLOSE("playlists.db", trackdb_playlistsdb);
D(("closed databases"));
}
* Currently we only allow the letters and digits in ASCII. We could be more
* liberal than this but it is a nice simple test. It is critical that
* semicolons are never allowed.
+ *
+ * NB also used by playlist_parse_name() to validate playlist names!
*/
- static int valid_username(const char *user) {
+ int valid_username(const char *user) {
if(!*user)
return 0;
while(*user) {
import pwd
import socket
import binascii
-import sha
+import hashlib
import sys
import locale
_response = re.compile("([0-9]{3}) ?(.*)")
+# hashes
+_hashes = {
+ "sha1": hashlib.sha1,
+ "SHA1": hashlib.sha1,
+ "sha256": hashlib.sha256,
+ "SHA256": hashlib.sha256,
+ "sha384": hashlib.sha384,
+ "SHA384": hashlib.sha384,
+ "sha512": hashlib.sha512,
+ "SHA512": hashlib.sha512,
+};
+
version = "_version_"
########################################################################
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.
"""
password = self.config['password']
else:
password = self.password
- # TODO support algorithms other than SHA-1
- h = sha.sha()
+ h = _hashes[algo]()
h.update(password)
h.update(binascii.unhexlify(challenge))
self._simple("user", user, h.hexdigest())
Returns the ID of the new queue entry.
- Note that queue IDs are unicode strings (because all track information
- values are unicode strings).
+ Note that queue IDs are unicode strings (because all track
+ information values are unicode strings).
"""
res, details = self._simple("play", track)
return unicode(details) # because it's unicode in queue() output
The return value is a list of dictionaries corresponding to
recently played tracks. The next track to be played comes first.
- See disorder_protocol(5) for the meanings of the keys. All keys are
- plain strings but the values will be unicode strings."""
+ See disorder_protocol(5) for the meanings of the keys.
+ All keys are plain strings but the values will be unicode strings."""
return self._somequeue("queue")
def _somedir(self, command, dir, re):
The callback should return True to continue or False to stop (don't
forget this, or your program will mysteriously misbehave). Once you
- stop reading the log the connection is useless and should be deleted.
+ stop reading the log the connection is useless and should be
+ deleted.
It is suggested that you use the disorder.monitor class instead of
calling this method directly, but this is not mandatory.
self._simple("schedule-del", event)
def schedule_get(self, event):
- """Get the details for an event as a dict (returns None if event not found)"""
+ """Get the details for an event as a dict (returns None if
+ event not found)"""
res, details = self._simple("schedule-get", event)
if res == 555:
return None
"""Adopt a randomly picked track"""
self._simple("adopt", id)
+ def playlist_delete(self, playlist):
+ """Delete a playlist"""
+ res, details = self._simple("playlist-delete", playlist)
+ if res == 555:
+ raise operationError(res, details, "playlist-delete")
+
+ def playlist_get(self, playlist):
+ """Get the contents of a playlist
+
+ The return value is an array of track names, or None if there is no
+ such playlist."""
+ res, details = self._simple("playlist-get", playlist)
+ if res == 555:
+ return None
+ return self._body()
+
+ def playlist_lock(self, playlist):
+ """Lock a playlist. Playlists can only be modified when locked."""
+ self._simple("playlist-lock", playlist)
+
+ def playlist_unlock(self):
+ """Unlock the locked playlist."""
+ self._simple("playlist-unlock")
+
+ def playlist_set(self, playlist, tracks):
+ """Set the contents of a playlist. The playlist must be locked.
+
+ Arguments:
+ playlist -- Playlist to set
+ tracks -- Array of tracks"""
+ self._simple_body(tracks, "playlist-set", playlist)
+
+ def playlist_set_share(self, playlist, share):
+ """Set the sharing status of a playlist"""
+ self._simple("playlist-set-share", playlist, share)
+
+ def playlist_get_share(self, playlist):
+ """Returns the sharing status of a playlist"""
+ res, details = self._simple("playlist-get-share", playlist)
+ if res == 555:
+ return None
+ return _split(details)[0]
+
+ def playlists(self):
+ """Returns the list of visible playlists"""
+ self._simple("playlists")
+ return self._body()
+
########################################################################
# I/O infrastructure
else:
raise protocolError(self.who, "invalid response %s")
- def _send(self, *command):
- # Quote and send a command
+ def _send(self, body, *command):
+ # Quote and send a command and optional body
#
# Returns the encoded command.
quoted = _quote(command)
try:
self.w.write(encoded)
self.w.write("\n")
+ if body != None:
+ for l in body:
+ if l[0] == ".":
+ self.w.write(".")
+ self.w.write(l)
+ self.w.write("\n")
+ self.w.write(".\n")
self.w.flush()
return encoded
except IOError, e:
self._disconnect()
raise
- def _simple(self, *command):
+ def _simple(self, *command):
# Issue a simple command, throw an exception on error
#
# If an I/O error occurs, disconnect from the server.
# On success or 'normal' errors returns response as a (code, details) tuple
#
# On error raise operationError
+ return self._simple_body(None, *command)
+
+ def _simple_body(self, body, *command):
+ # Issue a simple command with optional body, throw an exception on error
+ #
+ # If an I/O error occurs, disconnect from the server.
+ #
+ # On success or 'normal' errors returns response as a (code, details) tuple
+ #
+ # On error raise operationError
if self.state == 'disconnected':
self.connect()
if command:
- cmd = self._send(*command)
+ cmd = self._send(body, *command)
else:
cmd = None
res, details = self._response()
class monitor:
"""DisOrder event log monitor class
- Intended to be subclassed with methods corresponding to event log messages
- the implementor cares about over-ridden."""
+ Intended to be subclassed with methods corresponding to event log
+ messages the implementor cares about over-ridden."""
def __init__(self, c=None):
"""Constructor for the monitor class
def run(self):
"""Start monitoring logs. Continues monitoring until one of the
- message-specific methods returns False. Can be called more than once
- (but not recursively!)"""
+ message-specific methods returns False. Can be called more than
+ once (but not recursively!)"""
self.c.log(self._callback)
def when(self):
{ "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(0, "specify only a dump file name");
path = argv[optind];
}
- if(config_read(0)) fatal(0, "cannot read configuration");
+ if(config_read(0, NULL)) fatal(0, "cannot read configuration");
trackdb_init(recover|TRACKDB_MAY_CREATE);
trackdb_open(TRACKDB_NO_UPGRADE);
if(dump) {
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
+ * Copyright (C) 2004-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
*/
#include "disorder-server.h"
+#include "basen.h"
#ifndef NONCE_SIZE
# define NONCE_SIZE 16
#endif
#ifndef CONFIRM_SIZE
-# define CONFIRM_SIZE 10
+/** @brief Size of nonce in confirmation string in 32-bit words
+ *
+ * 64 bits gives 11 digits (in base 62).
+ */
+# define CONFIRM_SIZE 2
#endif
int volume_left, volume_right; /* last known volume */
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), "510 Prohibited\n");
return 1;
}
- if(mixer_control(-1/*as configured*/, &l, &r, set))
+ if(!api || !api->set_volume) {
sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n");
- else {
- sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
- if(l != volume_left || r != volume_right) {
- volume_left = l;
- volume_right = r;
- snprintf(lb, sizeof lb, "%d", l);
- snprintf(rb, sizeof rb, "%d", r);
- eventlog("volume", lb, rb, (char *)0);
- }
+ return 1;
+ }
+ (set ? api->set_volume : api->get_volume)(&l, &r);
+ sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
+ if(l != volume_left || r != volume_right) {
+ volume_left = l;
+ volume_right = r;
+ snprintf(lb, sizeof lb, "%d", l);
+ snprintf(rb, sizeof rb, "%d", r);
+ eventlog("volume", lb, rb, (char *)0);
}
return 1;
}
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_rtp_address(struct conn *c,
char attribute((unused)) **vec,
int attribute((unused)) nvec) {
- if(config->api == BACKEND_NETWORK) {
+ if(api == &uaudio_rtp) {
+ char **addr;
+
+ netaddress_format(&config->broadcast, NULL, &addr);
sink_printf(ev_writer_sink(c->w), "252 %s %s\n",
- quoteutf8(config->broadcast.s[0]),
- quoteutf8(config->broadcast.s[1]));
+ quoteutf8(addr[1]),
+ quoteutf8(addr[2]));
} else
sink_writes(ev_writer_sink(c->w), "550 No RTP\n");
return 1;
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
- *
- * This is used with generic_to_base64() and generic_base64(). We cannot use
- * the MIME table as that contains '+' and '=' which get quoted when
- * URL-encoding. (The CGI still does the URL encoding but it is desirable to
- * avoid it being necessary.)
- */
-static const char confirm_base64_table[] =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/.*";
-
static int c_register(struct conn *c,
char **vec,
int attribute((unused)) nvec) {
- char *buf, *cs;
- size_t bufsize;
- int offset;
-
- /* The confirmation string is base64(username;nonce) */
- bufsize = strlen(vec[0]) + CONFIRM_SIZE + 2;
- buf = xmalloc_noptr(bufsize);
- offset = byte_snprintf(buf, bufsize, "%s;", vec[0]);
- gcry_randomize(buf + offset, CONFIRM_SIZE, GCRY_STRONG_RANDOM);
- cs = generic_to_base64((uint8_t *)buf, offset + CONFIRM_SIZE,
- confirm_base64_table);
+ char *cs;
+ uint32_t nonce[CONFIRM_SIZE];
+ char nonce_str[(32 * CONFIRM_SIZE) / 5 + 1];
+
+ /* The confirmation string is username/base62(nonce). The confirmation
+ * process will pick the username back out to identify them but the _whole_
+ * string is used as the confirmation string. Base 62 means we used only
+ * letters and digits, minimizing the chance of the URL being mispasted. */
+ gcry_randomize(nonce, sizeof nonce, GCRY_STRONG_RANDOM);
+ if(basen(nonce, CONFIRM_SIZE, nonce_str, sizeof nonce_str, 62)) {
+ error(0, "buffer too small encoding confirmation string");
+ sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
+ }
+ byte_xasprintf(&cs, "%s/%s", vec[0], nonce_str);
if(trackdb_adduser(vec[0], vec[1], config->default_rights, vec[2], cs))
sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
else
static int c_confirm(struct conn *c,
char **vec,
int attribute((unused)) nvec) {
- size_t nuser;
char *user, *sep;
rights_type rights;
const char *host;
sink_writes(ev_writer_sink(c->w), "530 Authentication failure\n");
return 1;
}
- if(!(user = generic_base64(vec[0], &nuser, confirm_base64_table))
- || !(sep = memchr(user, ';', nuser))) {
+ /* Picking the LAST / means we don't (here) rule out slashes in usernames. */
+ if(!(sep = strrchr(vec[0], '/'))) {
sink_writes(ev_writer_sink(c->w), "550 Malformed confirmation string\n");
return 1;
}
- *sep = 0;
+ user = xstrndup(vec[0], sep - vec[0]);
if(trackdb_confirm(user, vec[0], &rights))
sink_writes(ev_writer_sink(c->w), "550 Incorrect confirmation string\n");
else {
return 1;
}
+ static int playlist_response(struct conn *c,
+ int err) {
+ switch(err) {
+ case 0:
+ assert(!"cannot cope with success");
+ case EACCES:
+ sink_writes(ev_writer_sink(c->w), "550 Access denied\n");
+ break;
+ case EINVAL:
+ sink_writes(ev_writer_sink(c->w), "550 Invalid playlist name\n");
+ break;
+ case ENOENT:
+ sink_writes(ev_writer_sink(c->w), "555 No such playlist\n");
+ break;
+ default:
+ sink_writes(ev_writer_sink(c->w), "550 Error accessing playlist\n");
+ break;
+ }
+ return 1;
+ }
+
+ static int c_playlist_get(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char **tracks;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, &tracks, 0, 0)))
+ return list_response(c, "Playlist contents follows", tracks);
+ else
+ return playlist_response(c, err);
+ }
+
+ static int c_playlist_set(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ return fetch_body(c, c_playlist_set_body, vec[0]);
+ }
+
+ static int c_playlist_set_body(struct conn *c,
+ char **body,
+ int nbody,
+ void *u) {
+ const char *playlist = u;
+ int err;
+
+ if(!c->locked_playlist
+ || strcmp(playlist, c->locked_playlist)) {
+ sink_writes(ev_writer_sink(c->w), "550 Playlist is not locked\n");
+ return 1;
+ }
+ if(!(err = trackdb_playlist_set(playlist, c->who,
+ body, nbody, 0))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+ }
+
+ static int c_playlist_get_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char *share;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, 0, 0, &share))) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(share));
+ return 1;
+ } else
+ return playlist_response(c, err);
+ }
+
+ static int c_playlist_set_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_set(vec[0], c->who, 0, 0, vec[1]))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+ }
+
+ static int c_playlists(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ char **p;
+
+ trackdb_playlist_list(c->who, &p, 0);
+ return list_response(c, "List of playlists follows", p);
+ }
+
+ static int c_playlist_delete(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_delete(vec[0], c->who))) {
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+ }
+
+ static int c_playlist_lock(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+ struct conn *cc;
+
+ /* Check we're allowed to modify this playlist */
+ if((err = trackdb_playlist_set(vec[0], c->who, 0, 0, 0)))
+ return playlist_response(c, err);
+ /* If we hold a lock don't allow a new one */
+ if(c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Already holding a lock\n");
+ return 1;
+ }
+ /* See if some other connection locks the same playlist */
+ for(cc = connections; cc; cc = cc->next)
+ if(cc->locked_playlist && !strcmp(cc->locked_playlist, vec[0]))
+ break;
+ if(cc) {
+ /* TODO: implement config->playlist_lock_timeout */
+ sink_writes(ev_writer_sink(c->w), "550 Already locked\n");
+ return 1;
+ }
+ c->locked_playlist = xstrdup(vec[0]);
+ time(&c->locked_when);
+ sink_writes(ev_writer_sink(c->w), "250 Acquired lock\n");
+ return 1;
+ }
+
+ static int c_playlist_unlock(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(!c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Not holding a lock\n");
+ return 1;
+ }
+ c->locked_playlist = 0;
+ sink_writes(ev_writer_sink(c->w), "250 Released lock\n");
+ return 1;
+ }
+
static const struct command {
/** @brief Command name */
const char *name;
{ "pause", 0, 0, c_pause, RIGHT_PAUSE },
{ "play", 1, 1, c_play, RIGHT_PLAY },
{ "playing", 0, 0, c_playing, RIGHT_READ },
+ { "playlist-delete", 1, 1, c_playlist_delete, RIGHT_PLAY },
+ { "playlist-get", 1, 1, c_playlist_get, RIGHT_READ },
+ { "playlist-get-share", 1, 1, c_playlist_get_share, RIGHT_READ },
+ { "playlist-lock", 1, 1, c_playlist_lock, RIGHT_PLAY },
+ { "playlist-set", 1, 1, c_playlist_set, RIGHT_PLAY },
+ { "playlist-set-share", 2, 2, c_playlist_set_share, RIGHT_PLAY },
+ { "playlist-unlock", 0, 0, c_playlist_unlock, RIGHT_PLAY },
+ { "playlists", 0, 0, c_playlists, RIGHT_READ },
{ "prefs", 1, 1, c_prefs, RIGHT_READ },
{ "queue", 0, 0, c_queue, RIGHT_READ },
{ "random-disable", 0, 0, c_random_disable, RIGHT_GLOBAL_PREFS },
{ "volume", 0, 2, c_volume, RIGHT_READ|RIGHT_VOLUME }
};
+ /** @brief Fetch a command body
+ * @param c Connection
+ * @param body_callback Called with body
+ * @param u Passed to body_callback
+ * @return 1
+ */
+ static int fetch_body(struct conn *c,
+ body_callback_type body_callback,
+ void *u) {
+ assert(c->line_reader == command);
+ c->line_reader = body_line;
+ c->body_callback = body_callback;
+ c->body_u = u;
+ vector_init(c->body);
+ return 1;
+ }
+
+ /** @brief @ref line_reader_type callback for command body lines
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
+ static int body_line(struct conn *c,
+ char *line) {
+ if(*line == '.') {
+ ++line;
+ if(!*line) {
+ /* That's the lot */
+ c->line_reader = command;
+ vector_terminate(c->body);
+ return c->body_callback(c, c->body->vec, c->body->nvec, c->body_u);
+ }
+ }
+ vector_append(c->body, xstrdup(line));
+ return 1; /* completed */
+ }
+
static void command_error(const char *msg, void *u) {
struct conn *c = u;
sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg);
}
- /* process a command. Return 1 if complete, 0 if incomplete. */
+ /** @brief @ref line_reader_type callback for commands
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
static int command(struct conn *c, char *line) {
char **vec;
int nvec, n;
while((eol = memchr(ptr, '\n', bytes))) {
*eol++ = 0;
ev_reader_consume(reader, eol - (char *)ptr);
- complete = command(c, ptr);
+ complete = c->line_reader(c, ptr); /* usually command() */
bytes -= (eol - (char *)ptr);
ptr = eol;
if(!complete) {
c->reader = reader_callback;
c->l = l;
c->rights = 0;
+ c->line_reader = command;
connections = c;
gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
sink_printf(ev_writer_sink(c->w), "231 %d %s %s\n",
l->pf = pf;
if(ev_listen(ev, fd, listen_callback, l, "server listener"))
exit(EXIT_FAILURE);
+ info("listening on %s", name);
return fd;
}
#
# This file is part of DisOrder.
-# Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007-2009 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
TESTS=cookie.py dbversion.py dump.py files.py play.py queue.py \
recode.py search.py user-upgrade.py user.py aliases.py \
- schedule.py hashes.py
- schedule.py playlists.py
++ schedule.py hashes.py playlists.py
TESTS_ENVIRONMENT=${PYTHON} -u
clean-local:
rm -rf testroot *.log *.pyc
-EXTRA_DIST=dtest.py ${TESTS}
+EXTRA_DIST=dtest.py ${TESTS} fail.py
CLEANFILES=*.gcda *.gcov *.gcno *.c.html index.html