From: Richard Kettlewell Date: Sat, 10 Oct 2009 20:03:23 +0000 (+0100) Subject: Merge playlist support. X-Git-Tag: 5.0~86^2~4 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/c9467b7a34160c4e25580a2dc82087c5ae0bb2d0?hp=-c Merge playlist support. --- c9467b7a34160c4e25580a2dc82087c5ae0bb2d0 diff --combined clients/disorder.c index ccfaf20,4161d93..a246eee --- a/clients/disorder.c +++ b/clients/disorder.c @@@ -32,7 -32,7 +32,8 @@@ #include #include #include +#include + #include #include "configuration.h" #include "syscalls.h" @@@ -51,7 -51,7 +52,8 @@@ #include "vector.h" #include "version.h" #include "dateparse.h" +#include "trackdb.h" + #include "inputline.h" static disorder_client *client; @@@ -65,7 -65,6 +67,7 @@@ static const struct option options[] = { "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 } }; @@@ -160,7 -159,7 +162,7 @@@ static void cf_shutdown(char attribute( 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); } @@@ -188,15 -187,34 +190,34 @@@ static void cf_queue(char attribute((un } static void cf_quack(char attribute((unused)) **argv) { - xprintf("\n" - " .------------------.\n" - " | Naath is a babe! |\n" - " `---------+--------'\n" - " \\\n" - " >0\n" - " (<)'\n" - "~~~~~~~~~~~~~~~~~~~~~~\n" - "\n"); + if(!strcasecmp(nl_langinfo(CODESET), "utf-8")) { + #define TL "\xE2\x95\xAD" + #define TR "\xE2\x95\xAE" + #define BR "\xE2\x95\xAF" + #define BL "\xE2\x95\xB0" + #define H "\xE2\x94\x80" + #define V "\xE2\x94\x82" + #define T "\xE2\x94\xAC" + xprintf("\n" + " "TL H H H H H H H H H H H H H H H H H H TR"\n" + " "V" Naath is a babe! "V"\n" + " "BL H H H H H H H H H T H H H H H H H H BR"\n" + " \\\n" + " >0\n" + " (<)'\n" + "~~~~~~~~~~~~~~~~~~~~~~\n" + "\n"); + } else { + xprintf("\n" + " .------------------.\n" + " | Naath is a babe! |\n" + " `---------+--------'\n" + " \\\n" + " >0\n" + " (<)'\n" + "~~~~~~~~~~~~~~~~~~~~~~\n" + "\n"); + } } static void cf_somelist(char **argv, @@@ -585,6 -603,61 +606,61 @@@ static void cf_adopt(char **argv) exit(EXIT_FAILURE); } + static void cf_playlists(char attribute((unused)) **argv) { + char **vec; + + if(disorder_playlists(getclient(), &vec, 0)) + exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); + } + + static void cf_playlist_del(char **argv) { + if(disorder_playlist_delete(getclient(), argv[0])) + exit(EXIT_FAILURE); + } + + static void cf_playlist_get(char **argv) { + char **vec; + + if(disorder_playlist_get(getclient(), argv[0], &vec, 0)) + exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); + } + + static void cf_playlist_set(char **argv) { + struct vector v[1]; + FILE *input; + const char *tag; + char *l; + + if(argv[1]) { + // Read track list from file + if(!(input = fopen(argv[1], "r"))) + fatal(errno, "opening %s", argv[1]); + tag = argv[1]; + } else { + // Read track list from standard input + input = stdin; + tag = "stdin"; + } + vector_init(v); + while(!inputline(tag, input, &l, '\n')) { + if(!strcmp(l, ".")) + break; + vector_append(v, l); + } + if(ferror(input)) + fatal(errno, "reading %s", tag); + if(input != stdin) + fclose(input); + if(disorder_playlist_lock(getclient(), argv[0]) + || disorder_playlist_set(getclient(), argv[0], v->vec, v->nvec) + || disorder_playlist_unlock(getclient())) + exit(EXIT_FAILURE); + } + static const struct command { const char *name; int min, max; @@@ -638,6 -711,14 +714,14 @@@ "Add TRACKS to the end of the queue" }, { "playing", 0, 0, cf_playing, 0, "", "Report the playing track" }, + { "playlist-del", 1, 1, cf_playlist_del, 0, "PLAYLIST", + "Delete a playlist" }, + { "playlist-get", 1, 1, cf_playlist_get, 0, "PLAYLIST", + "Get the contents of a playlist" }, + { "playlist-set", 1, 2, cf_playlist_set, isarg_filename, "PLAYLIST [PATH]", + "Set the contents of a playlist" }, + { "playlists", 0, 0, cf_playlists, 0, "", + "List playlists" }, { "prefs", 1, 1, cf_prefs, 0, "TRACK", "Display all the preferences for TRACK" }, { "quack", 0, 0, cf_quack, 0, 0, 0 }, @@@ -730,28 -811,8 +814,28 @@@ static void help_commands(void) 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; @@@ -762,7 -823,7 +846,7 @@@ 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(); @@@ -773,11 -834,10 +857,11 @@@ 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; @@@ -785,16 -845,9 +869,16 @@@ 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) diff --combined disobedience/Makefile.am index c7b702b,94a4c78..f8bdb14 --- a/disobedience/Makefile.am +++ b/disobedience/Makefile.am @@@ -1,6 -1,6 +1,6 @@@ # # 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 @@@ -28,9 -28,9 +28,9 @@@ disobedience_SOURCES=disobedience.h dis 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: diff --combined disobedience/disobedience.c index 38fc6eb,1f9ee43..2b31941 --- a/disobedience/disobedience.c +++ b/disobedience/disobedience.c @@@ -1,6 -1,6 +1,6 @@@ /* * 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 @@@ -20,12 -20,12 +20,12 @@@ */ #include "disobedience.h" -#include "mixer.h" #include "version.h" #include #include #include +#include /* Apologies for the numerous de-consting casts, but GLib et al do not seem to * have heard of const. */ @@@ -68,9 -68,6 +68,9 @@@ int volume_l /** @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 */ @@@ -243,6 -240,7 +243,7 @@@ static gboolean periodic_slow(gpointer /* Update everything to be sure that the connection to the server hasn't * mysteriously gone stale on us. */ all_update(); + event_raise("periodic-slow", 0); /* Recheck RTP status too */ check_rtp_address(0, 0, 0); return TRUE; /* don't remove me */ @@@ -266,10 -264,10 +267,10 @@@ static gboolean periodic_fast(gpointer } 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); @@@ -285,6 -283,7 +286,7 @@@ recheck_rights = 0; if(recheck_rights) check_rights(); + event_raise("periodic-fast", 0); return TRUE; } @@@ -449,24 -448,13 +451,24 @@@ int main(int argc, char **argv) } 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())) @@@ -493,6 -481,7 +495,7 @@@ disorder_eclient_version(client, version_completed, 0); event_register("log-connected", check_rtp_address, 0); suppress_actions = 0; + playlists_init(); /* If no password is set yet pop up a login box */ if(!config->password) login_box(); diff --combined disobedience/disobedience.h index ca5f7ef,4687724..e34d383 --- a/disobedience/disobedience.h +++ b/disobedience/disobedience.h @@@ -47,7 -47,6 +47,7 @@@ #include "eventdist.h" #include "split.h" #include "timeval.h" +#include "uaudio.h" #include #include @@@ -105,7 -104,6 +105,7 @@@ extern GtkTooltips *tips 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; @@@ -252,6 -250,18 +252,18 @@@ void load_settings(void) void set_tool_colors(GtkWidget *w); void popup_settings(void); + /* Playlists */ + + void playlists_init(void); + void edit_playlists(gpointer callback_data, + guint callback_action, + GtkWidget *menu_item); + extern char **playlists; + extern int nplaylists; + extern GtkWidget *playlists_widget; + extern GtkWidget *playlists_menu; + extern GtkWidget *editplaylists_widget; + #endif /* DISOBEDIENCE_H */ /* diff --combined disobedience/menu.c index 15fb4fb,1d50223..f8bd58e --- a/disobedience/menu.c +++ b/disobedience/menu.c @@@ -24,6 -24,9 +24,9 @@@ static GtkWidget *selectall_widget; static GtkWidget *selectnone_widget; static GtkWidget *properties_widget; + GtkWidget *playlists_widget; + GtkWidget *playlists_menu; + GtkWidget *editplaylists_widget; /** @brief Main menu widgets */ GtkItemFactory *mainmenufactory; @@@ -113,7 -116,7 +116,7 @@@ static void edit_menu_show(GtkWidget at && t->selectnone_sensitive(t->extra)); } } - + /** @brief Fetch version in order to display the about... popup */ static void about_popup(gpointer attribute((unused)) callback_data, guint attribute((unused)) callback_action, @@@ -173,7 -176,7 +176,7 @@@ static void about_popup_got_version(voi 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*/); @@@ -293,6 -296,15 +296,15 @@@ GtkWidget *menubar(GtkWidget *w) 0, /* item_type */ 0 /* extra_data */ }, + { + (char *)"/Edit/Edit playlists", /* path */ + 0, /* accelerator */ + edit_playlists, /* callback */ + 0, /* callback_action */ + 0, /* item_type */ + 0 /* extra_data */ + }, + { (char *)"/Control", /* path */ @@@ -334,6 -346,14 +346,14 @@@ (char *)"", /* item_type */ 0 /* extra_data */ }, + { + (char *)"/Control/Activate playlist", /* path */ + 0, /* accelerator */ + 0, /* callback */ + 0, /* callback_action */ + (char *)"", /* item_type */ + 0 /* extra_data */ + }, { (char *)"/Help", /* path */ @@@ -378,15 -398,23 +398,23 @@@ "/Edit/Deselect all tracks"); properties_widget = gtk_item_factory_get_widget(mainmenufactory, "/Edit/Track properties"); + playlists_widget = gtk_item_factory_get_item(mainmenufactory, + "/Control/Activate playlist"); + playlists_menu = gtk_item_factory_get_widget(mainmenufactory, + "/Control/Activate playlist"); + editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory, + "/Edit/Edit playlists"); assert(selectall_widget != 0); assert(selectnone_widget != 0); assert(properties_widget != 0); + assert(playlists_widget != 0); + assert(playlists_menu != 0); + assert(editplaylists_widget != 0); - GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory, "/Edit"); g_signal_connect(edit_widget, "show", G_CALLBACK(edit_menu_show), 0); - + event_register("rights-changed", menu_rights_changed, 0); users_set_sensitive(0); m = gtk_item_factory_get_widget(mainmenufactory, diff --combined doc/disorder.1.in index afa1eaa,784b37a..b61a825 --- a/doc/disorder.1.in +++ b/doc/disorder.1.in @@@ -152,6 -152,23 +152,23 @@@ Add \fITRACKS\fR to the end of the queu .B playing Report the currently playing track. .TP + .B playlist-del \fIPLAYLIST\fR + Deletes playlist \fIPLAYLIST\fR. + .TP + .B playlist-get \fIPLAYLIST\fR + Gets the contents of playlist \fIPLAYLIST\fR. + .TP + .B playlist-set \fIPLAYLIST\fR [\fIPATH\fR] + Set the contents of playlist \fIPLAYLIST\fR. + If an absolute path name is specified then the track list is read from + that filename. + Otherwise the track list is read from standard input. + In either case, the list is terminated either by end of file or by a line + containing a single ".". + .TP + .B playlists + Lists known playlists (in no particular order). + .TP .B prefs \fITRACK\fR Display all the preferences for \fITRACK\fR. See \fBdisorder_preferences\fR (5). @@@ -173,11 -190,6 +190,11 @@@ recently played one .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. diff --combined doc/disorder_protocol.5.in index b42ebd5,4ad1d42..a0baadb --- a/doc/disorder_protocol.5.in +++ b/doc/disorder_protocol.5.in @@@ -38,6 -38,15 +38,15 @@@ that comments are prohibited Bodies borrow their syntax from RFC821; they consist of zero or more ordinary lines, with any initial full stop doubled up, and are terminated by a line consisting of a full stop and a line feed. + .PP + Commands only have a body if explicitly stated below. + If they do have a body then the body should always be sent immediately; + unlike (for instance) the SMTP "DATA" command there is no intermediate step + where the server asks for the body to be sent. + .PP + Replies also only have a body if stated below. + The presence of a reply body can always be inferred from the response code; + if the last digit is a 3 then a body is present, otherwise it is not. .SH COMMANDS Commands always have a command name as the first field of the line; responses always have a 3-digit response code as the first field. @@@ -47,8 -56,6 +56,6 @@@ All commands require the connection to stated otherwise. If not stated otherwise, the \fBread\fR right is sufficient to execute the command. - .PP - Neither commands nor responses have a body unless stated otherwise. .TP .B adduser \fIUSERNAME PASSWORD \fR[\fIRIGHTS\fR] Create a new user with the given username and password. @@@ -208,6 -215,43 +215,43 @@@ track information (see below) .IP If the response is \fB259\fR then nothing is playing. .TP + .B playlist-delete \fIPLAYLIST\fR + Delete a playlist. + Requires permission to modify that playlist and the \fBplay\fR right. + .TP + .B playlist-get \fIPLAYLIST\fR + Get the contents of a playlist, in a response body. + Requires permission to read that playlist and the \fBread\fR right. + .TP + .B playlist-get-share \fIPLAYLIST\fR + Get the sharing status of a playlist. + The result will be \fBpublic\fR, \fBprivate\fR or \fBshared\fR. + Requires permission to read that playlist and the \fBread\fR right. + .TP + .B playlist-lock \fIPLAYLIST\fR + Lock a playlist. + Requires permission to modify that playlist and the \fBplay\fR right. + Only one playlist may be locked at a time on a given connection and the lock + automatically expires when the connection is closed. + .TP + .B playlist-set \fIPLAYLIST\fR + Set the contents of a playlist. + The new contents should be supplied in a command body. + Requires permission to modify that playlist and the \fBplay\fR right. + The playlist must be locked. + .TP + .B playlist-set-share \fIPLAYLIST\fR \fISHARE\fR + Set the sharing status of a playlist to + \fBpublic\fR, \fBprivate\fR or \fBshared\fR. + Requires permission to modify that playlist and the \fBplay\fR right. + .TP + .B playlist-unlock\fR + Unlock the locked playlist. + .TP + .B playlists + List all playlists that this connection has permission to read. + Requires the \fBread\fR right. + .TP .B prefs \fBTRACK\fR Send back the preferences for \fITRACK\fR in a response body. Each line of the response has the usual line syntax, the first field being the @@@ -238,11 -282,6 +282,11 @@@ See below for the track information syn .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. @@@ -598,6 -637,21 +642,21 @@@ Further details aren't included any mor .B playing \fITRACK\fR [\fIUSERNAME\fR] Started playing \fITRACK\fR. .TP + .B playlist_created \fIPLAYLIST\fR \fISHARING\fR + Sent when a playlist is created. + For private playlists this is intended to be sent only to the owner (but + this is not currently implemented). + .TP + .B playlist_deleted \fIPLAYLIST\fR + Sent when a playlist is deleted. + For private playlists this is intended to be sent only to the owner (but + this is not currently implemented). + .TP + .B playlist_modified \fIPLAYLIST\fR \fISHARING\fR + Sent when a playlist is modified (either its contents or its sharing status). + For private playlists this is intended to be sent only to the owner (but + this is not currently implemented). + .TP .B queue \fIQUEUE-ENTRY\fR... Added \fITRACK\fR to the queue. .TP diff --combined lib/Makefile.am index 4717fd6,ca0e82d..d0890e1 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@@ -1,6 -1,6 +1,6 @@@ # # 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 @@@ -27,6 -27,7 +27,6 @@@ endi 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 \ @@@ -38,7 -39,6 +38,7 @@@ 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 \ @@@ -54,11 -54,12 +54,11 @@@ 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 \ @@@ -78,12 -79,9 +78,13 @@@ 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 \ diff --combined lib/client.c index 20db675,f8a2c9e..a0502eb --- a/lib/client.c +++ b/lib/client.c @@@ -153,6 -153,8 +153,8 @@@ static int check_response(disorder_clie * @param c Client * @param rp Where to store result, or NULL * @param cmd Command + * @param body Body or NULL + * @param nbody Length of body or -1 * @param ap Arguments (UTF-8), terminated by (char *)0 * @return 0 on success, non-0 on error * @@@ -163,10 -165,21 +165,21 @@@ * * NB that the response will NOT be converted to the local encoding * nor will quotes be stripped. See dequote(). + * + * If @p body is not NULL then the body is sent immediately after the + * command. @p nbody should be the number of lines or @c -1 to count + * them if @p body is NULL-terminated. + * + * Usually you would call this via one of the following interfaces: + * - disorder_simple() + * - disorder_simple_body() + * - disorder_simple_list() */ static int disorder_simple_v(disorder_client *c, char **rp, - const char *cmd, va_list ap) { + const char *cmd, + char **body, int nbody, + va_list ap) { const char *arg; struct dynstr d; @@@ -185,13 -198,32 +198,32 @@@ dynstr_append(&d, '\n'); dynstr_terminate(&d); D(("command: %s", d.vec)); - if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) { - byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno)); - error(errno, "error writing to %s", c->ident); - return -1; + if(fputs(d.vec, c->fpout) < 0) + goto write_error; + if(body) { + if(nbody < 0) + for(nbody = 0; body[nbody]; ++nbody) + ; + for(int n = 0; n < nbody; ++n) { + if(body[n][0] == '.') + if(fputc('.', c->fpout) < 0) + goto write_error; + if(fputs(body[n], c->fpout) < 0) + goto write_error; + if(fputc('\n', c->fpout) < 0) + goto write_error; + } + if(fputs(".\n", c->fpout) < 0) + goto write_error; } + if(fflush(c->fpout)) + goto write_error; } return check_response(c, rp); + write_error: + byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno)); + error(errno, "error writing to %s", c->ident); + return -1; } /** @brief Issue a command and parse a simple response @@@ -218,7 -250,30 +250,30 @@@ static int disorder_simple(disorder_cli int ret; va_start(ap, cmd); - ret = disorder_simple_v(c, rp, cmd, ap); + ret = disorder_simple_v(c, rp, cmd, 0, 0, ap); + va_end(ap); + return ret; + } + + /** @brief Issue a command with a body and parse a simple response + * @param c Client + * @param rp Where to store result, or NULL (UTF-8) + * @param body Pointer to body + * @param nbody Size of body + * @param cmd Command + * @return 0 on success, non-0 on error + * + * See disorder_simple(). + */ + static int disorder_simple_body(disorder_client *c, + char **rp, + char **body, int nbody, + const char *cmd, ...) { + va_list ap; + int ret; + + va_start(ap, cmd); + ret = disorder_simple_v(c, rp, cmd, body, nbody, ap); va_end(ap); return ret; } @@@ -396,7 -451,7 +451,7 @@@ int disorder_connect(disorder_client *c 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, @@@ -670,6 -725,8 +725,8 @@@ static int readlist(disorder_client *c * *)0. They should be in UTF-8. * * 5xx responses count as errors. + * + * See disorder_simple(). */ static int disorder_simple_list(disorder_client *c, char ***vecp, int *nvecp, @@@ -678,7 -735,7 +735,7 @@@ int ret; va_start(ap, cmd); - ret = disorder_simple_v(c, 0, cmd, ap); + ret = disorder_simple_v(c, 0, cmd, 0, 0, ap); va_end(ap); if(ret) return ret; return readlist(c, vecp, nvecp); @@@ -1311,6 -1368,103 +1368,103 @@@ int disorder_adopt(disorder_client *c, return disorder_simple(c, 0, "adopt", id, (char *)0); } + /** @brief Delete a playlist + * @param c Client + * @param playlist Playlist to delete + * @return 0 on success, non-0 on error + */ + int disorder_playlist_delete(disorder_client *c, + const char *playlist) { + return disorder_simple(c, 0, "playlist-delete", playlist, (char *)0); + } + + /** @brief Get the contents of a playlist + * @param c Client + * @param playlist Playlist to get + * @param tracksp Where to put list of tracks + * @param ntracksp Where to put count of tracks + * @return 0 on success, non-0 on error + */ + int disorder_playlist_get(disorder_client *c, const char *playlist, + char ***tracksp, int *ntracksp) { + return disorder_simple_list(c, tracksp, ntracksp, + "playlist-get", playlist, (char *)0); + } + + /** @brief List all readable playlists + * @param c Client + * @param playlistsp Where to put list of playlists + * @param nplaylistsp Where to put count of playlists + * @return 0 on success, non-0 on error + */ + int disorder_playlists(disorder_client *c, + char ***playlistsp, int *nplaylistsp) { + return disorder_simple_list(c, playlistsp, nplaylistsp, + "playlists", (char *)0); + } + + /** @brief Get the sharing status of a playlist + * @param c Client + * @param playlist Playlist to inspect + * @param sharep Where to put sharing status + * @return 0 on success, non-0 on error + * + * Possible @p sharep values are @c public, @c private and @c shared. + */ + int disorder_playlist_get_share(disorder_client *c, const char *playlist, + char **sharep) { + return disorder_simple(c, sharep, + "playlist-get-share", playlist, (char *)0); + } + + /** @brief Get the sharing status of a playlist + * @param c Client + * @param playlist Playlist to modify + * @param share New sharing status + * @return 0 on success, non-0 on error + * + * Possible @p share values are @c public, @c private and @c shared. + */ + int disorder_playlist_set_share(disorder_client *c, const char *playlist, + const char *share) { + return disorder_simple(c, 0, + "playlist-set-share", playlist, share, (char *)0); + } + + /** @brief Lock a playlist for modifications + * @param c Client + * @param playlist Playlist to lock + * @return 0 on success, non-0 on error + */ + int disorder_playlist_lock(disorder_client *c, const char *playlist) { + return disorder_simple(c, 0, + "playlist-lock", playlist, (char *)0); + } + + /** @brief Unlock the locked playlist + * @param c Client + * @return 0 on success, non-0 on error + */ + int disorder_playlist_unlock(disorder_client *c) { + return disorder_simple(c, 0, + "playlist-unlock", (char *)0); + } + + /** @brief Set the contents of a playlst + * @param c Client + * @param playlist Playlist to modify + * @param tracks List of tracks + * @param ntracks Length of @p tracks (or -1 to count up to the first NULL) + * @return 0 on success, non-0 on error + */ + int disorder_playlist_set(disorder_client *c, + const char *playlist, + char **tracks, + int ntracks) { + return disorder_simple_body(c, 0, tracks, ntracks, + "playlist-set", playlist, (char *)0); + } + /* Local Variables: c-basic-offset:2 diff --combined lib/configuration.c index dc3d009,cd83224..ff59968 --- a/lib/configuration.c +++ b/lib/configuration.c @@@ -1,6 -1,6 +1,6 @@@ /* * 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 @@@ -43,12 -43,12 +43,12 @@@ #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 * @@@ -62,12 -62,6 +62,12 @@@ char *configfile */ 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 */ @@@ -109,11 -103,6 +109,11 @@@ struct conftype /** @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) { @@@ -425,7 -414,6 +425,7 @@@ static int set_namepart(const struct co 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; @@@ -478,6 -466,52 +478,6 @@@ static int set_transform(const struct c 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) { @@@ -495,18 -529,6 +495,18 @@@ 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, @@@ -516,7 -538,6 +516,7 @@@ 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, @@@ -569,7 -590,6 +569,7 @@@ static void free_namepartlist(struct co 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); } @@@ -592,13 -612,6 +592,13 @@@ static void free_transformlist(struct c 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 @@@ -614,8 -627,8 +614,8 @@@ 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 */ @@@ -841,63 -854,54 +841,63 @@@ static int validate_alias(const struct 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; @@@ -912,15 -916,15 +912,15 @@@ 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 }, @@@ -929,7 -933,7 +929,7 @@@ { 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 }, @@@ -945,8 -949,9 +945,10 @@@ { 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 }, @@@ -955,7 -960,6 +957,7 @@@ { 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 }, @@@ -963,7 -967,7 +965,7 @@@ { 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 }, @@@ -1163,7 -1167,7 +1165,7 @@@ static struct config *config_default(vo 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; @@@ -1176,7 -1180,7 +1178,7 @@@ 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"); @@@ -1195,6 -1199,8 +1197,8 @@@ 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); @@@ -1207,10 -1213,6 +1211,10 @@@ 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; } @@@ -1279,36 -1281,34 +1283,36 @@@ static void config_postdefaults(struct 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(""); } + 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 @@@ -1333,14 -1333,8 +1337,14 @@@ /** @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; @@@ -1375,41 -1369,6 +1379,41 @@@ } /* 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 */ @@@ -1419,12 -1378,6 +1423,12 @@@ 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; } @@@ -1464,59 -1417,6 +1468,59 @@@ char *config_get_file(const char *name 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 diff --combined lib/configuration.h index 875b9d6,54fb4f8..9170a2f --- a/lib/configuration.h +++ b/lib/configuration.h @@@ -1,6 -1,7 +1,6 @@@ - /* * 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 @@@ -26,9 -27,6 +26,9 @@@ #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. @@@ -70,8 -68,7 +70,8 @@@ struct collectionlist 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 */ @@@ -156,7 -153,7 +156,7 @@@ struct config long prefsync; /* preflog sync interval */ /** @brief Secondary listen address */ - struct stringlist listen; + struct netaddress listen; /** @brief Alias format string */ const char *alias; @@@ -173,18 -170,42 +173,24 @@@ /** @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; + /** @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 */ -#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 Home directory for state files */ const char *home; @@@ -195,7 -216,7 +201,7 @@@ const char *password; /** @brief Address to connect to */ - struct stringlist connect; + struct netaddress connect; /** @brief Directories to search for web templates */ struct stringlist templates; @@@ -235,14 -256,11 +241,14 @@@ 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; @@@ -293,8 -311,7 +299,8 @@@ 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. */ @@@ -317,8 -334,6 +323,8 @@@ char *config_private(void) extern char *configfile; extern int config_per_user; +extern const struct uaudio *const *config_uaudio_apis; + #endif /* CONFIGURATION_H */ /* diff --combined lib/trackdb.c index 83a67af,b98752f..a188caa --- a/lib/trackdb.c +++ b/lib/trackdb.c @@@ -157,6 -157,13 +157,13 @@@ DB *trackdb_scheduledb */ DB *trackdb_usersdb; + /** @brief The playlists database + * - Keys are playlist names + * - Values are encoded key-value pairs + * - Data is user data and cannot be reconstructed + */ + DB *trackdb_playlistsdb; + static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ static pid_t rescan_pid = -1; /* rescanner PID */ static int initialized, opened; /* state */ @@@ -472,7 -479,8 +479,8 @@@ void trackdb_open(int flags) trackdb_noticeddb = open_db("noticed.db", DB_DUPSORT, DB_BTREE, dbflags, 0666); trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666); + if(!trackdb_existing_database && !(flags & TRACKDB_READ_ONLY)) { + trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666); - if(!trackdb_existing_database) { /* Stash the database version */ char buf[32]; @@@ -503,6 -511,7 +511,7 @@@ void trackdb_close(void) CLOSE("noticed.db", trackdb_noticeddb); CLOSE("schedule.db", trackdb_scheduledb); CLOSE("users.db", trackdb_usersdb); + CLOSE("playlists.db", trackdb_playlistsdb); D(("closed databases")); } @@@ -2553,8 -2562,10 +2562,10 @@@ static int trusted(const char *user) * Currently we only allow the letters and digits in ASCII. We could be more * liberal than this but it is a nice simple test. It is critical that * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! */ - static int valid_username(const char *user) { + int valid_username(const char *user) { if(!*user) return 0; while(*user) { diff --combined python/disorder.py.in index f6fe1a4,3e1541e..d06c7ee --- a/python/disorder.py.in +++ b/python/disorder.py.in @@@ -50,7 -50,7 +50,7 @@@ import o import pwd import socket import binascii -import sha +import hashlib import sys import locale @@@ -66,18 -66,6 +66,18 @@@ _unquoted = re.compile("[^\"' \\t\\n\\r _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_" ######################################################################## @@@ -125,8 -113,8 +125,8 @@@ class operationError(Error) self.cmd_ = cmd self.details_ = details def __str__(self): - """Return the complete response string from the server, with the command - if available. + """Return the complete response string from the server, with the + command if available. Excludes the final newline. """ @@@ -399,7 -387,8 +399,7 @@@ class client 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()) @@@ -433,8 -422,8 +433,8 @@@ Returns the ID of the new queue entry. - Note that queue IDs are unicode strings (because all track information - values are unicode strings). + Note that queue IDs are unicode strings (because all track + information values are unicode strings). """ res, details = self._simple("play", track) return unicode(details) # because it's unicode in queue() output @@@ -539,8 -528,8 +539,8 @@@ The return value is a list of dictionaries corresponding to recently played tracks. The next track to be played comes first. - See disorder_protocol(5) for the meanings of the keys. All keys are - plain strings but the values will be unicode strings.""" + See disorder_protocol(5) for the meanings of the keys. + All keys are plain strings but the values will be unicode strings.""" return self._somequeue("queue") def _somedir(self, command, dir, re): @@@ -775,7 -764,8 +775,8 @@@ The callback should return True to continue or False to stop (don't forget this, or your program will mysteriously misbehave). Once you - stop reading the log the connection is useless and should be deleted. + stop reading the log the connection is useless and should be + deleted. It is suggested that you use the disorder.monitor class instead of calling this method directly, but this is not mandatory. @@@ -902,7 -892,8 +903,8 @@@ self._simple("schedule-del", event) def schedule_get(self, event): - """Get the details for an event as a dict (returns None if event not found)""" + """Get the details for an event as a dict (returns None if + event not found)""" res, details = self._simple("schedule-get", event) if res == 555: return None @@@ -920,6 -911,54 +922,54 @@@ """Adopt a randomly picked track""" self._simple("adopt", id) + def playlist_delete(self, playlist): + """Delete a playlist""" + res, details = self._simple("playlist-delete", playlist) + if res == 555: + raise operationError(res, details, "playlist-delete") + + def playlist_get(self, playlist): + """Get the contents of a playlist + + The return value is an array of track names, or None if there is no + such playlist.""" + res, details = self._simple("playlist-get", playlist) + if res == 555: + return None + return self._body() + + def playlist_lock(self, playlist): + """Lock a playlist. Playlists can only be modified when locked.""" + self._simple("playlist-lock", playlist) + + def playlist_unlock(self): + """Unlock the locked playlist.""" + self._simple("playlist-unlock") + + def playlist_set(self, playlist, tracks): + """Set the contents of a playlist. The playlist must be locked. + + Arguments: + playlist -- Playlist to set + tracks -- Array of tracks""" + self._simple_body(tracks, "playlist-set", playlist) + + def playlist_set_share(self, playlist, share): + """Set the sharing status of a playlist""" + self._simple("playlist-set-share", playlist, share) + + def playlist_get_share(self, playlist): + """Returns the sharing status of a playlist""" + res, details = self._simple("playlist-get-share", playlist) + if res == 555: + return None + return _split(details)[0] + + def playlists(self): + """Returns the list of visible playlists""" + self._simple("playlists") + return self._body() + ######################################################################## # I/O infrastructure @@@ -949,8 -988,8 +999,8 @@@ else: raise protocolError(self.who, "invalid response %s") - def _send(self, *command): - # Quote and send a command + def _send(self, body, *command): + # Quote and send a command and optional body # # Returns the encoded command. quoted = _quote(command) @@@ -959,6 -998,13 +1009,13 @@@ try: self.w.write(encoded) self.w.write("\n") + if body != None: + for l in body: + if l[0] == ".": + self.w.write(".") + self.w.write(l) + self.w.write("\n") + self.w.write(".\n") self.w.flush() return encoded except IOError, e: @@@ -969,7 -1015,7 +1026,7 @@@ self._disconnect() raise - def _simple(self, *command): + def _simple(self, *command): # Issue a simple command, throw an exception on error # # If an I/O error occurs, disconnect from the server. @@@ -977,10 -1023,20 +1034,20 @@@ # On success or 'normal' errors returns response as a (code, details) tuple # # On error raise operationError + return self._simple_body(None, *command) + + def _simple_body(self, body, *command): + # Issue a simple command with optional body, throw an exception on error + # + # If an I/O error occurs, disconnect from the server. + # + # On success or 'normal' errors returns response as a (code, details) tuple + # + # On error raise operationError if self.state == 'disconnected': self.connect() if command: - cmd = self._send(*command) + cmd = self._send(body, *command) else: cmd = None res, details = self._response() @@@ -1061,8 -1117,8 +1128,8 @@@ class monitor: """DisOrder event log monitor class - Intended to be subclassed with methods corresponding to event log messages - the implementor cares about over-ridden.""" + Intended to be subclassed with methods corresponding to event log + messages the implementor cares about over-ridden.""" def __init__(self, c=None): """Constructor for the monitor class @@@ -1078,8 -1134,8 +1145,8 @@@ def run(self): """Start monitoring logs. Continues monitoring until one of the - message-specific methods returns False. Can be called more than once - (but not recursively!)""" + message-specific methods returns False. Can be called more than + once (but not recursively!)""" self.c.log(self._callback) def when(self): diff --combined server/dump.c index 4b023aa,ab48e48..1cee366 --- a/server/dump.c +++ b/server/dump.c @@@ -29,8 -29,6 +29,6 @@@ static const struct option options[] = { "debug", no_argument, 0, 'D' }, { "recover", no_argument, 0, 'r' }, { "recover-fatal", no_argument, 0, 'R' }, - { "trackdb", no_argument, 0, 't' }, - { "searchdb", no_argument, 0, 's' }, { "recompute-aliases", no_argument, 0, 'a' }, { "remove-pathless", no_argument, 0, 'P' }, { 0, 0, 0, 0 } @@@ -55,14 -53,70 +53,70 @@@ static void help(void) exit(0); } + /** @brief Dump one record + * @param s Output stream + * @param tag Tag for error messages + * @param letter Prefix leter for dumped record + * @param dbname Database name + * @param db Database handle + * @param tid Transaction handle + * @return 0 or @c DB_LOCK_DEADLOCK + */ + static int dump_one(struct sink *s, + const char *tag, + int letter, + const char *dbname, + DB *db, + DB_TXN *tid) { + int err; + DBC *cursor; + DBT k, d; + + /* dump the preferences */ + cursor = trackdb_opencursor(db, tid); + err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), + DB_FIRST); + while(err == 0) { + if(sink_writec(s, letter) < 0 + || urlencode(s, k.data, k.size) + || sink_writec(s, '\n') < 0 + || urlencode(s, d.data, d.size) + || sink_writec(s, '\n') < 0) + fatal(errno, "error writing to %s", tag); + err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), + DB_NEXT); + } + switch(err) { + case DB_LOCK_DEADLOCK: + trackdb_closecursor(cursor); + return err; + case DB_NOTFOUND: + return trackdb_closecursor(cursor); + case 0: + assert(!"cannot happen"); + default: + fatal(0, "error reading %s: %s", dbname, db_strerror(err)); + } + } + + static struct { + int letter; + const char *dbname; + DB **db; + } dbtable[] = { + { 'P', "prefs.db", &trackdb_prefsdb }, + { 'G', "global.db", &trackdb_globaldb }, + { 'U', "users.db", &trackdb_usersdb }, + { 'W', "schedule.db", &trackdb_scheduledb }, + { 'L', "playlists.db", &trackdb_playlistsdb }, + /* avoid 'T' and 'S' for now */ + }; + #define NDBTABLE (sizeof dbtable / sizeof *dbtable) + /* dump prefs to FP, return nonzero on error */ - static void do_dump(FILE *fp, const char *tag, - int tracksdb, int searchdb) { - DBC *cursor = 0; + static void do_dump(FILE *fp, const char *tag) { DB_TXN *tid; struct sink *s = sink_stdio(tag, fp); - int err; - DBT k, d; for(;;) { tid = trackdb_begin_transaction(); @@@ -72,124 -126,18 +126,18 @@@ fatal(errno, "error calling fflush"); if(ftruncate(fileno(fp), 0) < 0) fatal(errno, "error calling ftruncate"); - if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0) + if(fprintf(fp, "V0") < 0) fatal(errno, "error writing to %s", tag); - /* dump the preferences */ - cursor = trackdb_opencursor(trackdb_prefsdb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('P', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } - cursor = 0; - - /* dump the global preferences */ - cursor = trackdb_opencursor(trackdb_globaldb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('G', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } - cursor = 0; + for(size_t n = 0; n < NDBTABLE; ++n) + if(dump_one(s, tag, + dbtable[n].letter, dbtable[n].dbname, *dbtable[n].db, + tid)) + goto fail; - /* dump the users */ - cursor = trackdb_opencursor(trackdb_usersdb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('U', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } - cursor = 0; - - /* dump the schedule */ - cursor = trackdb_opencursor(trackdb_scheduledb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('W', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } - cursor = 0; - - - if(tracksdb) { - cursor = trackdb_opencursor(trackdb_tracksdb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('T', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } - cursor = 0; - } - - if(searchdb) { - cursor = trackdb_opencursor(trackdb_searchdb, tid); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_FIRST); - while(err == 0) { - if(fputc('S', fp) < 0 - || urlencode(s, k.data, k.size) - || fputc('\n', fp) < 0 - || urlencode(s, d.data, d.size) - || fputc('\n', fp) < 0) - fatal(errno, "error writing to %s", tag); - err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), - DB_NEXT); - } - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } cursor = 0; - } - - if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag); - if(err == DB_LOCK_DEADLOCK) { - error(0, "c->c_get: %s", db_strerror(err)); - goto fail; - } - if(err && err != DB_NOTFOUND) - fatal(0, "cursor->c_get: %s", db_strerror(err)); - if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } + if(fputs("E\n", fp) < 0) + fatal(errno, "error writing to %s", tag); break; fail: - trackdb_closecursor(cursor); - cursor = 0; info("aborting transaction and retrying dump"); trackdb_abort_transaction(tid); } @@@ -276,9 -224,6 +224,6 @@@ static int undump_dbt(FILE *fp, const c /* undump from FP, return 0 or DB_LOCK_DEADLOCK */ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) { int err, c; - DBT k, d; - const char *which_name; - DB *which_db; info("undumping"); if(fseek(fp, 0, SEEK_SET) < 0) @@@ -291,6 -236,28 +236,28 @@@ if((err = truncdb(tid, trackdb_scheduledb))) return err; c = getc(fp); while(!ferror(fp) && !feof(fp)) { + for(size_t n = 0; n < NDBTABLE; ++n) { + if(dbtable[n].letter == c) { + DB *db = *dbtable[n].db; + const char *dbname = dbtable[n].dbname; + DBT k, d; + + if(undump_dbt(fp, tag, prepare_data(&k)) + || undump_dbt(fp, tag, prepare_data(&d))) + break; + switch(err = db->put(db, tid, &k, &d, 0)) { + case 0: + break; + case DB_LOCK_DEADLOCK: + error(0, "error updating %s: %s", dbname, db_strerror(err)); + return err; + default: + fatal(0, "error updating %s: %s", dbname, db_strerror(err)); + } + goto next; + } + } + switch(c) { case 'V': c = getc(fp); @@@ -299,54 -266,15 +266,15 @@@ break; case 'E': return 0; - case 'P': - case 'G': - case 'U': - case 'W': - switch(c) { - case 'P': - which_db = trackdb_prefsdb; - which_name = "prefs.db"; - break; - case 'G': - which_db = trackdb_globaldb; - which_name = "global.db"; - break; - case 'U': - which_db = trackdb_usersdb; - which_name = "users.db"; - break; - case 'W': /* for 'when' */ - which_db = trackdb_scheduledb; - which_name = "scheduledb.db"; - break; - default: - abort(); - } - if(undump_dbt(fp, tag, prepare_data(&k)) - || undump_dbt(fp, tag, prepare_data(&d))) - break; - switch(err = which_db->put(which_db, tid, &k, &d, 0)) { - case 0: - break; - case DB_LOCK_DEADLOCK: - error(0, "error updating %s: %s", which_name, db_strerror(err)); - return err; - default: - fatal(0, "error updating %s: %s", which_name, db_strerror(err)); - } - break; - case 'T': - case 'S': - if(undump_dbt(fp, tag, prepare_data(&k)) - || undump_dbt(fp, tag, prepare_data(&d))) - break; - /* We don't restore the tracks.db or search.db entries, instead - * we recompute them */ - break; case '\n': break; + default: + if(c >= 32 && c <= 126) + fatal(0, "unexpected character '%c'", c); + else + fatal(0, "unexpected character 0x%02X", c); } + next: c = getc(fp); } if(ferror(fp)) @@@ -435,13 -363,13 +363,13 @@@ fail int main(int argc, char **argv) { int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0; - int tracksdb = 0, searchdb = 0, remove_pathless = 0, fd; + int remove_pathless = 0, fd; const char *path; char *tmp; FILE *fp; mem_init(); - while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) { + while((n = getopt_long(argc, argv, "hVc:dDurRaP", options, 0)) >= 0) { switch(n) { case 'h': help(); case 'V': version("disorder-dump"); @@@ -449,8 -377,6 +377,6 @@@ case 'd': dump = 1; break; case 'u': undump = 1; break; case 'D': debugging = 1; break; - case 't': tracksdb = 1; break; - case 's': searchdb = 1; break; case 'r': recover = TRACKDB_NORMAL_RECOVER; case 'R': recover = TRACKDB_FATAL_RECOVER; case 'a': recompute = 1; break; @@@ -460,8 -386,6 +386,6 @@@ } if(dump + undump + recompute != 1) fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases"); - if((undump || recompute) && (tracksdb || searchdb)) - fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases"); if(recompute) { if(optind != argc) fatal(0, "--recompute-aliases does not take a filename"); @@@ -473,7 -397,7 +397,7 @@@ 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) { @@@ -484,7 -408,7 +408,7 @@@ fatal(errno, "error opening %s", tmp); if(!(fp = fdopen(fd, "w"))) fatal(errno, "fdopen on %s", tmp); - do_dump(fp, tmp, tracksdb, searchdb); + do_dump(fp, tmp); if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp); if(rename(tmp, path) < 0) fatal(errno, "error renaming %s to %s", tmp, path); diff --combined server/server.c index 8bd23e7,320cbb1..ce45a1d --- a/server/server.c +++ b/server/server.c @@@ -1,6 -1,6 +1,6 @@@ /* * 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 @@@ -17,18 -17,13 +17,18 @@@ */ #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 */ @@@ -44,6 -39,34 +44,34 @@@ struct listener int pf; }; + struct conn; + + /** @brief Signature for line reader callback + * @param c Connection + * @param line Line + * @return 0 if incomplete, 1 if complete + * + * @p line is 0-terminated and excludes the newline. It points into the + * input buffer so will become invalid shortly. + */ + typedef int line_reader_type(struct conn *c, + char *line); + + /** @brief Signature for with-body command callbacks + * @param c Connection + * @param body List of body lines + * @param nbody Number of body lines + * @param u As passed to fetch_body() + * @return 0 to suspend input, 1 if complete + * + * The body strings are allocated (so survive indefinitely) and don't include + * newlines. + */ + typedef int body_callback_type(struct conn *c, + char **body, + int nbody, + void *u); + /** @brief One client connection */ struct conn { /** @brief Read commands from here */ @@@ -77,6 -100,18 +105,18 @@@ struct conn *next; /** @brief True if pending rescan had 'wait' set */ int rescan_wait; + /** @brief Playlist that this connection locks */ + const char *locked_playlist; + /** @brief When that playlist was locked */ + time_t locked_when; + /** @brief Line reader function */ + line_reader_type *line_reader; + /** @brief Called when command body has been read */ + body_callback_type *body_callback; + /** @brief Passed to @c body_callback */ + void *body_u; + /** @brief Accumulating body */ + struct vector body[1]; }; /** @brief Linked list of connections */ @@@ -88,6 -123,15 +128,15 @@@ static int reader_callback(ev_source *e size_t bytes, int eof, void *u); + static int c_playlist_set_body(struct conn *c, + char **body, + int nbody, + void *u); + static int fetch_body(struct conn *c, + body_callback_type body_callback, + void *u); + static int body_line(struct conn *c, char *line); + static int command(struct conn *c, char *line); static const char *noyes[] = { "no", "yes" }; @@@ -830,18 -874,17 +879,18 @@@ static int c_volume(struct conn *c 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; } @@@ -1030,21 -1073,25 +1079,25 @@@ static int c_resolve(struct conn *c return 1; } - static int c_tags(struct conn *c, - char attribute((unused)) **vec, - int attribute((unused)) nvec) { - char **tags = trackdb_alltags(); - - sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n"); - while(*tags) { + static int list_response(struct conn *c, + const char *reply, + char **list) { + sink_printf(ev_writer_sink(c->w), "253 %s\n", reply); + while(*list) { sink_printf(ev_writer_sink(c->w), "%s%s\n", - **tags == '.' ? "." : "", *tags); - ++tags; + **list == '.' ? "." : "", *list); + ++list; } sink_writes(ev_writer_sink(c->w), ".\n"); return 1; /* completed */ } + static int c_tags(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + return list_response(c, "Tag list follows", trackdb_alltags()); + } + static int c_set_global(struct conn *c, char **vec, int attribute((unused)) nvec) { @@@ -1104,13 -1151,10 +1157,13 @@@ static int c_new(struct conn *c 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; @@@ -1314,36 -1358,33 +1367,26 @@@ static int c_userinfo(struct conn *c static int c_users(struct conn *c, char attribute((unused)) **vec, int attribute((unused)) nvec) { - /* TODO de-dupe with c_tags */ - char **users = trackdb_listusers(); - - sink_writes(ev_writer_sink(c->w), "253 User list follows\n"); - while(*users) { - sink_printf(ev_writer_sink(c->w), "%s%s\n", - **users == '.' ? "." : "", *users); - ++users; - } - sink_writes(ev_writer_sink(c->w), ".\n"); - return 1; /* completed */ + return list_response(c, "User list follows", trackdb_listusers()); } -/** @brief Base64 mapping table for confirmation strings - * - * 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 @@@ -1354,6 -1395,7 +1397,6 @@@ 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; @@@ -1363,12 -1405,12 +1406,12 @@@ 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 { @@@ -1599,6 -1641,152 +1642,152 @@@ static int c_adopt(struct conn *c return 1; } + static int playlist_response(struct conn *c, + int err) { + switch(err) { + case 0: + assert(!"cannot cope with success"); + case EACCES: + sink_writes(ev_writer_sink(c->w), "550 Access denied\n"); + break; + case EINVAL: + sink_writes(ev_writer_sink(c->w), "550 Invalid playlist name\n"); + break; + case ENOENT: + sink_writes(ev_writer_sink(c->w), "555 No such playlist\n"); + break; + default: + sink_writes(ev_writer_sink(c->w), "550 Error accessing playlist\n"); + break; + } + return 1; + } + + static int c_playlist_get(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + char **tracks; + int err; + + if(!(err = trackdb_playlist_get(vec[0], c->who, &tracks, 0, 0))) + return list_response(c, "Playlist contents follows", tracks); + else + return playlist_response(c, err); + } + + static int c_playlist_set(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + return fetch_body(c, c_playlist_set_body, vec[0]); + } + + static int c_playlist_set_body(struct conn *c, + char **body, + int nbody, + void *u) { + const char *playlist = u; + int err; + + if(!c->locked_playlist + || strcmp(playlist, c->locked_playlist)) { + sink_writes(ev_writer_sink(c->w), "550 Playlist is not locked\n"); + return 1; + } + if(!(err = trackdb_playlist_set(playlist, c->who, + body, nbody, 0))) { + sink_printf(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); + } + + static int c_playlist_get_share(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + char *share; + int err; + + if(!(err = trackdb_playlist_get(vec[0], c->who, 0, 0, &share))) { + sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(share)); + return 1; + } else + return playlist_response(c, err); + } + + static int c_playlist_set_share(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + + if(!(err = trackdb_playlist_set(vec[0], c->who, 0, 0, vec[1]))) { + sink_printf(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); + } + + static int c_playlists(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + char **p; + + trackdb_playlist_list(c->who, &p, 0); + return list_response(c, "List of playlists follows", p); + } + + static int c_playlist_delete(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + + if(!(err = trackdb_playlist_delete(vec[0], c->who))) { + sink_writes(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); + } + + static int c_playlist_lock(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + struct conn *cc; + + /* Check we're allowed to modify this playlist */ + if((err = trackdb_playlist_set(vec[0], c->who, 0, 0, 0))) + return playlist_response(c, err); + /* If we hold a lock don't allow a new one */ + if(c->locked_playlist) { + sink_writes(ev_writer_sink(c->w), "550 Already holding a lock\n"); + return 1; + } + /* See if some other connection locks the same playlist */ + for(cc = connections; cc; cc = cc->next) + if(cc->locked_playlist && !strcmp(cc->locked_playlist, vec[0])) + break; + if(cc) { + /* TODO: implement config->playlist_lock_timeout */ + sink_writes(ev_writer_sink(c->w), "550 Already locked\n"); + return 1; + } + c->locked_playlist = xstrdup(vec[0]); + time(&c->locked_when); + sink_writes(ev_writer_sink(c->w), "250 Acquired lock\n"); + return 1; + } + + static int c_playlist_unlock(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + if(!c->locked_playlist) { + sink_writes(ev_writer_sink(c->w), "550 Not holding a lock\n"); + return 1; + } + c->locked_playlist = 0; + sink_writes(ev_writer_sink(c->w), "250 Released lock\n"); + return 1; + } + static const struct command { /** @brief Command name */ const char *name; @@@ -1645,6 -1833,14 +1834,14 @@@ { "pause", 0, 0, c_pause, RIGHT_PAUSE }, { "play", 1, 1, c_play, RIGHT_PLAY }, { "playing", 0, 0, c_playing, RIGHT_READ }, + { "playlist-delete", 1, 1, c_playlist_delete, RIGHT_PLAY }, + { "playlist-get", 1, 1, c_playlist_get, RIGHT_READ }, + { "playlist-get-share", 1, 1, c_playlist_get_share, RIGHT_READ }, + { "playlist-lock", 1, 1, c_playlist_lock, RIGHT_PLAY }, + { "playlist-set", 1, 1, c_playlist_set, RIGHT_PLAY }, + { "playlist-set-share", 2, 2, c_playlist_set_share, RIGHT_PLAY }, + { "playlist-unlock", 0, 0, c_playlist_unlock, RIGHT_PLAY }, + { "playlists", 0, 0, c_playlists, RIGHT_READ }, { "prefs", 1, 1, c_prefs, RIGHT_READ }, { "queue", 0, 0, c_queue, RIGHT_READ }, { "random-disable", 0, 0, c_random_disable, RIGHT_GLOBAL_PREFS }, @@@ -1680,13 -1876,58 +1877,58 @@@ { "volume", 0, 2, c_volume, RIGHT_READ|RIGHT_VOLUME } }; + /** @brief Fetch a command body + * @param c Connection + * @param body_callback Called with body + * @param u Passed to body_callback + * @return 1 + */ + static int fetch_body(struct conn *c, + body_callback_type body_callback, + void *u) { + assert(c->line_reader == command); + c->line_reader = body_line; + c->body_callback = body_callback; + c->body_u = u; + vector_init(c->body); + return 1; + } + + /** @brief @ref line_reader_type callback for command body lines + * @param c Connection + * @param line Line + * @return 1 if complete, 0 if incomplete + * + * Called from reader_callback(). + */ + static int body_line(struct conn *c, + char *line) { + if(*line == '.') { + ++line; + if(!*line) { + /* That's the lot */ + c->line_reader = command; + vector_terminate(c->body); + return c->body_callback(c, c->body->vec, c->body->nvec, c->body_u); + } + } + vector_append(c->body, xstrdup(line)); + return 1; /* completed */ + } + static void command_error(const char *msg, void *u) { struct conn *c = u; sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg); } - /* process a command. Return 1 if complete, 0 if incomplete. */ + /** @brief @ref line_reader_type callback for commands + * @param c Connection + * @param line Line + * @return 1 if complete, 0 if incomplete + * + * Called from reader_callback(). + */ static int command(struct conn *c, char *line) { char **vec; int nvec, n; @@@ -1757,7 -1998,7 +1999,7 @@@ static int reader_callback(ev_source at while((eol = memchr(ptr, '\n', bytes))) { *eol++ = 0; ev_reader_consume(reader, eol - (char *)ptr); - complete = command(c, ptr); + complete = c->line_reader(c, ptr); /* usually command() */ bytes -= (eol - (char *)ptr); ptr = eol; if(!complete) { @@@ -1820,6 -2061,7 +2062,7 @@@ static int listen_callback(ev_source *e 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", @@@ -1850,7 -2092,6 +2093,7 @@@ int server_start(ev_source *ev, int pf l->pf = pf; if(ev_listen(ev, fd, listen_callback, l, "server listener")) exit(EXIT_FAILURE); + info("listening on %s", name); return fd; } diff --combined tests/Makefile.am index dfe7424,be3d73b..5563f7f --- a/tests/Makefile.am +++ b/tests/Makefile.am @@@ -1,6 -1,6 +1,6 @@@ # # 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 @@@ -26,12 -26,12 +26,12 @@@ disorder_udplog_DEPENDENCIES=../lib/lib TESTS=cookie.py dbversion.py dump.py files.py play.py queue.py \ recode.py search.py user-upgrade.py user.py aliases.py \ - schedule.py 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