From: rjk@greenend.org.uk <> Date: Tue, 23 Jan 2007 21:23:45 +0000 (+0000) Subject: Import from Arch revision: X-Git-Tag: debian-1_5_99dev8~315 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/460b9539a7c15580e41a71bbc0f47ae776238915 Import from Arch revision: rjk@greenend.org.uk--2004/disorder--mainline--0.1--patch-328 --- 460b9539a7c15580e41a71bbc0f47ae776238915 diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..78e4a86 --- /dev/null +++ b/BUGS @@ -0,0 +1,42 @@ +* Compilation Bugs + +The configure script make pointless checks for C++ and Fortran compilers. This +is a bug in Libtool not in DisOrder. See: http://bugs.debian.org/221873 + +* Server Bugs + +* Other Problems + +** Wrongly Specified Character Encoding + +A problem was reported where DisOrder was misconfigured to believe a UTF-8 +filesystem was actually an ISO-8859-1 filesystem. When it was reconfigured +correctly, the old (mangled) filenames remain in the database. + +This is not a bug as such, it is a configuration error. However recovering +from it can be painful if many filenames are involved. If this mistake is +widespread then it may be worth adding support for automatic recovery, but for +now the easiest answer is to stop the server, remove the databases and start +again. + +** Poor Sound Quality + +Poor sound quality may be a result of (hitherto unknown!) bugs in DisOrder, or +may be a problem with other software or hardware. Specific combinations that +have produced problems in the past are listed here. + +*** Stutter with VIA 82C686 + +Hardware: "VT82C686 AC97 Audio Controller" +Software: Linux via82cxxx_audio driver +Symptom: Stuttering playback with raw format play. +Solution: Use snd-via82xx driver instead (from ALSA). + +ALSA drivers can be found in Linux 2.6.x; for 2.4.x they must be built +separately. + +Local Variables: +mode:outline +fill-column:79 +End: +arch-tag:32a95f730a93f0073f62873ebae245f8 diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..04abd31 --- /dev/null +++ b/CHANGES @@ -0,0 +1,321 @@ +See ChangeLog.d/* for detailed revision history. + +* Changes up to version 1.6 + +** General + +There is a new client, 'Disobedience', that depends on the GTK+ library. +Feedback on the interface would be very welcome. + +Tracks can now have tags associated with them. See tags in disorder(1) +or the preferences documentation for the web interface or Disobedience. + +The search facility knows how to limit results by tag (see search +documentation for any interface) as well as by word search. It is +possible to limit random play by tag (see required-tags and +prohibited-tags in disorder_config(5)). + +** Server + +Cache slow file lookups in the server. Should help installations with +large collections and/or slow platforms. + +The communications protocol has changed, for the benefit of +Disobedience. + +The 'enabled' and 'random_enabled' configuration options are now gone. +Instead the state survives from one run of the server to the next. +'disable now' is gone as well - if you want to emulate it disable +playing and then scratch the current track. + +The 'pick' plugin has been abolished. All the logic formerly done there +is now built into the server, where it can be done much more +efficiently. + +** disorderfm + +There is a new command line tool called 'disorderfm' which is designed +for filename translation on (for instance) digital audio repositories. +It is not yet feature-complete. See its man page for additional +details. + +** Build And Configuration + +You can control which components are built with new --with options. See +README. + +options.transform and the 'transform' web option have gone, replaced +with a 'transform' configuration command. Both this and 'namepart' are +now optional. + +* Changes up to version 1.5.1 + +** Web Interface + +Correct regexp for non-alpha tracks. + +* Changes up to version 1.5 + +** Web Interface + +Regexp-based filtering of tracks (for instance as used by the initial +'Choose' page) now does the regexp matching in the server, limiting the +amount of data transferred to the web interface only to be discarded. + +** Client + +Regexp-base filtering of tracks is now available to the command line +client. + +** Server + +New server_nice, speaker_nice and rescan_nice configuration options +allow independent control of process priorities. + +Scratches are now attributed to the user who requested them. + +Bugs fixed: + A file descriptor was leaked for each track played. + The amount of a track played so far was not reported. + The speaker process could crash on underrun. + The server would crash if you paused a non-pause capable track. + Regexp matching in the file and directory list commands was not + reliable. + Handling of variable-argument commands in the client was broken. + +* Changes up to version 1.4 + +** General + +Raw format players are now supported. See README.upgrades and +README.raw for details. This allows pausing and eliminating the +inter-track gap. + +Pausing is also supported with suitably modified standalone player +plugins, though none of the supplied ones are capable of this. + +When random play is enabled the randomly picked track now appears in the +queue, and can be moved around the queue, removed from it, etc. + +** Web Interface + +Switches (random play, pause, ...) are now presented as a +fixed-appearance switch with an adjacent state indicator. + +The 'Manage' screen has new buttons to move tracks to the head or tail +of the queue. + +You can now edit the preferences for all the tracks in an album in a +single screen, rather than having to visit each separately. For the +time being the raw preferences editing has gone; it can be reintroduced +on some form if there is demand. (You can still edit raw preferences +from the command line.) + +Labels are now documented in options.labels rather than +disorder_config(5). + +** Server + +If you tried to start up on any empty database with random play enabled +the server would exit with an error. + +The server no longer risks failing if you strace its player +subprocesses. + +It was possible for the server to hang when a 'reconfigure' command was +issued. This should no longer be the case. + +The default signal to forcibly terminate players is now SIGKILL. + +** Plugins + +Plugins must now declare a type word. This allows them to document +whether they are a standalone player or a raw-format player, and whether +they support pausing. They can also arrange to get setup and cleanup +calls in the main server. See disorder(3) for more details. + +* Changes up to version 1.3 + +** Dependencies + +Berkeley DB 4.2 is no longer supported. Use 4.3. + +** Client + +There is a new 'authorize' command to simplify the addition of local +users. Please report successes as well as failures. + +There is a new 'resolve' command to return the real track name behind an +alias. + +The 'rescan' command no longer takes an argument. + +** Server + +The track database code has been largely rewritten to improve +maintainability. + +There is a new 'lock' directive. By default the server uses a lockfile +to prevent multiple copies of itself running simultaneously; this can be +inhibited e.g. if you are using a filesystem that does not support +locking and are confident you can prevent concurrent running yourself. + +Aliases for track names, constructed from trackname_display_ +preferences, now appear in the virtual filesystem. + +The server now executes a subprocess for the rescan operation. It also +runs a separate deadlock manager. + +Standard output and standard error from subprocesses are now logged. +This is handy if you need to figure out why a player failed unexpectedly +but might lead to huge log files if you have needlessly verbose players. + +** Web Interface + +Enable/disable buttons are now colored to reflect current state. + +Entering numeric volume values (rather than clicking on the arrows) now +works. + +Connection errors are reported more gracefuly. + +** Plugins + +Scanner plugins are now always invoked in a subprocess. + +disorder_track_count() and disorder_track_getn() are no longer +available. Instead use disorder_track_random(). + +Plugins are now opened with RTLD_NOW, so link errors are detected +immediately. + +** Tools + +disorder-dump now insists on the input/output file being a named regular +file, rather than using stdin or stdout. + +** Other + +Some missing files have been added, and some notes added regarding +getting text encoding right. + +* Changes up to version 1.2 + +See README.upgrades when upgrading to this version. + +** Bugs Fixed + +Avoid accumulating overlarge recently played list. + +When the server was stopped, the currently playing track would not be +added to the recently played list. This has been fixed. + +Reloading the 'volume' page no longer repeats the last volume-changing +action. + +The search facility now works properly for multiple hits within a single +artist or album. + +** Server + +New namepart directive replaces web interface's trackname-part. There +are associated changes to the protocol and clients. + +The number of database queries per candidate match required when +searching has been reduced. + +The operator can control the signal used to scratch playing tracks. The +default has been changed to SIGINT from SIGKILL. + +The 'log' command now provides a formalised event log, rather than raw +access to the server's ordinary log output. + +** Web Interface Changes + +*** Choosing Tracks + +When picking a track the client now stays on the same screen rather than +redirecting back to the 'Playing' screen. So that the user gets +feedback from their action, playing and queued tracks are now marked as +such in the track picking screen. + +It is possible to revert to the old behaviour by removing the back= +argument from the choose.html and search.html templates (and optionally +the trackstate lines). + +*** Search + +Non-ASCII characters are now properly supported in search terms. + +*** Syntax + +The template syntax has been changed slightly to ignore whitespace in +certain places. + +*** Miscellaneous + +Some formerly textual buttons are now replaced by images (with ALT text +reflecting the old value). The stylesheet is now a .css file (installed +in the same place as the images) rather than being embedded into every +template. + +Artist and album names in the playing and recently-played lists are now +links to the corresponding directory. + +More functions are now available from the 'manage' screen. + +The menus are now (by default) across the top of the screen instead of +down the side. Set the 'menu' label to 'sidebar' to restore the old +appearance. 'Volume' is not present in this new menu, use 'Manage' +instead (or edit the template). + +** tkdisorder + +tkdisorder now displays artist, album and title in the queue and +recently played widgets, rather than just the title (as formerly). + +* Changes up to version 1.1 + +** Bugs Fixed + +Corrected various problems with UTF-8 parsing. + +In the web interface, "The Beatles" (etc) are now grouped under 'B' not +'T' when grouping tracks by initial letter. + +** Server + +The list of recently played tracks is now preserved across server +restarts. + +Track IDs are more compact. + +Versions of libdb before 4.2 are no longer supported. 4.2 and 4.3 both +work now. 4.2 support will be removed in some future release. + +Prehistoric backwards-compatibility logic removed. Only affects people +upgrading from long before 1.0 (who should upgrade to 1.0 and then to +1.1.) + +** Command Line + +Tracks can be moved in the queue from the command line. + +'disorder queue' now reports track IDs. + +$pkgdatadir/completion.bash provides tab completion over commands and +options. + +** Web Interface + +New 'cooked' preferences interface saves users having to know arcane +details of trackname preferences and so on. Non-ASCII characters are +now properly supported in this context. + +CGI arguments to the web interface are now checked for UTF-8 compliance. + +Local Variables: +mode:outline +fill-column:72 +End: +arch-tag:9dfc21f4428056e647e3656822342956 diff --git a/ChangeLog.d/cvs--ChangeLog b/ChangeLog.d/cvs--ChangeLog new file mode 100644 index 0000000..050c274 --- /dev/null +++ b/ChangeLog.d/cvs--ChangeLog @@ -0,0 +1,881 @@ +2004-10-04 00:59:42 +0100 Richard Kettlewell + + * dcgi.c: proper track ordering in search. + + * regsub.c, tracks.c: chattier error messages + + * utf8.h: stricter UTF-8 parsing + +2004-09-26 10:47:45 +0100 Richard Kettlewell + + * Makefile.am, plugins/Makefile.am: tidy up to build in separate + object directory + +2004-09-25 17:47:23 +0100 Richard Kettlewell + + * version 0.11 + +2004-09-25 17:41:21 +0100 Richard Kettlewell + + * BUGS: some known bugs + +2004-09-25 17:25:49 +0100 Richard Kettlewell + + * tracks.c: chattier errors + +2004-09-25 17:17:25 +0100 Richard Kettlewell + + * api-client.c, cgi.c, cgimain.c, disorder.c, server.c: EXIT_FAILURE + pedantry + +2004-09-18 17:28:32 +0100 Richard Kettlewell + + * client.c: rework handling of commands that get lists back, + unbreaking 'stats' in the process. + +2004-09-18 16:54:58 +0100 Richard Kettlewell + + * tracks.c: add a lockfile to prevent concurrent access to databases. + +2004-07-31 19:05:38 +0100 Richard Kettlewell + + * debian/postinst, prerm: hopefuly better upgrade handling + +2004-07-31 18:44:52 +0100 Richard Kettlewell + + * queue.c: don't reverse the queue on restart + + * Document queue/recent ordering a bit better. + +2004-07-20 20:22:09 +0100 Richard Kettlewell + + * tkdisorder: primitive queue widget, simplify MonitorStateThread + +2004-07-20 19:42:52 +0100 Richard Kettlewell + + * tkdisorder: python + tkinter gui, still under development + +2004-07-18 14:21:12 +0100 Richard Kettlewell + + * Extra navigation links in 'Choose' screen allow easier navigation + back up the directory tree. It's not quite right yet because + track names are relative to the filesystem root rather than the root + of their collection. + + New @navigate@, @fullname@, @dirname@, @basename@, @ne@ and @eq@ + expansions. + +2004-07-18 13:22:53 +0100 Richard Kettlewell + + * use new label sidebar.choosewhich to determine which 'Choose' screen + to pick, rather than making operator edit sidebar.html + + * mention config.USERNAME in README + +2004-07-18 13:05:45 +0100 Richard Kettlewell + + * debian/control: more detailed build deps + +2004-07-18 13:03:56 +0100 Richard Kettlewell + + * README: dependency version notes + +2004-07-18 12:57:55 +0100 Richard Kettlewell + + * cope with gcrypt version skew + +2004-07-18 02:46:07 +0100 Richard Kettlewell + + * Control connections can go over the net (using anything that + getaddrinfo() knows about) using the new 'listen' and 'connect' + options. + +2004-07-17 19:01:25 +0100 Richard Kettlewell + + * Increase/decrease volume buttons + +2004-07-17 16:35:18 +0100 Richard Kettlewell + + * templates/help.html: document 'Manage' + + * templates/playing.html: extra empty buttons + +2004-07-17 16:24:12 +0100 Richard Kettlewell + + * Any track that had a filesystem encoding that differed from itself + when converted to UTF-8 track (that is to say, any non-ASCII track in + a non-UTF-8 filesystem) would not be played, as only the UTF-8 + version of the name was passed to the player. + + * The disorder_play_track plugin interface now takes both the path and + the track name (the former being the raw bytes from the filesystem, + the latter being the UTF-8 version). + + * Document that scratch names must be UTF-8 (or ASCII). + +2004-07-17 16:11:21 +0100 Richard Kettlewell + + * 'move' command in control protocol to move tracks around + + * New 'Manage' page allows tracks to be moved around in the queue. + +2004-07-17 14:56:04 +0100 Richard Kettlewell + + * disorder.c: teach 'disorder play' to take multile tracks. Hence for + instance you could do: + disorder play /path/to/some/album/*.ogg + +2004-07-17 14:45:55 +0100 Richard Kettlewell + + * templates/help.html: mention choosealpha + +2004-07-17 14:37:18 +0100 Richard Kettlewell + + * Use regexp-filtered file listings to implement choosealpha.html, + which split the top-level 'Choose' page up according to the initial + letter of the filenames. + +2004-07-17 14:19:33 +0100 Richard Kettlewell + + * files and directory listings can now be filtered using regexps (you + could do this in your client anyway but now the server does it for + you, thus causing less data to be transferred from server to + client). + + * correct disorder.py's quoting of empty strings + +2004-07-17 13:07:51 +0100 Richard Kettlewell + + * disorder.py.in: correct exception-to-string functions + +2004-07-17 13:05:06 +0100 Richard Kettlewell + + * disorder.py.in: add another example + +2004-07-17 12:57:57 +0100 Richard Kettlewell + + * disorder.py.in: better docs and error handling + +2004-07-17 01:06:59 +0100 Richard Kettlewell + + * disorder.py.in: add a full set of methods + +2004-07-17 01:06:31 +0100 Richard Kettlewell + + * plugins/tracklength.c: quieten compiler + +2004-07-16 22:31:31 +0100 Richard Kettlewell + + * rudimentary Python client support + +2004-07-15 00:55:08 +0100 Richard Kettlewell + + * configuration.c: stricter syntax check for 'url' + +2004-07-15 00:41:22 +0100 Richard Kettlewell + + * disorder_config.5.in: clarify 'url' syntax + + * templates/help.html: link to DisOrder control protocol page + +2004-07-14 19:46:38 +0100 Richard Kettlewell + + * config.sample.in, debian/disorder.config: play WAV files correctly + + * debian/control: depend on sox + + * scratches are now *.ogg files + +2004-07-11 19:45:24 +0100 Richard Kettlewell + + * plugins/tracklength.c: build fix + +2004-07-11 19:39:40 +0100 Richard Kettlewell + + * add notify_queue to notify plugin + +2004-07-11 19:01:26 +0100 Richard Kettlewell + + * tracks.c: always sync log after replay, so that upgrade works + +2004-07-11 18:54:33 +0100 Richard Kettlewell + + * new prefs.log file records all preferences in ASCII and guarantess + to be up to date. So provided you back up this file the rest of the + databases can be lost completely. + + * document the use and properties of the various database files + + * new prefsync configuration command controls interval between + minimization of prefs.log. + + * event.c, event.h: timeouts get an associated handle which can be + used to cancel them. + + * don't remove the search database at the drop of a hat as that would + require it to be rebuilt more frequently than is sensible. + +2004-07-11 18:38:52 +0100 Richard Kettlewell + + * disorder.c: missing 'break', oops! + +2004-07-10 19:43:42 +0100 Richard Kettlewell + + * plugins/tracklength.c: binary search over extensions + round up WAV duration + +2004-07-10 19:22:50 +0100 Richard Kettlewell + + * plugins/tracklength.c: tracklength plugin now knows about WAV files + remember to unmap files after parsing them! + + * sounds/slap.wav: correct broken encoding (was two WAVs concatenated, + is now a single WAV; it worked with 'cat > /dev/audio' but doesn't + work with things that expect a single correctly formatted WAV file.) + +2004-07-10 18:11:26 +0100 Richard Kettlewell + + * 'disorder --length' allows command-line access to tracklength plugin + +2004-07-10 14:47:41 +0100 Richard Kettlewell + + * server.c: use libgcrypt for random numbers + + * configure.ac: don't need dev/[u]random any more + +2004-07-10 13:33:16 +0100 Richard Kettlewell + + * add log command to stream logs to clients + + * disorder_protocol.5.in: new man page documenting internal + communications protocol + +2004-07-10 11:51:54 +0100 Richard Kettlewell + + * use URL-encoding in dumps because it's more convenient to duck + encoding issues that way. It also looks more consistent with the + use of URL-encoding elsewhere in DisOrder. + + * change logging interface so that messages are formatted only once. + +2004-07-10 11:40:13 +0100 Richard Kettlewell + + * printf.c: correct number bases! + +2004-07-10 00:00:33 +0100 Richard Kettlewell + + * send back our URL in Refresh HTTP header to work around weird Apache + behaviour + + * log to (in theory) multiple outputs + + * '#define NO_MEMORY_ALLOCATION' allows us to enforce rules about + files that shouldn't perform memory allocation + + * split *printf frontends into separate files since some need memory + allocation and some do not + +2004-06-12 11:37:37 +0100 Richard Kettlewell + + * Use a sink to handle writing to an ev_writer + + * disorder-dump --dump now works against a running server, so it is + not necessary to take the server down to back up the preference + data. + + * printf.c: correct handling of flags (which were completely broken) + +2004-05-24 15:56:30 +0100 Richard Kettlewell + + * new help page, plus HTML-ized versions of man pages + + * de-dupe code in Makefile + + * template names can't have / in and can't be dot-files + (previously they weren't allowed . in) + + * @ is @ not ( + +2004-05-24 14:00:38 +0100 Richard Kettlewell + + * templates/playing.html: put track title in element + +2004-05-24 13:58:29 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: remove dead code + +2004-05-23 19:29:16 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: distribute missing files + + * dcgi.c: quiten compiler + + * printf.c: missing base and padding settings + +2004-05-23 19:08:40 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.3: update for disorder_snprintf + +2004-05-23 19:01:31 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * New *printf implementation, which is guaranteed to behave reliable + in the face of strange encoding games (standard *printf insist on + MBC strings in the current encoding, which isn't great for us). + We don't support floating point yet. + + * Log output is always ASCII (non-ASCII characters are escaped) so we + don't have to rely on the encoding of stderr or syslog. + + * No longer depend on the target providing a working snprintf. + + * Corrected a few broken *printf calls + +2004-05-22 16:55:02 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * reach preferences edit from 'Recent' + +2004-05-22 16:30:56 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * @search@ converted to new template infrastructure + +2004-05-21 21:55:55 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * optionally restrict scratch and/or remove to submitting user + + * export 'become' command to command-line client + + * determine username from UID, not environment + +2004-05-21 21:13:30 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: minor improvements + +2004-05-21 21:00:05 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: rearrange into alpha order + +2004-05-18 23:14:11 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.c: add {enable,disable}-random as synonyms for + random-{enable,disable} + +2004-05-18 20:18:02 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: prevent rapid web refresh is all playing is disabled + +2004-05-18 00:30:28 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * support libdb 4.2 as well as 3.2. Versions inbetween might well work + but this hasn't been tested. + +2004-05-16 23:43:34 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * debian/control: list build depends + +2004-05-16 23:34:40 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * #include <db.h> when checking libdb, to cope with variants that + redefine all the symbols. + +2004-05-16 23:28:55 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * new disorder-dump program to read/write prefs in text format + (e.g. for backup, database upgrade, replication) + +2004-05-16 10:37:10 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: clean generated files + + * debian/autorules.m4: fix clean target + + * debian/rules.m4: man pages in /usr/share/man + +2004-04-26 23:28:18 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate dependency on <inttypes.h> + +2004-04-26 20:59:20 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * convert UTF-8 -> UCS-4 directly, rather than relying on iconv (which + doesn't always know how to do it). + +2004-04-26 20:47:12 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Do not expand dcgi-generated txet in expansion argument values (as + it'll get expanded properly later on anyway). + + * Add @urlquote@ expansion. + +2004-04-25 20:47:26 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * arg:directory, not arg:dir + Include containing directory name in choose.html + +2004-04-25 20:11:49 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: minor docs improvements. + +2004-04-25 19:53:19 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * unify transform- web options, and make them available via the + @transform@ expansion. + +2004-04-25 19:52:13 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: setting a trackname_ pref would cause a crash. + +2004-04-25 18:51:56 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Bring documentation up to date a bit. + +2004-04-25 18:29:49 Richard Kettlewell <rjk@greenend.org.uk> + + * @choose@ converted to new template infrastructure. + +2004-04-25 17:14:56 Richard Kettlewell <rjk@greenend.org.uk> + + * @prefs@ now takes a template argument, shifting the preferences + table furniture into the template file. + +2004-04-25 15:44:38 Richard Kettlewell <rjk@greenend.org.uk> + + * Portability hacks of various degrees of nastiness in the interest of + building on FreeBSD. + +2004-04-18 19:00:52 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: add some missing bits. + +2004-04-18 18:05:17 Richard Kettlewell <rjk@greenend.org.uk> + + * @playing@ and @recent@ now take a template argument, allowing the + template file to contain the table furniture rather than having it + generated from inside dcgi. Templates updated accordingly. + Documentation updated. + + * Moved the calculation of expected start times into the server. + +2004-04-18 13:45:19 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: booleans for the expansion language + +2004-04-18 13:13:46 Richard Kettlewell <rjk@greenend.org.uk> + + * Expansions can now take multiple parameters using a new quoting + syntax. Parameters are (for all current expansions, but not for + some future one) recursively expanded before use. + + * sink.c: interface for things that accept output (current + implementations being stdio and dynstrs) + + * vacopy.h: find a va_copy somewhere. + +2004-04-17 16:48:00 Richard Kettlewell <rjk@greenend.org.uk> + + * version 0.10 + +2004-04-17 16:25:59 Richard Kettlewell <rjk@greenend.org.uk> + + * Typo fixes. + +2004-04-17 16:03:45 Richard Kettlewell <rjk@greenend.org.uk> + + * README: recommend basic authentication instead of digest auth, as + the latter seems too poorly supported. + + * configure.ac: don't check for things that are very standard + + * debian/control: add Section and Priority fields + +2004-04-04 17:45:34 Richard Kettlewell <rjk@greenend.org.uk> + + * Build GNU getopt for systems where libc doesn't have it + +2004-04-04 15:11:49 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: check that various things are available and work at + configure time, rather than build or (worse still!) run time. More + library checks. + + * charset.c: <wchar.h> no longer needed + +2004-04-04 12:06:16 Richard Kettlewell <rjk@greenend.org.uk> + + * server.c: attempt to play tracks when they are added to the queue. + Otherwise they wouldn't be played at all nothing was already + playing. + +2004-04-03 13:39:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Use libiconv if libc doesn't have iconv + +2004-04-03 12:42:05 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: check for both libdb and libdb3 + report all missing libs at once + +2004-04-02 23:42:13 Richard Kettlewell <rjk@greenend.org.uk> + + * Only use __attribute__ syntax under GNU C. + +2004-04-02 23:16:41 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: use whatever libdb the system defaults to. + +2004-04-02 23:06:50 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.c: add a --help-commands option giving a one-line summary + of each command. + + * Updated documentation a bit. + +2004-04-02 20:20:47 Richard Kettlewell <rjk@greenend.org.uk> + + * Ship another scratch sound. + +2004-04-02 20:10:10 Richard Kettlewell <rjk@greenend.org.uk> + + * Debianization fixes: + + distribute postrm + + call Libtool correctly + + fix shared library dependencies + + * Quieten compiler when optimization turned on. + +2004-04-02 19:27:59 Richard Kettlewell <rjk@greenend.org.uk> + + * Tidy up appearance of preferences screen + +2004-03-28 20:01:40 Richard Kettlewell <rjk@greenend.org.uk> + + * Track prefs editor in web interface. Rather rough-edged for now. + +2004-03-28 17:41:43 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: add @shell@ expansion + +2004-03-28 15:02:02 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: handle empty search result lists correctly. + +2004-03-28 14:38:01 Richard Kettlewell <rjk@greenend.org.uk> + + * split.c: quote empty strings properly + +2004-03-28 14:35:39 Richard Kettlewell <rjk@greenend.org.uk> + + * README.streams: how to play streams with DisOrder + +2004-03-28 14:10:51 Richard Kettlewell <rjk@greenend.org.uk> + + * play.c: unpick wait status of failed players + +2004-03-28 14:07:49 Richard Kettlewell <rjk@greenend.org.uk> + + * play.c: put players in their own process group and send SIGKILL + rather than SIGTERM (the latter because ogg123 doesn't seem to honor + SIGTERM when playing streams). + +2004-03-28 13:57:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Handle directories that contain both files and directories better + + dcgi.c: headings for directory/track lists + correct output for choose.playall label + + server.c: support file listing in the root + + Associated label and style sheet additions + +2004-03-28 12:35:20 Richard Kettlewell <rjk@greenend.org.uk> + + * New 'shell' player plugin executes a shell command with an environment + variable identifying the track. Optionally, the user can control what + shell is used. + + * The sample config file knows how to play .wav files now. + + * Ship a default scratch sound. + +2004-03-27 20:48:13 Richard Kettlewell <rjk@greenend.org.uk> + + * Play tracks via a plugin. 'exec' plugin module provides previous + behaviour. + +2004-03-27 20:28:57 Richard Kettlewell <rjk@greenend.org.uk> + + * log interesting events if a username is attached (see previous + change) + +2004-03-27 19:30:44 Richard Kettlewell <rjk@greenend.org.uk> + + * server.c: don't log auth data any more - with the Refresh: HTTP + header causing disorder.cgi to run every few seconds you end up with + vast amounts of useless information. It would be more sensible to + log actions instead. + +2004-03-27 18:41:22 Richard Kettlewell <rjk@greenend.org.uk> + + * Add a sidebar to the web interface, making everything available from + everywhere. Tidy up the default stylesheet. + +2004-03-27 18:19:40 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: eliminate a spurious </div> + +2004-03-27 17:17:16 Richard Kettlewell <rjk@greenend.org.uk> + + * When updating the track list, only process one track per select(). + This makes the server much more responsive to clients during + rescans. + +2004-03-27 16:20:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Don't need file extensions in stopword list by default any more. + +2004-03-27 16:07:53 Richard Kettlewell <rjk@greenend.org.uk> + + * Common code now lives in a shared library. + This library is not intended for other programs to link against, + so no ABI guarantees exist for it. + + * The disorder(3) API is now implemented by shim functions rather than + aliases. These are always part of the executable that imports the + plugin, never the shared library. Varargs functions are a bit messy + as you can't just write a forwarding function for them. + +2004-03-27 14:22:22 Richard Kettlewell <rjk@greenend.org.uk> + + * Volume control support. + + new 'mixer' and 'channel' configuration commands + + 'volume' server command + + disorder_set_volume and disorder_get_volume client functions + + get-volume and set-volume command-line client commands + + @volume:SPEAKER@ expansion to get current volumes + + 'volume' action to set volume + + volume.html template (a bit primitive still) + +2004-03-26 19:15:09 Richard Kettlewell <rjk@greenend.org.uk> + + * Bring back 'Play all' button in @choose@, and fix track selection + + * Scratch by ID, so that late scratches don't get the wrong track + +2004-03-25 00:40:12 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: report top 10 search words in server stats + +2004-03-24 23:24:45 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: de-dupe track/directory code in @choose@ + +2004-03-22 23:31:55 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: fine-tune sort order. Now we try the 'sort' transformation + or part context first - once case-folded, once raw; then we try the + 'display' version, again case-folded then raw; and finally we use + the unprocessed track name as as tie-breaker. + + Repeated calls to casefold aren't very efficient, this should be + fixed. + + * templates/options.transform: correct transform-track replacement + string + +2004-03-22 19:58:42 Richard Kettlewell <rjk@greenend.org.uk> + + * Global and case-independent regexp replacement. + + * Strip out punctuation for sorting directories in @choose@. + +2004-03-21 19:30:39 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: remove search database in track_sync(). It'll be removed + when we re-open anyway, and there's no point occupying the disk + space longer than necessary. + + By the same token, we don't bother syncing it in track_sync(). + +2004-03-21 18:47:31 Richard Kettlewell <rjk@greenend.org.uk> + + * Sort strings are now case-folded before comparison. + + * words.c: casefold now returns the original string if it was + malformed, rather than a null pointer. + +2004-03-21 17:04:48 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.h: new function disorder_track_set_data allows plugins to + set preferences as well as query them. + + * disorder.3: document the above and add some general notes + + * plugins/notify.c: record time and count tracks are played at + + * plugins/pick.c: don't pick recently played tracks + +2004-03-21 16:06:21 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: server stats had the database types all wrong, leading to + a crash. Fixed. + +2004-03-21 15:14:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Separate sorting and display with new context arguments in + configuration and trackname_ preferences. + + * By default track numbers take part in sorting, but are not displayed + + * By default "The" is display unmodified but moved to the end for + sorting + +2004-03-21 14:37:36 Richard Kettlewell <rjk@greenend.org.uk> + + * Group search results according to 'columns search' configuration. + +2004-03-21 14:25:35 Richard Kettlewell <rjk@greenend.org.uk> + + cvs -Q up -j mergepoint-0-9-bugfixes-1 -j mergepoint-0-9-bugfixes-2 + +2004-03-21 14:21:34 Richard Kettlewell <rjk@greenend.org.uk> + + * templates/search.html: fix broken HTML. + +2004-03-20 21:11:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Include trackname_ preferences in searches, and rebuild search + database from scratch on startup (to completely eliminate any + contamination due to prior configuration). + +2004-03-20 18:50:36 Richard Kettlewell <rjk@greenend.org.uk> + + * words.c: more characters are separators + +2004-03-20 18:44:45 Richard Kettlewell <rjk@greenend.org.uk> + + * Split preferences into a separate database, and automatically + convert the old one. + +2004-03-18 00:11:26 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: correct scanning for directories, to correctly handle + cases where you have <path>/<prefix> and <path>/<prefix><suffix>. + +2004-03-17 00:20:55 Richard Kettlewell <rjk@greenend.org.uk> + + * config.sample.in: prefer mpg321 to mpg123 + +2004-03-16 23:45:54 Richard Kettlewell <rjk@greenend.org.uk> + + * Server statistics support: + o track_stats() reports stats from databases + o disorder_stats() reports stats in client + o stats command to command line interface + o @stats@ expansion in web interface + + * about.html: provides information about DisOrder + + * client.c: correct list parsing in client interface + + * disorder_config.5.in: add missing docs for @arg@ and @search@ + +2004-03-16 23:16:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Do all database writes synchronously and add some missing + transaction IDs. In fact the missing transaction IDs probably + ameliorated problems caused by the asynchronous writes (which leave + the database actually unusable, rather than merely with bits + missing). + +2004-03-16 20:30:00 Richard Kettlewell <rjk@greenend.org.uk> + + * Improved track search: + o Instead of encoding all track names into a single db item, we use + sorted duplicate data items. libdb does all the work for us. + o Strip collection root and extensions from filenames + o Delete stopwords from search db at startup (in case new ones have + been added) + o Rename search db to search2.db so it doesn't conflict with the old + one + o Always update search db even if we've seen the track, so that + database format/name changes don't trash searching + + * Add a few missing transaction ID arguments. + +2004-03-16 20:24:25 Richard Kettlewell <rjk@greenend.org.uk> + + * rescan.c: rescan would crash on shut down + +2004-03-16 12:43:54 Richard Kettlewell <rjk@greenend.org.uk> + + * quieten compiler when optimization turned on + +2004-03-16 12:12:40 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: complete renaming of trackinfo.* to heading.* + +2004-03-15 23:18:02 Richard Kettlewell <rjk@greenend.org.uk> + + * version 0.9 + +2004-03-15 19:56:50 Richard Kettlewell <rjk@greenend.org.uk> + + * Define table columns with a new 'columns' web option, rather than by + relying on the ordering of trackinfo.* labels. trackinfo.* labels + are thus only used as headings and are renamed accordingly. The + button column is no longer magical. + + * Always search the config directory and data directory for templates + and web options (after any explicitly configured directories). + +2004-03-15 19:17:02 Richard Kettlewell <rjk@greenend.org.uk> + + * make table classes more consistent + +2004-03-15 18:13:39 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: log closing databases. Checkpoint database at shutdown. + +2004-03-15 18:03:35 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: issue log messages from database checkpointing, since we + don't know how long they'll take. + +2004-03-15 17:31:35 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.init.in: make 'stop' work even if server not running. + +2004-03-15 14:37:59 Richard Kettlewell <rjk@greenend.org.uk> + + Added debianization files. + +2004-03-15 13:51:12 Richard Kettlewell <rjk@greenend.org.uk> + + * cgi.c: avoid UB if template path is empty + +2004-03-14 16:33:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: correct dependencies on version script + distribute version script + +2004-03-14 16:30:56 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate zombie rescans. + +2004-03-14 16:25:37 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate clash with glibc error(). + +2004-03-14 16:19:55 Richard Kettlewell <rjk@greenend.org.uk> + + * Use a version script to limit the exported symbols. There's a glibc + naming clash with error() which should work around. Also this + should be conditional on the linker actually supporting this + feature. + +2004-03-14 15:37:56 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: correct wrong handling of <gc.h> + +2004-03-14 15:04:44 Richard Kettlewell <rjk@greenend.org.uk> + + * DisOrder has been rewritten in C. + +arch-tag:b64f2a539d1621207b46b5bd202244e9 diff --git a/ChangeLog.d/disorder--mainline--0.1 b/ChangeLog.d/disorder--mainline--0.1 new file mode 100644 index 0000000..2daf877 --- /dev/null +++ b/ChangeLog.d/disorder--mainline--0.1 @@ -0,0 +1,6487 @@ +# do not edit -- automatically generated by arch changelog +# arch-tag: automatic-ChangeLog--rjk@greenend.org.uk--2004/disorder--mainline--0.1 +# + +2006-11-11 18:04:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-328 + + Summary: + Missing ship + Revision: + disorder--mainline--0.1--patch-328 + + * scripts/Makefile.am: Remember to ship oggrename. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/Makefile.am + + +2006-11-11 13:13:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-327 + + Summary: + oggrename + Revision: + disorder--mainline--0.1--patch-327 + + * scripts/oggrename: Script to rename OGG files according to embedded + title information. + + new files: + scripts/oggrename + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2006-11-04 15:56:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-326 + + Summary: + disorderfm filename converter + Revision: + disorder--mainline--0.1--patch-326 + + * clients/disorderfm.c: New filename management tool. + * doc/disorderfm.1.in: Documentation. + * lib/charset.c: New entry points required for disorderfm. + * lib/eclient.c: Quieten compiler. + * clients/filename-bytes.c: Grotty utility for examining byte strings in + filenames. + + new files: + clients/disorderfm.c clients/filename-bytes.c + doc/disorderfm.1.in + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + clients/Makefile.am doc/Makefile.am lib/charset.c + lib/charset.h lib/eclient.c + + +2006-10-08 21:26:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-325 + + Summary: + Update CHANGES + Revision: + disorder--mainline--0.1--patch-325 + + * CHANGES: Update change description + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-10-08 21:20:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-324 + + Summary: + Copyright dates. + Revision: + disorder--mainline--0.1--patch-324 + + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/trackname.c + templates/prefs.html + + +2006-10-08 21:12:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-323 + + Summary: + Search by tag + Revision: + disorder--mainline--0.1--patch-323 + + * server/trackdb.c: Search for tags using tag: syntax. + + * templates/help.html: Mention tag: syntax. + * doc/disobedience.1.in: Mention tag: syntax. + * doc/disorder.1.in: Mention tag: syntax. + * doc/disorder_protocol.5.in: Mention tag: syntax. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + doc/disorder.1.in doc/disorder_protocol.5.in server/trackdb.c + templates/help.html + + +2006-10-08 19:17:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-322 + + Summary: + Avoid needless redraws. + Revision: + disorder--mainline--0.1--patch-322 + + * disobedience/choose.c: Don't issue a redraw on an empty search result + if we were already displaying the right thing. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 19:01:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-321 + + Summary: + Docs + window title. + Revision: + disorder--mainline--0.1--patch-321 + + * disobedience/disobedience.c: Correct window title. + * doc/disobedience.1.in: Document search box. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c doc/disobedience.1.in + + +2006-10-08 18:56:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-320 + + Summary: + Better search responsiveness. + Revision: + disorder--mainline--0.1--patch-320 + + * disobedience/choose.c: More efficient initial construction of search + results tree. The display of the tree is now the expensive bit (e.g + 0.2s for 300 hits on my Athlon). The old logic remains for expanding + individual items in the tree, but it does much less work in that + context and so isn't a performance problem. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 18:35:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-319 + + Summary: + Cancel search button + Revision: + disorder--mainline--0.1--patch-319 + + * disobedience/choose.c: Add a cancel button to clear the current search. + You can do this by deleting all the text but having an obvious button + for it seems friendlier. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 17:58:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-318 + + Summary: + Search cleanup + Revision: + disorder--mainline--0.1--patch-318 + + 'search-parse' makes much more sense as it means we guarantee a uniform + interpretation of search strings across all clients. So we make 'search' + do that. + + * server/server.c: 'search' takes on the meaning of 'search-parse' now. + * lib/eclient.c: Keep up with changed 'search' names. + * lib/client.c: Use new search interface. + * server/dcgi.c: Use new search interface. + * clients/disorder.c: Use new search interface. + + * doc/disorder_protocol.5.in: Document resolved search semantics + + modified files: + ChangeLog.d/disorder--mainline--0.1 clients/disorder.c + disobedience/TODO doc/disorder_protocol.5.in lib/client.c + lib/client.h lib/eclient.c server/dcgi.c server/server.c + + +2006-10-08 17:45:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-317 + + Summary: + Search in Disobedience + Revision: + disorder--mainline--0.1--patch-317 + + Lots of cleanup and documentation to do. + + * disobedience/choose.c: Track searching. This is implemented as a text + entry in the choose window. Whenever contains a valid search string + then the choose tree is replaced with the search results. + * disobedience/choose.c: Disable breakdown by initial letter. The code + is still there and available via --choosealpha. + * disobedience/disobedience.c: Make report window available earlier. + * disobedience/disobedience.c: --choosealpha option to re-enable initial + letter breakup of choose tree. + + * server/server.c: search-parse parses search string instead of expecting + caller to do so. + + * lib/eclient.c: Implement search. + * lib/hash.c: Constness. + * lib/split.c: Cope with an absent error handler. + * server/trackdb.c: Directory-tree-order comparison moved to lib/trackname.c. + * lib/trackname.c: Export directory-tree-order comparison as compare_path(). + + * doc/disorder_protocol.5.in: Document search-parse. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + disobedience/disobedience.c disobedience/disobedience.h + disobedience/disobedience.rc doc/disobedience.1.in + doc/disorder_protocol.5.in lib/eclient.c lib/eclient.h + lib/hash.c lib/hash.h lib/split.c lib/trackname.c + lib/trackname.h server/server.c server/trackdb.c + server/trackname.c + + +2006-09-17 14:53:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-316 + + Summary: + Support for obsolete GCC. + Revision: + disorder--mainline--0.1--patch-316 + + * configure.ac: Turn off -Werror for GCC 2.95. + + Not sure how worthwhile this is... + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + disobedience/TODO + + +2006-09-17 10:24:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-315 + + Summary: + Fix popup menu behaviour. + Revision: + disorder--mainline--0.1--patch-315 + + * disobedience/queue.c: Pop up menu on button press, not release, giving + more traditional behaviour. + * disobedience/choose.c: Pop up menu on button press. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + disobedience/queue.c + + +2006-09-17 10:21:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-314 + + Summary: + Document Disobedience tag support. + Revision: + disorder--mainline--0.1--patch-314 + + * doc/disobedience.1.in: Document tag support. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + + +2006-09-17 10:18:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-313 + + Summary: + Tags in Disobedience + Revision: + disorder--mainline--0.1--patch-313 + + * disobedience/properties.c: Edit tags. Also fix boolean prefs to work + for prefs other than pick_at_random, not that there are any yet. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-09-17 10:04:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-312 + + Summary: + Web editing for tags + Revision: + disorder--mainline--0.1--patch-312 + + * server/dcgi.c: Accept tags in a prefs response. + * templates/prefs.html: Edit tags in web prefs screen. + * templates/options.labels: Label for tags field. + * templates/help.html: Mention tags in html help. + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/dcgi.c + templates/help.html templates/options.labels + templates/prefs.html + + +2006-09-17 09:50:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-311 + + Summary: + Tags fiddling and documntation. + Revision: + disorder--mainline--0.1--patch-311 + + * server/trackdb.c: Tags are now separated by commas and can contain + spaces. + * doc/disorder.1.in: Mention tags in track preferences. + * doc/disorder_config.5.in; Mention tag list syntax. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_config.5.in server/trackdb.c + + +2006-09-17 09:28:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-310 + + Summary: + Update copyright dates + Revision: + disorder--mainline--0.1--patch-310 + + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/client.h lib/disorder.h lib/plugin.c lib/plugin.h + plugins/Makefile.am server/play.h + + +2006-09-17 09:22:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-309 + + Summary: + Fix missing events bugs (hooray) + Revision: + disorder--mainline--0.1--patch-309 + + * disobedience/queue.c: Only create drag target widgets when actually + dragging, as otherwise they sometimes(!) steal events from the widgets + they overlap. + The padding cell at the RHS of every row is now sensitive to input. + Add a comment describing widget hierarchy. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-09-15 21:46:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-308 + + Summary: + Minor fixups + Revision: + disorder--mainline--0.1--patch-308 + + * server/play.c: Close the spare writing end of the player's log pipe - + it only needs to be visible as stdout/err. + + * disobedience/queue.c: Conditioned out diagnostic code for lost clicks. + + * scripts/completion.bash: Update completeions for current command set. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + disobedience/queue.c scripts/completion.bash server/play.c + + +2006-05-14 16:49:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-307 + + Summary: + Heartbeat + Revision: + disorder--mainline--0.1--patch-307 + + * disobedience/disobedience.c: Add a (conditioned-out) heartbeat in + pursuit of unresponsiveness. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c + + +2006-05-03 23:11:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-306 + + Summary: + Tags and global preferences. + Revision: + disorder--mainline--0.1--patch-306 + + Higher-level user interfaces have yet to be written and not much testing + has been done but the basics seem to be working. + + * server/trackdb.c: Tags. Global preferences for recording long-term + server state. This is relatively involved; although the logic for + maintaining tags is very simple, being similar to search, picking a + track at random when it must have particular tags is more annoying and + we don't use the database to help us much, but instead keep a cache and + remember to blow it in various places. + * server/play.c: playing_enable and random_enabled are now database + entries. + * server/disorderd.c: Need to do initial setup more directly now. + * server/server.c: Track protocol changes. + + * server/dcgi.c: Abolish disable-now. + + * lib/client.c: Track protocol changes. + * lib/configuration.c: enabled/random_enabled config options abolished in + favour of new global prefs. + * lib/hash.c: Start with a 256-slot hash. Cope with null values. New + hash_keys() returns a list of keys in no particular order. + + * clients/disorder.c: Kill disable-now. + Add tags, get-global, set-global, unset-global. + + * plugins/pick.c: Removed. + * lib/plugin.c: Pick plugin abolished. + + * doc/disorder.1.in: Document new command line options. + * doc/disorder.3: Document removal of pick plugin. + * doc/disorder_config.5.in: Document global prefs and removal of + enabled/random_enabled config options and disable-now CGI action. + * doc/disorder_protocol.5.in: Document get-global, set-global, + unset-global, tags. + + removed files: + plugins/pick.c + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.upgrades + clients/disorder.c doc/disorder.1.in doc/disorder.3 + doc/disorder_config.5.in doc/disorder_protocol.5.in + lib/client.c lib/client.h lib/configuration.c + lib/configuration.h lib/disorder.h lib/hash.c lib/hash.h + lib/plugin.c lib/plugin.h plugins/Makefile.am server/dcgi.c + server/disorderd.c server/play.c server/play.h server/server.c + server/trackdb.c server/trackdb.h + + +2006-05-01 17:38:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-305 + + Summary: + Drag+drop fixing. + Revision: + disorder--mainline--0.1--patch-305 + + * disobedience/queue.c: Cope with dragging to the head of the queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-05-01 17:32:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-304 + + Summary: + Quieten compiler + Revision: + disorder--mainline--0.1--patch-304 + + * disobedience/queue.c: Rename 'time' args to keep gcc happy. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-05-01 17:31:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-303 + + Summary: + Drag+drop queue rearrangement + Revision: + disorder--mainline--0.1--patch-303 + + * disobedience/queue.c: Drag+drop queue rearrangement. Use button + release events, not press, as the latter get confused with drag starts. + * disobedience/choose.c: Use button release events, not press. + * disobedience/disobedience.c: New log_moved() signature. + * disobedience/disobedience.rc: Colors for drag target zones + + * doc/disorder_protocol.5.in: Document moveafter and 'moved' log change. + + * lib/queue.c: queue_moveafter() is the underlying implementation of the + 'moveafter' command. + * server/server.c: New 'moveafter' command moves a bunch of tracks to a + single ocation in the queue. + * lib/eclient.c: Support moveafter and 'moved' log change. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/choose.c disobedience/disobedience.c + disobedience/disobedience.rc disobedience/queue.c + doc/disorder_protocol.5.in lib/eclient.c lib/eclient.h + lib/queue.c lib/queue.h server/server.c + + +2006-05-01 14:33:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-302 + + Summary: + Reduce accidental scratching. + Revision: + disorder--mainline--0.1--patch-302 + + * disobedience/queue.c: Only make scratch item in popup sensitive if the + playing track is selected, to cut down on accidental scratching. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/queue.c + + +2006-05-01 12:15:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-301 + + Summary: + Cache fixes. + Revision: + disorder--mainline--0.1--patch-301 + + * lib/hash.c: Remember to actually save value. + * lib/cache.c: Pass correct time when expiring. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + lib/cache.c lib/hash.c + + +2006-05-01 12:01:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-300 + + Summary: + More administrivia + Revision: + disorder--mainline--0.1--patch-300 + + More copyright dates and exceptions. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/check + scripts/completion.bash scripts/copyright.exceptions + + +2006-05-01 11:57:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-299 + + Summary: + Default transform/namepart. + Revision: + disorder--mainline--0.1--patch-299 + + * lib/configuration.c: Default transform/namepart. + + * debian/disorder.config: Commment out transform/namepart. + * examples/config.sample.in: Commment out transform/namepart. + + * README.client: more notes. + * README.upgrades: Mention that transform/namepart are optional now. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.client + README.upgrades debian/disorder.config + doc/disorder_config.5.in examples/config.sample.in + lib/configuration.c + + +2006-05-01 11:21:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-298 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-298 + + * scripts/check: Easier invocation. + * scripts/completion.bash: Option completion for Disobedience. + + Also updated copyright dates on a bunch of files. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/rules.m4 + disobedience/disobedience.rc doc/Makefile.am + images/Makefile.am lib/asprintf.c lib/authhash.c + lib/authhash.h lib/hash.c lib/hash.h lib/log.c lib/mem.c + lib/mem.h lib/printf.h lib/queue.c lib/queue.h lib/split.c + lib/trackname.c lib/trackname.h prepare scripts/Makefile.am + scripts/check scripts/completion.bash + scripts/copyright.exceptions server/cgi.c server/server.h + server/trackdb.h templates/Makefile.am templates/help.html + + +2006-05-01 11:07:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-297 + + Summary: + Fix queue column width + Revision: + disorder--mainline--0.1--patch-297 + + * disobedience/queue.c: Columns should shrink to fit, not stay at their + maximum extent indefinitely. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-04-30 23:22:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-296 + + Summary: + Memory optimization. + Revision: + disorder--mainline--0.1--patch-296 + + * lib/hash.c: Less memory-heavy hash implementation. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/hash.c + + +2006-04-30 19:53:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-295 + + Summary: + More documentation. + Revision: + disorder--mainline--0.1--patch-295 + + * README: mention --without-* options. + * README.client: how to set up a standalone client install. + + new files: + README.client + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am README + disobedience/TODO + + +2006-04-30 19:42:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-294 + + Summary: + Documentation updates. + Revision: + disorder--mainline--0.1--patch-294 + + * doc/disorder.1.in: Mention the automatic rescan. + Add a troubleshooting section. + + * doc/disobedience.1.in: Hide --sync. Document keyboard shortcuts and + recent changes to 'Choose'. + + * templates/help.html: Add a troubleshooting section. Possibly this + should just be a link to the equivalent disorder(1) section. + + * CHANGES: Updated. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disobedience.1.in doc/disorder.1.in templates/help.html + + +2006-04-30 19:05:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-293 + + Summary: + debian policy fixup + Revision: + disorder--mainline--0.1--patch-293 + + * debian/autorules.m4: Make binary targets depend on build target. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/autorules.m4 + + +2006-04-30 18:55:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-292 + + Summary: + Don't forget queue selection on update + Revision: + disorder--mainline--0.1--patch-292 + + * disobedience/queue.c: Use new selection_*() functions to record + selection so that it survives updates to the queue reliably. + + * lib/selection.c: Selection management functions using a hash. + + * lib/hash.c: hash_count() to count the number of items in a hash. + + * lib/queue.h: queue_entry.selected is gone. + + new files: + lib/selection.c lib/selection.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.h disobedience/queue.c + lib/Makefile.am lib/hash.c lib/hash.h lib/queue.c lib/queue.h + + +2006-04-30 16:42:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-291 + + Summary: + Faster startup + Revision: + disorder--mainline--0.1--patch-291 + + * lib/eclient.c: Batch up command writes once authenticated. This + improves Disobedience performance, in particular it fills in the track + names faster at startup if the server is over a network, by reducing + the number of round trip times. + * lib/log.c: DISORDER_DEBUG_ONLY allows you to limit debug output to that + from a single file. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/eclient.c lib/log.c + + +2006-04-30 15:02:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-290 + + Summary: + Fix playing indicator for aliased tracks. + Revision: + disorder--mainline--0.1--patch-290 + + * lib/choose.c: Resolve filenames (so that the playing indicator and + properties window work). + + * lib/eclient.c: disorder_eclient_resolve(). + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + lib/eclient.c lib/eclient.h + + +2006-04-30 14:40:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-289 + + Summary: + Popup menu and selection in choose tab + Revision: + disorder--mainline--0.1--patch-289 + + * disobedience/choose.c: Abolish buttons and just use labels and do our + own click parsing. Maintain a selection in the same way as queue.c. + Popup menu to play/edit tracks, middle click to play straight away. + + * disobedience/disobedience.rc: Supply bg whenever we supply fg. + + * disobedience/menu.c: Edit menu uses callbacks to deal with different + kinds of tabs rather than explicit knowledge. + + * disobedience/queue.c: Callbacks for edit menu. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/choose.c disobedience/disobedience.h + disobedience/disobedience.rc disobedience/menu.c + disobedience/queue.c + + +2006-04-30 11:59:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-288 + + Summary: + Set sensitivity of main menu items. + Revision: + disorder--mainline--0.1--patch-288 + + * disobedience/menu.c: Move out main menu code. Set sensitivity of + Properties and Select All appropriately from menu_update(). + + * disobedience/queue.c: Call menu_update() when queue/recent changes. + Provide queue-counting functions queue_count_*() to set sensitivity of + main menu items. Fix 'remove' option. + + * disobedience/disobedience.c: Move main menu out to menu.c. Call + menu_update() when the user switches tabs. + + * disobedience/disobedience.h: Include almost all headers from here. + Organize function prototypes into logical groups. + + new files: + disobedience/menu.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/Makefile.am + disobedience/choose.c disobedience/client.c + disobedience/control.c disobedience/disobedience.c + disobedience/disobedience.h disobedience/misc.c + disobedience/properties.c disobedience/queue.c + + +2006-04-30 11:21:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-287 + + Summary: + Edit>Properties starts working. + Revision: + disorder--mainline--0.1--patch-287 + + * disobedience/disobedience.c: Edit>Properties menu item now works for + queues. + + * disobedience/queue.c: queue_properties() entry point for the above. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c disobedience/disobedience.h + disobedience/queue.c + + +2006-04-29 18:00:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-286 + + Summary: + Document properties window. + Revision: + disorder--mainline--0.1--patch-286 + + * doc/disobedience.1.in: Document properties window. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + + +2006-04-29 16:04:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-285 + + Summary: + Stock buttons in properties window + Revision: + disorder--mainline--0.1--patch-285 + + * disobedience/properties.c: Use stock items for properties window buttons. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-04-29 13:46:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-284 + + Summary: + Stop badness if user closes progress bar window + Revision: + disorder--mainline--0.1--patch-284 + + * disobedience/properties.c: Cope with progress bar window being + destroyed part way through. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-04-29 13:38:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-283 + + Summary: + Properties window + Revision: + disorder--mainline--0.1--patch-283 + + Only usable from queue/recent so far - need to do select and the menu bar + properties item too. Also search when that is done. + + * disobedience/properties.c: New properties popup window. + * disobedience/queue.c: Pass queue definition as well as item to menu + item activation. + namepart_update() notifies that a namepart might have changed. + If we right click away from any selected item, select just the hovered + item. + Make properties menu item in popup sensitive if anything is selected, + and call properties() with the selected tracks when it is activated. + Include formerly missing backlink from first queued track to playing + track. + * lib/eclient.c: get, set and unset. + * disobedience/client.c: Split out popup_error(). + * disobedience/misc.c: popup_error(). + * disobedience/disobedience.c: Rename function to avoid collision. + + new files: + disobedience/properties.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/Makefile.am + disobedience/client.c disobedience/disobedience.c + disobedience/disobedience.h disobedience/misc.c + disobedience/queue.c lib/eclient.c lib/eclient.h + + +2006-04-26 20:24:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-282 + + Summary: + Rename gdisorder to 'Disobedience' + Revision: + disorder--mainline--0.1--patch-282 + + Name suggested by Owen Dunn. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + disobedience/Makefile.am disobedience/choose.c + disobedience/client.c disobedience/control.c + disobedience/disobedience.c disobedience/disobedience.h + disobedience/disobedience.rc disobedience/misc.c + disobedience/queue.c doc/Makefile.am doc/disobedience.1.in + + renamed files: + doc/gdisorder.1.in + ==> doc/disobedience.1.in + gdisorder/.arch-ids/=id + ==> disobedience/.arch-ids/=id + gdisorder/gdisorder.c + ==> disobedience/disobedience.c + gdisorder/gdisorder.h + ==> disobedience/disobedience.h + gdisorder/gdisorder.rc + ==> disobedience/disobedience.rc + + new directories: + disobedience/.arch-ids + + removed directories: + gdisorder/.arch-ids + + renamed directories: + gdisorder + ==> disobedience + + +2006-04-17 18:29:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-281 + + Summary: + Build fixes + Revision: + disorder--mainline--0.1--patch-281 + + * gdisorder/Makefile.am: ship gdisorder.c + * server/server.c: Quieten compiler. + * lib/eclient.c: Quieten stupid compiler. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + gdisorder/Makefile.am lib/eclient.c server/server.c + + +2006-04-17 11:18:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-280 + + Summary: + Install gdisorder + Revision: + disorder--mainline--0.1--patch-280 + + * gdisorder/Makefile.am: Install gdisorder. + * doc/gdisorder.1.in: Update man page. + * doc/Makefile.am: Install gdisorder man page + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + doc/gdisorder.1.in gdisorder/Makefile.am + + +2006-04-17 11:02:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-279 + + Summary: + Popup improvements, multi-track remove. + Revision: + disorder--mainline--0.1--patch-279 + + * gdisorder/queue.c: Table driven popup menus. The menu items are now + fixed but the sensitivity changes according to context. Multi-track + remove now works. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/queue.c + + +2006-04-16 23:39:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-278 + + Summary: + gdisorder tidying + Revision: + disorder--mainline--0.1--patch-278 + + * gdisorder/gdisorder.c: Remove bogus underlines. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + + +2006-04-16 23:37:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-277 + + Summary: + Selection in queue/recent. + Revision: + disorder--mainline--0.1--patch-277 + + * gdisorder/queue.c: Show and maintain the selection. You can't do + anything useful with the selection yet, however. + * lib/eclient.c: Fill in backlinks in queue lists. + * gdisorder/gdisorder.c: New Edit menu. Just a placeholder right now. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/queue.c lib/eclient.c + + +2006-04-16 22:47:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-276 + + Summary: + Popup menu in queue/recent. + Revision: + disorder--mainline--0.1--patch-276 + + * lib/eclient.c: disorder_eclient_scratch() now takes an ID. + disorder_eclient_scratch_playing() provides the old scratch-anything + interface for the benefit of control.c. + * lib/queue.h: Add a 'ql' field to the queue so gdisorder can remember + which queue each entry belongs to. + * gdisorder/queue.c: Popup menus on right button in queues. Currently + only scratch and remove work, though 'properties' appears in the menu + for the sake of show. + * gdisorder/control.c: Keep up with eclient.c. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/control.c + gdisorder/queue.c lib/eclient.c lib/eclient.h lib/queue.c + lib/queue.h + + +2006-04-16 18:28:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-275 + + Summary: + Show how much of the currently playing track has played. + Revision: + disorder--mainline--0.1--patch-275 + + * gdisorder/queue.c: Show how much of the currently playing track has + been played. Destroy queue label widgets as well as their containing + eventbox. + + * gdisorder/gdisorder.c: Refetch currently playing track data whenever + the track is paused or resumed. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/gdisorder.c gdisorder/queue.c + + +2006-04-16 17:17:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-274 + + Summary: + New gdisorder buttons to enable/disable play/random play + Revision: + disorder--mainline--0.1--patch-274 + + * gdisorder/control.c: New buttons to enable/disable play/random play. + + * lib/eclient.c: New calls to enable/disable play/random play. + + * images/random.png: Question-mark icon to enable random play. + * images/randomcross.png: Crossed question-mark icon to disable random play + * images/notescross.png: Notes icon to enable play. We use the existing + notes.png to disable play. + + new files: + images/.arch-ids/notescross.png.id + images/.arch-ids/random.png.id + images/.arch-ids/randomcross.png.id images/notescross.png + images/random.png images/randomcross.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/control.c + images/Makefile.am lib/eclient.c lib/eclient.h + + +2006-04-14 17:06:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-273 + + Summary: + Volume control for gdisorder + Revision: + disorder--mainline--0.1--patch-273 + + * gdisorder/control.c: Volume control. Visually rather ugly but the + feature is now there. + * gdisorder/gdisorder.c: Monitor volume. + * lib/eclient.c: Volume support. + * server/disorderd.c: Check the current volume from time to time in case + it's changed outside the server's control. + * server/server.c: Remember the (believed) current volume. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/control.c gdisorder/gdisorder.c + gdisorder/gdisorder.h lib/eclient.c lib/eclient.h + server/disorderd.c server/server.c server/server.h + + +2006-04-14 10:45:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-272 + + Summary: + Split out icon code to control.c + Revision: + disorder--mainline--0.1--patch-272 + + * gdisorder/control.c: Split icon code out to a control.c. + * gdisorder/gdisorder.c: Split icon code out to a control.c. + * gdisorder/queue.c: Rename 'playing' to 'playing_track' to avoid + conflict with now-global playing boolean. + + new files: + gdisorder/control.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h gdisorder/queue.c + + +2006-04-12 19:45:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-271 + + Summary: + Fix queue/recent title truncation. + Revision: + disorder--mainline--0.1--patch-271 + + * gdisorder/queue.c: Determine title cell width each time round rather + than stashing it. Eliminates truncation of rightmost title. + + new files: + gdisorder/TODO + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + + +2006-04-12 18:36:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-270 + + Summary: + Pause and scratch buttons for gdisorder + Revision: + disorder--mainline--0.1--patch-270 + + The scratch button doesn't currently use the same trick as the GUI to be + sure it's scratching exactly the right thing. Since the display is much + more likely to be up to date that's less of an issue here. Still, it + would be good to fix it sometime. + + * gdisorder/gdisorder.c: Icon bar between menu bar and tabs containing + pause/resume buttons and a scratch button. Pause and resume are + actually separate buttons but exactly one is ever visible at any given + time. + + * lib/eclient.c: Implement _pause/_resume/_scratch commands. + protocol_error() takes an operation not a client, since the operation + pointer in the client may be the wrong one by the point it gets called. + Support the 'state' log entry. + + * server/server.c: The 'log' command now issues some initial lines to + synchronize the current state. + + * images/pause.png: Pause icon. + + * images/play.png: Play icon. + + new files: + images/.arch-ids/pause.png.id images/.arch-ids/play.png.id + images/pause.png images/play.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + images/Makefile.am lib/eclient.c lib/eclient.h server/server.c + + +2006-04-11 19:30:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-269 + + Summary: + Display track lengths + Revision: + disorder--mainline--0.1--patch-269 + + * lib/eclient.c: disorder_eclient_length() + * gdisorder/queue.c: Include track lengths in queue/recent. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + lib/eclient.c lib/eclient.h + + +2006-04-11 19:10:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-268 + + Summary: + Minor gdisorder fixes. + Revision: + disorder--mainline--0.1--patch-268 + + * lib/mem.c: Reverse test in xcalloc. You can ask for count=0 but size=0 + will give silly results. + + * gdisorder/choose.c: Build fixes for Mac. + + * gdisorder/queue.c: Cope with completely empty queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/choose.c + gdisorder/queue.c lib/mem.c + + +2006-04-09 22:12:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-267 + + Summary: + More concise logging + Revision: + disorder--mainline--0.1--patch-267 + + * server/server.c: Don't log boring errors (EPIPE when talking to a + client in particular). When we do log an error make sure it's the + correct one (though it usually was anyway). + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/server.c + + +2006-04-09 22:11:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-266 + + Summary: + Kill a recently introduced crash... + Revision: + disorder--mainline--0.1--patch-266 + + * lib/cache.c: Don't try to clean the cache if it does not exist yet! + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/cache.c + + +2006-04-09 22:03:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-265 + + Summary: + Title bar for gdisorder queue/recent listing + Revision: + disorder--mainline--0.1--patch-265 + + * gdisorder/queue.c: Rewrite in terms of layouts. This proved to be the + least painful way of getting a title bar which panned in synch. + * gdisorder/gdisorder.rc: gdisorder-title style is (by default) white on + black in a bold font. *.row-title is bound to it. + * lib/mem.c: xcalloc(). + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.rc + gdisorder/queue.c lib/mem.c lib/mem.h + + +2006-04-08 11:44:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-264 + + Summary: + Update CHANGES. + Revision: + disorder--mainline--0.1--patch-264 + + * CHANGES: updated. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-04-08 11:42:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-263 + + Summary: + Cache track lookups that use regexps. + Revision: + disorder--mainline--0.1--patch-263 + + Top-level track lookups generally involve scanning over the whole + database and filtering. This is significantly slower than any other + lookup - noticably slow in one slightly underpowered installation - so + well worthwhile caching the results. + + Note that we do not have a call to cache_expire anywhere in the server + yet. It doesn't matter: there is a daily automatic rescan, and all the + cached file lookups are junked at the end of any rescan, so a cache entry + never has a lifetime much greater than a day anyway. + + * lib/cache.c: cache_clean() allows selective or total elimination of + cache elements. + * server/trackdb.c: Clean out track lookup cache when a rescan completes. + * server/server.c: Cache track lookups that use regexps. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/cache.c lib/cache.h + server/server.c server/trackdb.c server/trackdb.h + + +2006-04-05 22:37:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-262 + + Summary: + Better reporting. + Revision: + disorder--mainline--0.1--patch-262 + + * lib/eclient.c: 'report' callback to signal what's going on to the + application. + * gdisorder/choose.c, gdisorder/queue.c: Set report line when we start a + command. + * lib/client.c: Clear the report line when the client goes idle. + + modified files: + ChangeLog.d/disorder--mainline--0.1 clients/test-eclient.c + gdisorder/choose.c gdisorder/client.c gdisorder/queue.c + lib/eclient.c lib/eclient.h + + +2006-04-05 22:24:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-261 + + Summary: + Tidying up + Revision: + disorder--mainline--0.1--patch-261 + + * templates/options.transform: Removed because now obsolete. + * CHANGES: Note the move here too. + + removed files: + templates/options.transform + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/Makefile.am + + +2006-04-05 21:34:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-260 + + Summary: + Correct text and sorting in track choice tab + Revision: + disorder--mainline--0.1--patch-260 + + * lib/configuration.c: 'transform' directive moved here from web options. + * lib/trackname.c: trackname_transform() and compare_tracks() moved from + CGI code. + * server/cgi.c: Don't parse 'transform' directive. cgi_transform() moved + to trackname.c. + * server/dcgi.c: compare_multi() moved to trackname.c. + + * gdisorder/choose.c: Display and sort directory and track names using + 'transform' directive, as the web interface. + + * README.upgrades: Mention move of 'transform'. + * doc/disorder_config.5.in: Move documentation for 'transform' to its new + section. + + * examples/config.sample.in: 'transform' directives moved to config file. + * debian/disorder.config: 'transform' directives moved to config file. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in gdisorder/choose.c + lib/configuration.c lib/configuration.h lib/trackname.c + lib/trackname.h server/cgi.c server/dcgi.c + + +2006-04-05 20:23:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-259 + + Summary: + Queue fixes + Revision: + disorder--mainline--0.1--patch-259 + + * gdisorder/queue.c: Queue now expands to fill horizontal space + available. Kill a GTK+ error message when there is nothing in the + queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + + +2006-04-02 18:58:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-258 + + Summary: + Memory leak + Revision: + disorder--mainline--0.1--patch-258 + + * gdisorder/choose.c: Don't leak tree widgets. It seems GTK+ remembers + pointers to them somewhere even after thay have been deparented, + frustrating the garbage collector. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/choose.c + + +2006-04-02 18:50:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-257 + + Summary: + Prettier colors + Revision: + disorder--mainline--0.1--patch-257 + + * gdisorder/choose.c: Don't set colors explicitly, just set widget names + instead. + + * gdisorder/gdisorder.c: Apply a default style. + + * gdisorder/misc.c: scroll_widget() adds a GtkViewport to non-GtkLayout + widgets and sets the name of the scrolled window's child (i.e. the + GtkViewport or the GtkLayout). + + * gdisorder/queue.c: Use a GtkTable instead of a list store in order to + colorize rows conveniently. Still not happy with the queue views but + they look better than they did. + + * doc/gdisorder.1.in: Start of a man page for gdisorder. + + * gdisorder/gdisorder.rc: Default style information for gdisorder. + + * scripts/text2c: Script to convert gdisorder.rc (or other files) into a + variable in C. + + new files: + doc/gdisorder.1.in gdisorder/gdisorder.rc scripts/text2c + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + gdisorder/Makefile.am gdisorder/choose.c gdisorder/gdisorder.c + gdisorder/gdisorder.h gdisorder/misc.c gdisorder/queue.c + scripts/Makefile.am + + +2006-04-02 14:55:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-256 + + Summary: + State reporting to event log. + Revision: + disorder--mainline--0.1--patch-256 + + * server/play.c: Report state changes to event log. + * server/server.c: Report volume changes to event log. + + * doc/disorder_protocol.5.in: Document the above. + + * scripts/inst: Correct path to CGI. + * debian/rules.m4: Correct path to CGI. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/rules.m4 + doc/disorder_protocol.5.in scripts/inst server/play.c + server/server.c + + +2006-04-02 14:43:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-255 + + Summary: + Tidying up + Revision: + disorder--mainline--0.1--patch-255 + + * gdisorder/Makefile.am: Lose 'gtk' prefix from source files. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + + renamed files: + gdisorder/gtkchoose.c + ==> gdisorder/choose.c + gdisorder/gtkclient.c + ==> gdisorder/client.c + gdisorder/gtkmisc.c + ==> gdisorder/misc.c + gdisorder/gtkqueue.c + ==> gdisorder/queue.c + + +2006-04-02 14:42:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-254 + + Summary: + Make scroll bar arrows work. + Revision: + disorder--mainline--0.1--patch-254 + + * gdisorder/gtkmisc.c: Fix up scroll step increments for layouts, which + for some reason default to 0. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gtkmisc.c + + +2006-04-02 11:52:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-253 + + Summary: + Mark currently playing tracks in the choose track widget. + Revision: + disorder--mainline--0.1--patch-253 + + * gdisorder/gtkchoose.c: Display the notes icon next to tracks that are + queued or playing. + * gdisorder/gtkqueue.c: Notify Choose tab when then the queue or playing + track change. New queued() function to tell whether a track is queued + or playing. + * gdisorder/gtkmisc.c: find_image() loads images into the cache as + pixbufs. + * lib/cache.c: More careful checking for cache expiry, since we may have + very large lifetimes. + * images/notes.png: New notes icon to mark currently playing tracks. + * configure.ac: Build and install images/ for GTK+ builds as well as + server builds. We share the images between the two. + + * gdisorder/gtkchoose.c: If the Choose layout shrinks then invalidate the + regions outside it, since they are not redrawn otherwise. + + new files: + images/.arch-ids/notes.png.id images/notes.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + gdisorder/gdisorder.h gdisorder/gtkchoose.c + gdisorder/gtkmisc.c gdisorder/gtkqueue.c images/Makefile.am + lib/cache.c + + +2006-04-01 19:00:06 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-252 + + Summary: + Client-only (and Mac) fixups + Revision: + disorder--mainline--0.1--patch-252 + + The result of this works on my Mac with a minimal configuration file, + connecting to the server on a Linux box using TCP/IP. + + * configure.ac: Don't search fink db4 includes if not building server. + Fix bad test for want_server when checking db version. + * lib/log.c: Use explicit casts and wide types when printing timestamps + in debug messages. + * lib/configuration.c: Removed server-specific configuration checks. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/configuration.c lib/log.c + + +2006-04-01 18:20:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-251 + + Summary: + Split up clients from server and allow configure-time selection. + Revision: + disorder--mainline--0.1--patch-251 + + The command line clients are split into their own directory, clients/ and + everything else that used to be in progs/ is now in server. You can + control what is built with --without-python, --without-gtk and + --without-server. + + * configure.ac: Tell the top-level makefile what subdirectories to build + based on --with/--without options. We also only ask for libraries that + we actually need. + The shipped getopt is abolished until someone wants it enough to figure + out a convenient way of having it used from multiple directories. + * clients/disorder.c: Abolish --length option. It was always in the + wrong place anyway and the client/server build split makes it even + siller as well as inconvenient. + + new files: + clients/.arch-ids/=id clients/Makefile.am + + removed files: + DESIGN2 + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am + clients/disorder.c configure.ac prepare server/Makefile.am + + renamed files: + progs/.arch-ids/=id + ==> server/.arch-ids/=id + progs/authorize.c + ==> clients/authorize.c + progs/authorize.h + ==> clients/authorize.h + progs/disorder.c + ==> clients/disorder.c + progs/test-eclient.c + ==> clients/test-eclient.c + + new directories: + clients clients/.arch-ids server/.arch-ids + + removed directories: + progs/.arch-ids + + renamed directories: + progs + ==> server + + +2006-04-01 14:46:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-250 + + Summary: + Minor fixes. + Revision: + disorder--mainline--0.1--patch-250 + + * gdisorder/gdisorder.c: Refetch server state once every 10m. + * gdisorder/gtkchoose.c: Make background white. Probably the wrong + answer - we really want to make it do whatever the standard tree view + widget does. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/gtkchoose.c + + +2006-04-01 14:27:41 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-249 + + Summary: + Show currently playing track + Revision: + disorder--mainline--0.1--patch-249 + + * lib/eclient.c: disorder_eclient_playing() reports currently playing + track. + * gdisorder/gtkqueue.c: Bung currently playing track at top of queue. It + could really do with being a separate color or something. + * gdisorder/gdisorder.c: Call playing_update() when the currently playing + track might have changed. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/gdisorder.h gdisorder/gtkqueue.c lib/eclient.c + lib/eclient.h + + +2006-04-01 12:57:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-248 + + Summary: + Pick tracks in gdisorder. + Revision: + disorder--mainline--0.1--patch-248 + + * gdisorder/gtkchoose.c: Choose tracks from a tree structure. + + * gdisorder/gdisorder.c: choose_widget() moved to new gtkchoose.c. + * gdisorder/gtkqueue.c: Use scroll_widget(). + * gdisorder/gtkclient.c: Split out popup_protocol_error(). + * gdisorder/gtkmisc.c: Split out scroll_widget(). + + * lib/eclient.c: Support null callbacks. + + new files: + gdisorder/gtkchoose.c gdisorder/gtkmisc.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h + gdisorder/gtkclient.c gdisorder/gtkqueue.c lib/eclient.c + lib/eclient.h + + +2006-03-30 22:29:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-247 + + Summary: + Starting blocks for a GTK+ client + Revision: + disorder--mainline--0.1--patch-247 + + Currently the client can display the queue and recently played list and + keep up to date with them, and not much else. Hence it's noinst_ for + now. + + It's in a separate directory so that we can easily turn it on or off from + the configure with --with arguments. At some point the server and its + helpers will have to be moved to a new directory for the same reason so + you can conveniently do a client-only build. + + * lib/cache.c: Generic cache. + * lib/asprintf.c: New byte_xvasprintf(). + * lib/authhash.c: const-correct. + * lib/client.c: Split with_sockaddr() out to client-common.c + * lib/hash.c: New hash_foreach() (used by cache expiry) + * lib/queue.h: New selected field in queue entries for use by client. + Not in marshalled form! + * lib/queue.c: queue_unmarshall_vec() for parsing pre-split queue + descriptions. + * lib/client-common.c: Common code for the two C client implementations. + * lib/eclient.c: New asynchronous C client. + + * gdisorder/gdisorder.c: Main program for GTK+ client. Not finished! + * gdisorder/gtkqueue.c: Queue management in GTK+. + * gdisorder/gtkclient.c: Wrap an eclient up so it can be used in a GTK+ + program. + * gdisorder/gdisorder.h: Header file for GTK+ client. + + * progs/test-eclient.c: Test rig for eclient.c + + * doc/disorder_protocol.5.in: XX4 description was missing from last + commit. + + * configure.ac: Find GTK+/Glib includes; we need to fix them up to use + -isystem as they can provoke warnings. + + new files: + gdisorder/.arch-ids/=id gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h + gdisorder/gtkclient.c gdisorder/gtkqueue.c lib/cache.c + lib/cache.h lib/client-common.c lib/client-common.h + lib/eclient.c lib/eclient.h progs/test-eclient.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am configure.ac + doc/disorder_protocol.5.in lib/Makefile.am lib/asprintf.c + lib/authhash.c lib/authhash.h lib/client.c lib/hash.c + lib/hash.h lib/printf.h lib/queue.c lib/queue.h + progs/Makefile.am {arch}/=tagging-method + + new directories: + gdisorder gdisorder/.arch-ids + + +2006-03-30 21:50:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-246 + + Summary: + Log changes + Revision: + disorder--mainline--0.1--patch-246 + + Required for work that isn't checked in yet. + + * progs/server.c: Log returns 254 to indicate an indefinite body. + * doc/disorder_protocol.5.in: Document xx4 responses and log namespace + change. + * lib/queue.c: Change log tags so that they are valid C identifiers. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + lib/queue.c progs/server.c + + +2006-03-26 23:28:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-245 + + Summary: + Tidier debug output. + Revision: + disorder--mainline--0.1--patch-245 + + * lib/log.c: Strip ../ from filenames in debug output. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c + + +2006-03-26 19:36:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-244 + + Summary: + Quoting bug and response code sanity + Revision: + disorder--mainline--0.1--patch-244 + + * lib/split.c: Quote strings containing newlines properly. + * progs/server.c: Coherent response code policy. + * doc/disorder_protocol.5.in: Document response code policy and also the + authentication protocol. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + lib/split.c progs/server.c + + +2006-03-25 18:28:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-243 + + Summary: + Release 1.5.1 + Revision: + disorder--mainline--0.1--patch-243 + + * configure.ac: Release 1.5.1 + * CHANGES: Bring up to date + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog + + +2006-03-22 20:38:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-242 + + Summary: + Buglet in choose page + Revision: + disorder--mainline--0.1--patch-242 + + * templates/choosealpha.html: correct '*' link. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choosealpha.html + + +2006-03-20 22:43:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-241 + + Summary: + Documentation setting + Revision: + disorder--mainline--0.1--patch-241 + + * doc/disorder_config.5.in: Trivial formatting fix. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2006-03-20 22:41:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-240 + + Summary: + Release 1.5 + Revision: + disorder--mainline--0.1--patch-240 + + * lib/configuration.c: Compatibility alias 'nice' for 'nice_rescan' to + keep old config files working (it was in the example even if it wasn't + documented). + * examples/config.sample.in: Remove 'nice' from sample config. + * configure.ac: Release 1.5 + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog debian/disorder.config + examples/config.sample.in lib/configuration.c + + +2006-03-20 22:30:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-239 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-239 + + * templates/help.html: Eliminate 'just'. + * doc/checklist.txt: More checklist items. + * templates/about.html: Copyright dates + * scripts/dist: Copyright dates + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/checklist.txt + scripts/dist templates/about.html templates/help.html + + +2006-03-20 21:55:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-238 + + Summary: + Bug blame + Revision: + disorder--mainline--0.1--patch-238 + + * BUGS: Point a finger of blame at Libtool. + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 + + +2006-03-19 20:04:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-237 + + Summary: + Attribute scratches properly. + Revision: + disorder--mainline--0.1--patch-237 + + * progs/play.c: Attribute scratches to the user who requested the scratch. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2006-03-19 20:03:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-236 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-236 + + * progs/rescan.c: Copyright date. + * progs/play.c: Copyright date. + * README: Copyright date. + * scripts/dist: cd into =build if it exists. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README progs/play.c + progs/rescan.c scripts/dist + + +2006-03-19 00:19:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-235 + + Summary: + New speaker_nice option. + Revision: + disorder--mainline--0.1--patch-235 + + * lib/configuration.c: New speaker_nice option. + * progs/disorderd.c: Start speaker process as root. + * progs/speaker.c: Set nice value, ignore SIGPIPE and become mortal. + * progs/trackdb.c: Reset subprocess priority to zero. (Ineffectual if + the main server is already at positive niceness.) + * lib/user.c: Moved to lib since shared between several programs. + + * doc/disorder_config.5.in: Document speaker_nice. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/Makefile.am lib/configuration.c + lib/configuration.h progs/Makefile.am progs/disorderd.c + progs/speaker.c progs/trackdb.c templates/credits.html + + renamed files: + progs/user.c + ==> lib/user.c + progs/user.h + ==> lib/user.h + + +2006-03-18 23:52:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-234 + + Summary: + Move deployed filtering of track names closer to the right place + Revision: + disorder--mainline--0.1--patch-234 + + * progs/dcgi.c: Use the server's regexp filtering rather than getting all + the files and throwing away the ones that do not match. + + * progs/disorder.c: Correct handling of variable-argument commands + Make regexp support available to files/allfiles/dirs. + + * progs/trackdb.c: Correct handling of pcre_exec() return value, so we + don't report no match if there are subpatterns. We still log + unexpected errors. + + * templates/choosealpha.html: With server-based regexp filtering, we need + slightly more subtle regexps to keep the initial 'the' out. Perhaps + better still (for choosealpha) would be some kind of server-held cache, + but we'll see what performance is like in reality. + + * doc/disorder.1.in: Document regexp support in the command line client. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_config.5.in progs/dcgi.c progs/disorder.c + progs/trackdb.c templates/choosealpha.html + + +2006-03-18 21:00:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-233 + + Summary: + First round of process priority changes. + Revision: + disorder--mainline--0.1--patch-233 + + * progs/disorderd.c: Apply nice_server config setting at startup. + * progs/rescan.c: nice_rescan applies to whole rescan process, not just + to collection scanner subprocesses. + * lib/configuration.c: 'nice' renamed to nice_rescan. Added + nice_server. + * doc/disorder_config.5.in: Document nice_rescan and nice_server. (The + former was never documented in its old guise as 'nice'.) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/disorderd.c progs/rescan.c + + +2006-03-18 20:43:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-232 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-232 + + * CHANGES: missing CHANGES note for the last commit. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-03-18 20:42:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-231 + + Summary: + Don't crash when the user tries to pause a non-pausible track. + Revision: + disorder--mainline--0.1--patch-231 + + * progs/play.c: Correct test for pause-capable players, which contained + an embarassing parenthesization error. Because of this mistake, if you + tried to pause a track using a non-pause-capable player, the server + would immediately crash. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2006-03-18 20:36:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-230 + + Summary: + Fix a disorder-speaker crash + Revision: + disorder--mainline--0.1--patch-230 + + * progs/speaker.c: If the speaker process detects underrun then it should + not later treat written_bytes as a byte count! This could lead to the + speaker process crashing. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/speaker.c + scripts/inst + + +2006-02-19 11:39:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-229 + + Summary: + Missing build dependencies + Revision: + disorder--mainline--0.1--patch-229 + + * debian/control (Build-Depends): Was missing libao-dev. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/control + + +2005-11-15 19:28:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-228 + + Summary: + Report track played so far properly. + Revision: + disorder--mainline--0.1--patch-228 + + * progs/speaker.c: Record (and therefore report) amount of a track played + so far, which got lost in the ALSA transition. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/checklist.txt + progs/speaker.c + + renamed files: + doc/ui-checklist.txt + ==> doc/checklist.txt + + +2005-11-15 19:14:45 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-227 + + Summary: + Clean up logging FD leaks. + Revision: + disorder--mainline--0.1--patch-227 + + * lib/logfd.c: Don't leak reader end of log pipe when it's finished. + * progs/play.c: Don't leak writer end of log pipe if fork fails. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/logfd.c progs/play.c + + +2005-11-05 15:38:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-226 + + Summary: + Release 1.4 + Revision: + disorder--mainline--0.1--patch-226 + + * BUGS: Document past problems with VIA OSS driver. + * configure.ac: Change version number. + * debian/changelog: New version number. + + modified files: + BUGS CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog + + +2005-11-05 15:01:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-225 + + Summary: + Update copyright dates and testing + Revision: + disorder--mainline--0.1--patch-225 + + * doc/ui-checklist.txt: More checks. + * scripts/check: Ignore certain files. + + new files: + scripts/copyright.exceptions + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/ui-checklist.txt + lib/syscalls.c lib/syscalls.h plugins/Makefile.am + plugins/exec.c plugins/shell.c scripts/check + + +2005-11-05 14:36:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-224 + + Summary: + New stopwords + Revision: + disorder--mainline--0.1--patch-224 + + * examples/config.sample.in: Add 'for' and 'is' to stopwords. + * debian/disorder.config: Add 'for' and 'is' to stopwords. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/disorder.config + examples/config.sample.in + + +2005-11-05 14:34:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-223 + + Summary: + Documentation cleanups. + Revision: + disorder--mainline--0.1--patch-223 + + * templates/help.html: Missing words. + * doc/disorder-dump.8.in: Typo. + * doc/disorder.3: Catch up with reality. + * doc/disorder_protocol.5.in: Document 'pause' and 'resume'. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder-dump.8.in + doc/disorder.3 doc/disorder_protocol.5.in doc/ui-checklist.txt + templates/help.html + + +2005-11-05 14:19:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-222 + + Summary: + Make @nfiles@ consistent. + Revision: + disorder--mainline--0.1--patch-222 + + * progs/dcgi.c: In @nfiles@, if cgi arg files is not set then default to + 1 (to be consistent with everywhere else). + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-11-05 14:15:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-221 + + Summary: + No Darwin support for now. + Revision: + disorder--mainline--0.1--patch-221 + + Remove references to Darwin, since ALSA dependency breaks it. You could + still build without support for raw players, but it won't work "out of + the box". + + removed files: + README.darwin + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 Makefile.am + + +2005-11-04 12:49:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-220 + + Summary: + disorder-speaker now uses ALSA. + Revision: + disorder--mainline--0.1--patch-220 + + This seems to work better than the various libao attempts. We still use + a libao plugin to capture decoded audio from ogg123 etc. + + * progs/speaker.c: Use ALSA directly rather than libao. + * lib/configuration.c: Configurable ALSA device. + * lib/log.c: Timestamp debug messages. + * configure.ac: Chheck for ALSA library. + * doc/disorder_config.5.in: Document 'device' option. + * README: Mention ALSA dependency. + * debian/control: Depend on alsa development library package. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README configure.ac + debian/changelog debian/control doc/disorder_config.5.in + lib/configuration.c lib/configuration.h lib/log.c + progs/Makefile.am progs/speaker.c + + +2005-11-02 17:58:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-219 + + Summary: + Log speaker stderr. + Revision: + disorder--mainline--0.1--patch-219 + + * progs/play.c: log speaker stderr + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/play.c + + +2005-11-01 15:54:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-218 + + Summary: + Typo fixes + Revision: + disorder--mainline--0.1--patch-218 + + * doc/disorder.1.in: Typo fixes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + + +2005-10-23 13:59:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-217 + + Summary: + Fix process-management problems. + Revision: + disorder--mainline--0.1--patch-217 + + * lib/hash.c: Grow array correctly so we don't lose PIDs. + * lib/queue.h: Abolish per-track signal. Too much hassle for not enough + gain. + * progs/play.c: Abolish per-track signal. Log failures to find PIDs. + * progs/speaker.c: Log too-short grace periods. + + * doc/disorder_config.5.in: Document the above. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw + debian/changelog doc/disorder_config.5.in lib/hash.c + lib/queue.c lib/queue.h progs/play.c progs/speaker.c + + +2005-10-22 16:22:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-216 + + Summary: + Various shutdown fixes + Revision: + disorder--mainline--0.1--patch-216 + + * progs/play.c: Terminate all players on shutdown, not just the one for + the currently playing track. + * lib/configuration.c: Default signal to forcibly terminate players is + now SIGKILL. + * examples/config.sample.in: Don't use --signal=SIGKILL any more. + * debian/disorder.config: Don't use --signal=SIGKILL any more. + * doc/disorder_config.5.in: Document change to SIGKILL. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.raw + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in lib/configuration.c progs/play.c + + +2005-10-22 16:03:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-215 + + Summary: + Stop web UI refreshing like mad + Revision: + disorder--mainline--0.1--patch-215 + + * progs/dcgi.c: Don't clamp refresh to gap if next track in queue is a + random track and random play is not enabled. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/dcgi.c + + +2005-10-16 20:16:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-214 + + Summary: + Don't forget PIDs across reconfiguration. + Revision: + disorder--mainline--0.1--patch-214 + + This change moves PIDs out of queue entries and instead uses a hash table + to map IDs to PIDs. The reason is that on reconfigure the queue is saved + and reloaded, losing the PID along the way. + + * lib/hash.c: Simple hash table. + * lib/queue.h: Don't keep PID in queue data structure. + * progs/play.c: Move PID records to a hash table. + + new files: + lib/hash.c lib/hash.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/queue.c lib/queue.h progs/play.c + + +2005-10-16 18:12:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-213 + + Summary: + Clean up rescan abort. + Revision: + disorder--mainline--0.1--patch-213 + + * progs/trackdb.c: reap_rescan() no longer trashes PID of new rescanner. + trackdb_scan() is now interruptible. Propagate debug status to + subprocesses. + * progs/deadlock.c: --no-debug option. + * progs/rescan.c: Cleaner abort. --no-debug option. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/deadlock.c + progs/rescan.c progs/trackdb-int.h progs/trackdb.c + + +2005-10-16 17:38:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-212 + + Summary: + Don't forget lookahead on reconfigure. + Revision: + disorder--mainline--0.1--patch-212 + + * progs/play.c: Commit queue after adding a random track. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2005-10-16 17:33:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-211 + + Summary: + More on disorder-rescan interruption. + Revision: + disorder--mainline--0.1--patch-211 + + * progs/rescan.c: SA_RESTART for SIGINT/SIGTERM. + * CHANGES: Mention the reconfigure hang bug as fixed. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/rescan.c + + +2005-10-16 17:24:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-210 + + Summary: + Propagate reconfigure requests to the speaker process. + Revision: + disorder--mainline--0.1--patch-210 + + This is more work than it sounds because previously the reconfigure code + would leak memory in the absence of a garbage collector. It still will + on error, but this should be rather rare - if the server fails to parse + the configuration it doesn't tell the speaker to reload it. + + Along the way fixed a logging bug and a rescan bug than could hang the + server in a db close operation. + + * progs/state.c: Transmit reconfiguration requests to the speaker process. + * progs/play.c: Transmit reconfiguration requests to the speaker process. + * progs/speaker.c: Log configuration reloads. + * lib/configuration.c: Free old configs when new ones are installed. + This implies taking a bit more care over when we strdup. + Also put in VALUE/ADDRESS macros for less typo-prone access to + configuration struct members. + * lib/mem.c: xfree() calls free/GC_free as appropriate. + * lib/charset.c: Use realloc rather than malloc to avoid leaking memory + in non-GC case + * progs/rescan.c: Shut down cleanly on SIGTERM/SIGINT to keep db happy. + Also use stderr rather than stdout to determine whether to log to + syslog or stderr. + * progs/trackdb.c: Only redirect subprogram output if we're not on a + terminal. Also stomp db_deadlock_pid on a just-in-case basis. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/configuration.c lib/configuration.h lib/mem.c lib/mem.h + progs/play.c progs/play.h progs/rescan.c progs/speaker.c + progs/state.c progs/trackdb.c + + +2005-10-16 15:44:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-209 + + Summary: + More careful checking for track termination in speaker. + Revision: + disorder--mainline--0.1--patch-209 + + * progs/speaker.c: Cope with player being interrupted part way through a + frame. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/speaker.c + + +2005-10-16 13:53:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-208 + + Summary: + Workarounds for various external bugs. + Revision: + disorder--mainline--0.1--patch-208 + + * README.raw: Document ogg123 braindamage. + * examples/config.sample.in: --signal=SIGKILL for ogg123. + * debian/disorder.config: --signal=SIGKILL for ogg123. + * driver/disorder.c: Call exit instead of _exit, to allow for player + cleanup. + * lib/event.c: Work around ptrace braindamage. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.raw + debian/changelog debian/disorder.config driver/disorder.c + examples/config.sample.in lib/event.c + + +2005-10-15 19:20:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-207 + + Summary: + Fix crash in speaker process. + Revision: + disorder--mainline--0.1--patch-207 + + * progs/speaker.c: Check the number of bits per sample in the incoming + data, not the currently playing track. This could not only cause the + answer to be potentially wrong, but would crash if there was no playing + track. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/speaker.c + + +2005-10-15 15:59:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-206 + + Summary: + Catch up. + Revision: + disorder--mainline--0.1--patch-206 + + * CHANGES: Mention multi-track editing. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-15 15:44:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-205 + + Summary: + Edit prefs for a whole directory at once. + Revision: + disorder--mainline--0.1--patch-205 + + * progs/dcgi.c: Support editing prefs for multiple track simultaneously. + * templates/choose.html: Link to whole-directory prefs edit. + * templates/prefs.html: Support editing prefs for multiple track + simultaneously. We lose the raw prefs interface for the time being; it + remains available via the command line however. + * templates/recent.html: New prefs args. + * templates/options.labels: New labels for whole-directory prefs-editing. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/choose.html templates/options.labels + templates/prefs.html templates/recent.html + + +2005-10-15 13:07:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-204 + + Summary: + Random track lookahead. + Revision: + disorder--mainline--0.1--patch-204 + + * lib/queue.c: Support adding queue entries just before the final random + track. Cope with reading back in randomly pick tracks from a saved + queue (they have no submitter which was previously not allowed). + * progs/dcgi.c: @who@ now just reports an empty string if there was no + submitter rather than  . Also correct the order of the table of + expansions. + * progs/play.c: add_random_track() adds a random track if random play is + enabled and there is no such track in the queue. play() now uses that + instead of doing it itself, and doesn't play random tracks found in the + queue if random play is disabled. It also adds a new random track just + after playing the old one. + If necessary add a random track when play/random play are enabled. + * progs/server.c: Add a new random track if the old one is removed. Also + when random track is moved to a non-final position in the queue then we + make it into a normal track and add a new random track. + * templates/playing.html: Indicate the random track in the queue and if + playing, using new labels. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/queue.c + lib/queue.h progs/dcgi.c progs/play.c progs/play.h + progs/server.c templates/options.labels templates/playing.html + + +2005-10-15 12:28:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-203 + + Summary: + Cope with randomly chosen tracks in the queue. + Revision: + disorder--mainline--0.1--patch-203 + + At the moment nothing adds such tracks to the queue, so this is just + laying foundations. + + * lib/queue.h: New 'random' track state indicates that the track was a + randomly chosen track in the queue rather than explicitly requested. + * progs/dcgi.c: @state@ expansion provides access to track state field in + queue. Process random tracks just as unplayed ones. + * progs/play.c: Handle 'random' state just like 'unplayed'. + * doc/disorder_config.5.in: Document @state@ expansion and track states. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + lib/queue.c lib/queue.h progs/dcgi.c progs/play.c + + +2005-10-15 11:58:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-202 + + Summary: + Complete label docs. + Revision: + disorder--mainline--0.1--patch-202 + + * templates/options.labels: Document links.css, missed in previous pass. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + + +2005-10-15 11:56:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-201 + + Summary: + More change detail. + Revision: + disorder--mainline--0.1--patch-201 + + * CHANGES: More detail. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-15 11:39:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-200 + + Summary: + Move documentation of labels to options.labels. + Revision: + disorder--mainline--0.1--patch-200 + + * templates/options.labels: Document labels. + * doc/disorder_config.5.in: No longer document labels here. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in templates/options.labels + + +2005-10-15 11:16:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-199 + + Summary: + New buttons to move to head/tail of queue. + Revision: + disorder--mainline--0.1--patch-199 + + * images/Makefile.am: Install new images. + * templates/playing.html: New buttons to move to head/tail. + * templates/options.labels: Labels to configure new buttons. + * debian/options.debian: Keep up to date. + * templates/help.html: Document new queue management. + * doc/disorder_config.5.in: Document new labels (at least as well as the + old ones; I am starting to think that the options.* files would be a + better place for this stuff.) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + debian/options.debian doc/disorder_config.5.in + images/Makefile.am templates/help.html + templates/options.labels templates/playing.html + + +2005-10-15 10:55:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-198 + + Summary: + new images + Revision: + disorder--mainline--0.1--patch-198 + + New {no,}{upup,downdown}.png images for sending a track right to the end + of the queue. + + new files: + images/.arch-ids/downdown.png.id + images/.arch-ids/nodowndown.png.id + images/.arch-ids/noupup.png.id images/.arch-ids/upup.png.id + images/downdown.png images/nodowndown.png images/noupup.png + images/upup.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-10-14 18:33:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-197 + + Summary: + more debian catchup + Revision: + disorder--mainline--0.1--patch-197 + + * debian/options.debian: bring images up to date + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/options.debian + + +2005-10-14 18:14:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-196 + + Summary: + debian config catchup + Revision: + disorder--mainline--0.1--patch-196 + + * debian/disorder.config: no gap between tracks + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/disorder.config + + +2005-10-09 13:31:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-195 + + Summary: + Documentation. + Revision: + disorder--mainline--0.1--patch-195 + + * templates/help.html: Bring help up to date with UI changes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/help.html + + +2005-10-09 13:24:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-194 + + Summary: + Release notes. + Revision: + disorder--mainline--0.1--patch-194 + + * CHANGES: More coherent change description. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-09 13:07:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-193 + + Summary: + More release notes. + Revision: + disorder--mainline--0.1--patch-193 + + * README.raw: mpg321 is less buggy than ogg123. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw + + +2005-10-09 13:05:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-192 + + Summary: + Fix plugin interface. + Revision: + disorder--mainline--0.1--patch-192 + + * lib/disorder.h: Missing 'extern'. + * lib/speaker.c: Build fixes (for Darwin). + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/speaker.c + + +2005-10-09 12:59:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-191 + + Summary: + Pre-decode raw tracks; better support for buggy players; tidy-ups. + Revision: + disorder--mainline--0.1--patch-191 + + * progs/play.c: prepare() and abandon() allow tracks to be prepared for + play (and abandoned when they are removed from the queue) before they + are actually to be played. + * progs/server.c: Call prepare() and abandon() when appropriate. + * driver/disorder.c: New 'fragile' option to work around players that + ignore write errors. + + * doc/disorder_config.5.in: Document libao driver properly. + * README.raw: Bring notes on raw players up to date. + + * examples/config.sample.in: Use fragile driver option for ogg213. + * debian/disorder.config: Use disorder libao driver. + * debian/control: Depend on mpg321 directly as we address its command + line syntax now. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw debian/control + debian/disorder.config doc/disorder_config.5.in + driver/disorder.c examples/config.sample.in progs/play.c + progs/play.h progs/server.c + + +2005-10-09 10:52:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-190 + + Summary: + cleanfiles + Revision: + disorder--mainline--0.1--patch-190 + + * progs/Makefile.am: clean up temprary files + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + + +2005-10-09 10:41:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-189 + + Summary: + miscellaneous command-line fixups + Revision: + disorder--mainline--0.1--patch-189 + + * progs/Makefile.am: check that command completions are up to date + * scripts/completion.bash: bring command completions up to date + * progs/disorder.c: fix help for scratch-id + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + progs/disorder.c scripts/completion.bash + + +2005-10-08 17:00:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-188 + + Summary: + more web pause + Revision: + disorder--mainline--0.1--patch-188 + + Sort out pausing labels and use thereof. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + templates/playing.html + + +2005-10-08 16:55:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-187 + + Summary: + pausing from the web + Revision: + disorder--mainline--0.1--patch-187 + + * progs/dcgi.c: new @paused@ expansion and pause/resume actions. + * templates/playing.html: pausing. State buttons now have a uniform + button and a tick/cross by them to indicate state. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/disorder.css templates/options.labels + templates/playing.html + + +2005-10-08 16:32:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-186 + + Summary: + Images again + Revision: + disorder--mainline--0.1--patch-186 + + More coherent image naming. + + removed files: + images/.arch-ids/cross.png.id images/cross.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/Makefile.am + templates/options.labels templates/playing.html + + renamed files: + images/.arch-ids/noscratch.png.id + ==> images/.arch-ids/nocross.png.id + images/.arch-ids/scratch.png.id + ==> images/.arch-ids/cross.png.id + images/noscratch.png + ==> images/nocross.png + images/scratch.png + ==> images/cross.png + + +2005-10-08 16:29:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-185 + + Summary: + new images + Revision: + disorder--mainline--0.1--patch-185 + + Tick and cross images. + + new files: + images/.arch-ids/cross.png.id images/.arch-ids/tick.png.id + images/cross.png images/tick.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/Makefile.am + + +2005-10-08 15:08:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-184 + + Summary: + release notes + Revision: + disorder--mainline--0.1--patch-184 + + * CHANGES: mention pausing + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-08 15:05:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-183 + + Summary: + more pausing work + Revision: + disorder--mainline--0.1--patch-183 + + * lib/plugin.c: fix a rather disastrous typo + * lib/queue.c: new 'paused' state for a track. queue_fix_sofar() broken + out as a separate function. + * progs/dcgi.c: cope with paused tracks + * progs/server.c: call queue_fix_sofar() before sending currently playing + track or calculating expected start times. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c lib/queue.c + lib/queue.h progs/dcgi.c progs/disorder.c progs/play.c + progs/server.c + + +2005-10-08 14:38:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-182 + + Summary: + support pausers that don't know how much played + Revision: + disorder--mainline--0.1--patch-182 + + * lib/queue.c: support a played-so-far return of -1, as described in + disorder.h + * doc/disorder.3: document played-so-far of -1. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 lib/queue.c + + +2005-10-08 14:26:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-181 + + Summary: + typo + Revision: + disorder--mainline--0.1--patch-181 + + * progs/disorder.c: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-10-08 14:24:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-180 + + Summary: + more docs + Revision: + disorder--mainline--0.1--patch-180 + + * doc/disorder.3: note that prefork/pause functions mustn't block. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + + +2005-10-08 14:15:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-179 + + Summary: + more pause/resume work + Revision: + disorder--mainline--0.1--patch-179 + + * lib/play.c: Pausing is now a capabality (of standalone players) not + a new player type. New notify calls for pausing/resuming. + * python/disorder.py.in: Python bindings for pause/resume. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/plugin.c lib/plugin.h plugins/notify.c + progs/play.c python/disorder.py.in + + +2005-10-08 14:05:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-178 + + Summary: + docs + minor fixes + Revision: + disorder--mainline--0.1--patch-178 + + * doc/disorder.3: document updated player plugin interface + * doc/disorder.1.in: document pause/resume commands + * lib/disorder.h: correct sifnature of disorder_play_resume + * progs/disorder.c: correct order of commands + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder.3 lib/disorder.h progs/disorder.c + + +2005-10-08 13:49:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-177 + + Summary: + typo fix + Revision: + disorder--mainline--0.1--patch-177 + + * progs/disorder.c: typo + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-10-08 13:48:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-176 + + Summary: + implement pausing support + Revision: + disorder--mainline--0.1--patch-176 + + _PLAYER_PAUSES is not really tested since I don't have any playes that + support that protocol. Also the web interface does not know anything + about this yet, and it is not documented. + + * progs/play.c: Implement pausing. + + * lib/queue.c: Pause/resume tracking for _PLAYER_PAUSES players. + * lib/client.c: C pause/resume bindings. + * lib/plugin.c: Pause/resume stubs. + * progs/disorder.c: Pause/resume commands. + * progs/server.c: Pause/resume commands. Automatically resumes under + conditions. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/client.c lib/client.h + lib/plugin.c lib/plugin.h lib/queue.c lib/queue.h + progs/disorder.c progs/play.c progs/play.h progs/server.c + + +2005-10-05 21:53:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-175 + + Summary: + documentation catchup + Revision: + disorder--mainline--0.1--patch-175 + + First pass at documenting the latest batch of changes. Also updated the + sample config file to use raw players for OGG and MP3, and to remove the + inter-track gap. + + new files: + README.raw + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + README.upgrades doc/disorder_config.5.in + examples/config.sample.in + + +2005-10-05 20:58:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-174 + + Summary: + implement and use the speaker process. + Revision: + disorder--mainline--0.1--patch-174 + + No documentation yet! + + * driver/disorder.c: Look at ${DISORDER_RAW_FD} for default output file + descriptor. + * lib/plugin.c: New play_get_type(). Tidy up. + * lib/queue.h: Correct order of enum constants. + New queue entry fields: + * type - type word from plugin + * pid - process ID of decoder/player + * sofar - how many seconds played so far + * signal - signal to kill decoder/player + * lib/queue.c: fake up sofar field where it's not filled in by the + speaker process + * lib/syscalls.c: we never use chdir, so remove it. + * plugins/execraw.c: exec plugin for decoders, uses modified exec.c + * progs/deadlock.c: use stderr not stdout to determine whether we're a + daemon + * progs/disorder.c: report sofar + * progs/disorderd.c: start speaker process + * progs/play.c: use the speaker process for _RAW players. + + send the speaker an SM_PLAY if a _RAW player + + support --wait-for-device and --signal player options + + act on incoming messages from the speaker + + split player-finished logic into process-finished and generic halves + + still scratch even if subprocess terminated + + disconnect from speaker when quitting + * lib/signame.c: split out of configuration.c, needed by --signal player + option + * lib/speaker.c: speaker/server communication support + * progs/speaker.c: the speaker process itself. + + new files: + lib/signame.c lib/signame.h lib/speaker.c lib/speaker.h + plugins/execraw.c progs/speaker.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 driver/disorder.c + lib/Makefile.am lib/configuration.c lib/configuration.h + lib/disorder.h lib/plugin.c lib/plugin.h lib/queue.c + lib/queue.h lib/syscalls.c lib/syscalls.h plugins/Makefile.am + plugins/exec.c plugins/shell.c progs/Makefile.am + progs/deadlock.c progs/disorder.c progs/disorderd.c + progs/play.c progs/play.h progs/state.c + + +2005-10-01 17:31:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-173 + + Summary: + Prefork/cleanup support + Revision: + disorder--mainline--0.1--patch-173 + + * lib/plugin.c: simplify plugin opening interface. The main reason for + the plugin struct is to keep track of plugin names and to avoid repeat + opening, but it also allows the hiding of the dlopen() interface. + Also add support for the prefork/cleanup interface. + + * lib/queue.h: track plugin and its data for each track. Currently only + for the playing track but perhaps also for pre-decoded tracks in the + future. + + * progs/play.c: Support the prefork/cleanup player plugin interface. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c lib/plugin.h + lib/queue.c lib/queue.h progs/play.c + + +2005-10-01 16:35:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-172 + + Summary: + new plugin interface + Revision: + disorder--mainline--0.1--patch-172 + + New plugin interface declarations. No implementation behind them yet + however. + + * lib/disorder.h: new player plugin declarations + * plugins/shell.c: new plugin interface + * plugins/exec.c: new plugin interface + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/disorder.h + plugins/exec.c plugins/shell.c + + +2005-10-01 16:16:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-171 + + Summary: + libao driver + Revision: + disorder--mainline--0.1--patch-171 + + * driver/disorder.c: libao driver that outputs in raw format with a + descriptive header. + * README: note dependency on libao + + new files: + driver/.arch-ids/=id driver/Makefile.am driver/disorder.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am README + README.darwin configure.ac + + new directories: + driver driver/.arch-ids + + +2005-09-27 18:30:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-170 + + Summary: + random play enabled on first startup + Revision: + disorder--mainline--0.1--patch-170 + + If random play is enabled before we've got any tracks then + trackdb_random() tried to get record 1 of 0, and fails, and terminates + the process. Fixed. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/trackdb.c + + +2005-08-07 11:11:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-169 + + Summary: + build against fink libraries without extra configure args + Revision: + disorder--mainline--0.1--patch-169 + + * configure.ac: if fink is installed, add some CPPFLAGS/LDFLAGS based on + its path. + * README.darwin: no longer need to specify paths to fink libraries. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.darwin configure.ac + + +2005-07-04 21:55:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-168 + + Summary: + more build-time testing + Revision: + disorder--mainline--0.1--patch-168 + + * lib/Makefile.am: don't automatically run checks + * progs/Makefile.am: check that --help options work + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/Makefile.am scripts/dist scripts/inst + + +2005-07-04 21:40:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-167 + + Summary: + don't crash in --help-commands + Revision: + disorder--mainline--0.1--patch-167 + + * progs/disorder.c: don't crash in --help-commands + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-06-18 17:31:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-166 + + Summary: + correct version numbers + Revision: + disorder--mainline--0.1--patch-166 + + * README.upgrades: correct version numbers + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + + +2005-06-17 17:24:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-165 + + Summary: + post 1.3 + Revision: + disorder--mainline--0.1--patch-165 + + * CHANGES: remove entries up to 1.0. 200 lines is plenty of history in a + file people are expected to actually read. + * configure.ac: version changed to 1.3+dev + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + + +2005-06-16 20:42:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-164 + + Summary: + release 1.2 + Revision: + disorder--mainline--0.1--patch-164 + + * progs/server.c: save queue after move operation + * configure.ac: release 1.3 + * debian/changelog: release 1.3 + * CHANGES: release 1.3 + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog progs/server.c + + +2005-06-16 20:31:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-163 + + Summary: + missing documentation + Revision: + disorder--mainline--0.1--patch-163 + + * doc/disorder_config.5.in: document 'alias' directive + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-06-16 20:16:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-162 + + Summary: + distribution administrivia + Revision: + disorder--mainline--0.1--patch-162 + + * debian/rules.m4: install all READMEs + * progs/Makefile.am: remember trackdb-int.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/rules.m4 progs/Makefile.am + + +2005-06-08 09:48:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-161 + + Summary: + quieten tree-lint + Revision: + disorder--mainline--0.1--patch-161 + + * {arch}/=tagging-method: stop tree-lint from complaining about emacs + droppings + + modified files: + ChangeLog.d/disorder--mainline--0.1 {arch}/=tagging-method + + +2005-06-01 22:45:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-160 + + Summary: + darwin port update + Revision: + disorder--mainline--0.1--patch-160 + + Build on Mac OS X again. + + * progs/trackdb.c: missing header + * progs/trackdb-int.h: correct types + * README.darwin: note requirement for db4.3 + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.darwin + progs/trackdb-int.h progs/trackdb.c + + +2005-05-31 22:13:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-159 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-159 + + * CHANGES: mention missing files included for 1.2.1. The fclose checked + in 1.2.1 isn't present any more anyway. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-05-31 19:55:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-158 + + Summary: + web ui reports errors more gracefully + Revision: + disorder--mainline--0.1--patch-158 + + * progs/cgimain.c: report errors by calling disorder_cgi_error() + * progs/dcgi.c: disorder_cgi_error expands the 'error' template with the + 'error' label set to an error indicator string. + * progs/cgi.c: cgi_set_option allows web options to be set other than + through the config files + + * templates/error.html: new template for error pages + * templates/options.labels: default error strings + + new files: + templates/error.html + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/cgi.c progs/cgi.h + progs/cgimain.c progs/dcgi.c progs/dcgi.h + templates/Makefile.am templates/options.labels + + +2005-05-31 19:26:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-157 + + Summary: + management screen fixes + Revision: + disorder--mainline--0.1--patch-157 + + * templates/playing.html: put a <form> around volume input boxes, so the + client knows where to send the change. + * templates/disorder.css: management stuff is all displayed inline + * doc/ui-checklist.txt: new checklist for manual UI testing + + new files: + doc/ui-checklist.txt + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/disorder.css + templates/playing.html + + +2005-05-31 18:23:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-156 + + Summary: + minor build fixes + Revision: + disorder--mainline--0.1--patch-156 + + * progs/dcgi.c: quieten compiler + * lib/Makefile.am: delete the right file + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/dcgi.c + + +2005-05-30 11:02:45 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-155 + + Summary: + color enable/disable buttons to reflect current state + Revision: + disorder--mainline--0.1--patch-155 + + * templates/disorder.css: enable/disable button colors + * templates/playing.html: enable/disable buttons get red/green + backgrounds + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/disorder.css templates/options.labels + templates/playing.html + + +2005-05-30 10:27:41 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-154 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-154 + + Administrivia. + + modified files: + ChangeLog.d/disorder--mainline--0.1 acinclude.m4 + examples/disorder.init.in progs/api-server.c + + +2005-05-29 23:17:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-153 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-153 + + * CHANGES: note that subprocess output is logged + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-05-29 22:51:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-152 + + Summary: + log output of subprocesses + Revision: + disorder--mainline--0.1--patch-152 + + * lib/logfd.c: logfd() returns an FD which (via the event loop) ends up + in log output. Useful to pick up stdout/stderr of subprocesses. + * progs/trackdb.c: log output of deadlock and rescan subprocesses + * progs/play.c: log output of player subprocesses + + * progs/rescan.c: check for newline in raw path, not track + * progs/dcgi.c: quieten compiler + * progs/dump.c: quieten compiler + + new files: + lib/logfd.c lib/logfd.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/dcgi.c progs/dump.c progs/play.c progs/rescan.c + progs/trackdb.c + + +2005-05-29 18:30:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-151 + + Summary: + unweird search output + Revision: + disorder--mainline--0.1--patch-151 + + Searching was producing weird results: the artist 'Various' was being + displayed as the first aliased artist (but sorted, correctly, as + 'Various'). This change fixes this, though artists are still sorted by + their display name (so the 'The ...' bug remains in this context). + + * progs/dcgi.c: @search@ takes an additional CONTEXT argument, and marks + the last element in a group correctly. + * doc/disorder_config.5.in: document changes to @search@ + * templates/search.html: @search@ context used to avoid weirdness. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/search.html + + +2005-05-29 18:01:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-150 + + Summary: + rewritten track database code + Revision: + disorder--mainline--0.1--patch-150 + + * lib/disorder.h: _count and _getn are gone, replaced by _random + * plugins/pick.c: use _random instead of _count and _getn + + * lib/client.c: rescan no longer takes an arg; dump is gone; new resolve + command. + * lib/filepart.c: strip_extension() and extension() + * lib/trackname.c: implement 'ext' and 'path' parts here + * progs/api-server.c: adapt to new trackdb_ interface + * progs/dcgi.c: resolve paths when figuring out track status; implement + @resolve@ + * progs/disorder.c: new resolve, scratch-id; modified rescan + * progs/disorderd.c: adapt to new trackdb_ code; open database after + taking lockfile, not before; move user change code to user.c + * progs/play.c: adapt to new trackdb_ interface + * progs/server.c: adapt to new trackdb_ interface. resolve aliases when + playing. dump is gone. new resolve command. + * progs/state.c: dapt to new trackdb_ interface + * progs/trackdb.c: new trackdb. Largely rewritten though a little old + tracks.c material remains. + + * progs/dump.c: rewritten for new trackdb_ code. Dump and undump both + access the database directly and work while the server is running + (although particularly undumping while it is running is likely to be + painful). --recompute-aliases and --remove-pathless can be used to + tidy up broken databases, mostly useful when developing. --recover and + --recover-fatal provide access to libdb facilities (though these are + also available through db_recover). + * progs/deadlock.c: disorder-deadlock implementation. Just runs the + deadlock detector once a second. + * progs/rescan.c: disorder-rescan implementation. + * progs/user.c: user-switching code taken from disorderd.c + + * templates/choose.html: resolve track when linking to prefs + + * scripts/completion.bash: disorder-dump support + * examples/disorder.init.in: put sbindir on the path + + * debian/control: need libdb4.3-dev + + * doc/disorder-dump.8.in: document rewritten dump program + * doc/disorder.1.in: document resolve, scratch-id and modified rescan + * doc/disorder.3: _count and _getn are gone, replaced by _random; recheck + happens in a subprocess. + * doc/disorder_config.5.in: document @resolve@ + * doc/disorder_protocol.5.in: document resolve and changed rescan; dump + is gone. + * doc/disorderd.8.in: helper programs must be on the path; discuss + backups using disorder-dump. Mention DB_CONFIG. + + new files: + doc/disorder-deadlock.8.in doc/disorder-rescan.8.in + progs/deadlock.c progs/rescan.c progs/trackdb-int.h + progs/trackdb.c progs/trackdb.h progs/user.c progs/user.h + + removed files: + progs/rescan.c progs/tracks.c progs/tracks.h + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + README.upgrades debian/control doc/Makefile.am + doc/disorder-dump.8.in doc/disorder.1.in doc/disorder.3 + doc/disorder_config.5.in doc/disorder_protocol.5.in + doc/disorderd.8.in examples/disorder.init.in lib/client.c + lib/client.h lib/disorder.h lib/event.c lib/filepart.c + lib/filepart.h lib/log-impl.h lib/trackname.c plugins/pick.c + progs/Makefile.am progs/api-server.c progs/dcgi.c + progs/disorder.c progs/disorderd.c progs/dump.c progs/play.c + progs/server.c progs/state.c scripts/completion.bash + scripts/inst templates/choose.html + + +2005-05-27 14:24:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-149 + + Summary: + new 'lock' directive + Revision: + disorder--mainline--0.1--patch-149 + + * progs/disorderd.c: take lock here, under control of 'lock' directive + * progs/tracks.c: don't lock here any more + * lib/configuration.c: 'lock' directive + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/disorderd.c progs/tracks.c + + +2005-05-27 14:19:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-148 + + Summary: + wrap up program name setting + Revision: + disorder--mainline--0.1--patch-148 + + * lib/log.c: set_progname() sets progname from argv[0] + * progs/disorderd.c: use set_progname() + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c lib/log.h + progs/disorderd.c + + +2005-05-27 14:17:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-147 + + Summary: + optionally abort on fatal error + Revision: + disorder--mainline--0.1--patch-147 + + * lib/log-impl.h: DISORDER_FATAL_ABORT=yes forces fatal() to abort, for + debugging purposes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log-impl.h + + +2005-05-27 14:04:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-146 + + Summary: + more liberal url-encoding + Revision: + disorder--mainline--0.1--patch-146 + + * lib/kvp.c: allow RFC2396 unreserved characters plus '/' to pass + URL-encoding unchanged. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/kvp.c + + +2005-05-27 13:55:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-145 + + Summary: + more conservative signal handling + Revision: + disorder--mainline--0.1--patch-145 + + * lib/event.c: if we fail to write to the signal pipe then abort. This + saves worry about write() modifiying errno. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/event.c + + +2005-05-27 13:50:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-144 + + Summary: + libtool 1.4 no good + Revision: + disorder--mainline--0.1--patch-144 + + * README: libtool note + + modified files: + ChangeLog.d/disorder--mainline--0.1 README + + +2005-05-26 12:02:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-143 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-143 + + Update copyright dates. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/regsub.c lib/regsub.h + lib/vector.h + + +2005-05-26 11:54:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-142 + + Summary: + detect missing symbols in plugins early + Revision: + disorder--mainline--0.1--patch-142 + + * lib/plugin.c: use RTLD_NOW instead of RTLD_LAZY. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c + + +2005-05-22 13:15:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-141 + + Summary: + minor doc fixes + Revision: + disorder--mainline--0.1--patch-141 + + * debian/README.Debian: leftover names converted to /etc/disorder. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/README.Debian + + +2005-05-21 17:16:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-140 + + Summary: + check for UTF-8 support in pcre. + Revision: + disorder--mainline--0.1--patch-140 + + * acinclude.m4: new RJK_REQUIRE_PCRE_UTF8 macro checks that PCRE was + built with UTF-8 support. + + modified files: + ChangeLog.d/disorder--mainline--0.1 acinclude.m4 configure.ac + + +2005-05-21 12:17:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-139 + + Summary: + config overview + Revision: + disorder--mainline--0.1--patch-139 + + * doc/disorder_config.5.in: add an overview section + + + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-05-15 20:02:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-138 + + Summary: + unchecked fclose + Revision: + disorder--mainline--0.1--patch-138 + + * progs/tracks.c: unchecked fclose() + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-05-15 19:59:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-137 + + Summary: + correct for multiple =build directories + Revision: + disorder--mainline--0.1--patch-137 + + * {arch}/=tagging-method: correct =build* exclusion + + modified files: + ChangeLog.d/disorder--mainline--0.1 {arch}/=tagging-method + + +2005-05-15 19:57:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-136 + + Summary: + typos + Revision: + disorder--mainline--0.1--patch-136 + + * doc/disorder_config.5.in: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-05-11 18:56:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-135 + + Summary: + add missing files + Revision: + disorder--mainline--0.1--patch-135 + + * lib/filepart.c, lib/filepart.c: missing files from previous commit. + Thought that wasn't supposed to be possible... + + new files: + lib/filepart.c lib/filepart.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-05-08 19:05:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-134 + + Summary: + results of trackname_ prefs appear in virtual filesystem + Revision: + disorder--mainline--0.1--patch-134 + + * lib/configuration.c: 'alias' directive; syntax-check collection root + * lib/regsub.c: REGSUB_REPLACE flag to just expand the substitution + string rather than substituting it into the subject string + * lib/trackname.c: compute track name parts from track name without + collection root. + * progs/rescan.c: recompute aliases as part of rescan. + * progs/tracks.c: support aliases, i.e. names that appear in the virtual + filesystem corresponding to the results of trackname_display_ prefs. + When they are in the same directory as their real file, only the alias + is visible. + + * README.upgrades: note 'ext' namepart and namepart changes + * debian/disorder.config: add 'ext' directive and update namepart + directives for new semantics + * doc/disorder_config.5.in: note that collection root is removed. + * examples/config.sample.in: add 'ext' directive and update namepart + directives for new semantics + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in lib/Makefile.am lib/configuration.c + lib/configuration.h lib/regsub.c lib/regsub.h lib/trackname.c + lib/trackname.h lib/vector.h progs/play.c progs/rescan.c + progs/tracks.c progs/tracks.h + + +2005-05-07 16:08:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-133 + + Summary: + 'disorder authorize' command to simplify adding users + Revision: + disorder--mainline--0.1--patch-133 + + * lib/configuration.c: export paths to subsiduary configuration files as + required. + * progs/disorder.c: 'authorize' command + * progs/authorize.c: implementation of 'authorize' command + * scripts/completion.bash: add 'authorize' command to list + * README: mention disorder authorize; alternative advice about + config.USER ownership. + * doc/disorder.1.in: document 'authorize' command + * doc/disorder_config.5.in: config.private is root:jukebox, not jukebox:* + according to README and debian/postinst; make disorder_config(5) + consistent. + + new files: + progs/authorize.c progs/authorize.h + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + doc/disorder.1.in doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/Makefile.am progs/disorder.c + scripts/completion.bash + + +2005-05-07 13:50:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-132 + + Summary: + more encoding safeguard text + Revision: + disorder--mainline--0.1--patch-132 + + * debian/templates (disorder/encoding): note in template text that you + can't guess the encoding. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/templates + + +2005-05-07 13:30:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-131 + + Summary: + notes about wrong fs encoding troubles + Revision: + disorder--mainline--0.1--patch-131 + + * README: emphasize need to get filesystem encoding right + * BUGS: note about difficulty of recovering from wrong filesystem + encoding. + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 README + + +2005-03-12 17:53:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-130 + + Summary: + missing file + Revision: + disorder--mainline--0.1--patch-130 + + * progs/Makefile.am: remember to distribute getopt.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + + +2005-03-12 17:51:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-129 + + Summary: + post 1.2 + Revision: + disorder--mainline--0.1--patch-129 + + * configure.ac: 1.2+dev + + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + + +2005-03-11 20:07:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-128 + + Summary: + release 1.2 + Revision: + disorder--mainline--0.1--patch-128 + + * configure.ac, debian/changelog, CHANGES: version number + * scripts/inst: run ldconfig + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog scripts/inst + + +2005-03-05 20:57:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-127 + + Summary: + build fix + Revision: + disorder--mainline--0.1--patch-127 + + * plugins/mad.c: quieten gcc 4 prerelease + + modified files: + ChangeLog.d/disorder--mainline--0.1 plugins/mad.c + + +2005-03-04 23:31:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-126 + + Summary: + fix broken @navigate@ + Revision: + disorder--mainline--0.1--patch-126 + + * progs/dcgi.c: fix path name parsing in @navigate@. Was always broken + but never showed up until memory allocation changes in patch-125. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-03-04 19:41:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-125 + + Summary: + disable gc for some utilities + Revision: + disorder--mainline--0.1--patch-125 + + * lib/mem.c: callers can disable gc too + * progs/cgimain.c, progs/trackname.c: no gc for web interface or + trackname utility. These are very short running programs. + * progs/disorderd.c: enable gc. Long running program. + * progs/disorder.c, progs/dump.c: enable gc. disorder(1) might run a + long time if log is used; dump.c allocates memory for every track which + soon goes unreachable. If there are very many tracks then not freeing + this memory along the way might consume excessive amounts. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mem.c lib/mem.h + progs/cgimain.c progs/disorder.c progs/disorderd.c + progs/dump.c progs/trackname.c + + +2005-03-04 19:22:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-124 + + Summary: + optional disabling of garbage collection for debug purposes + Revision: + disorder--mainline--0.1--patch-124 + + * lib/mem.c: allow garbage collection to be turned off, e.g. for use with + memory allocation checkers that don't play nicely with libgc. To turn + garbage collection off set the environment variable DISORDER_GC=no. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mem.c + + +2005-03-01 00:08:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-123 + + Summary: + tidying up + Revision: + disorder--mainline--0.1--patch-123 + + * lib/log.h: move log_output structure to log.c, since it is not needed + by callers any more. + * templates/disorder.css, templates/volume.html, + templates/stylesheet.html, scripts/htmlman: copyright date + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c lib/log.h + progs/dcgi.c progs/dcgi.h scripts/htmlman + templates/choosealpha.html templates/disorder.css + templates/sidebar.html templates/stylesheet.html + templates/volume.html + + +2005-02-27 19:43:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-122 + + Summary: + more spelling + Revision: + disorder--mainline--0.1--patch-122 + + * python/disorder.py.in: typo fixes + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + + +2005-02-27 19:41:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-121 + + Summary: + better python docs + Revision: + disorder--mainline--0.1--patch-121 + + * python/disorder.py.in: more verbose docs + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + templates/help.html + + +2005-02-27 19:21:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-120 + + Summary: + more 'tooltips' + Revision: + disorder--mainline--0.1--patch-120 + + * templates/playing.html: TITLE attributes for links and buttons + * templates/options.labels: values for the above attributes + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + templates/playing.html + + +2005-02-27 19:05:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-119 + + Summary: + typo fixes + Revision: + disorder--mainline--0.1--patch-119 + + * README.upgrades: typo fixes + * CHANGES: typo fixes + * doc/disorder_config.5.in: typo fixes + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.upgrades + doc/disorder_config.5.in + + +2005-02-27 18:24:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-118 + + Summary: + remove annoying hang in tkdisorder + Revision: + disorder--mainline--0.1--patch-118 + + * python/tkdisorder: perform initial fill of recent window (or whatever) + in a background thread, to avoid blocking the rest of the UI. We set + the cursor to 'watch' for the duration. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-02-26 20:36:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-117 + + Summary: + debian fixes for new web arrangements + Revision: + disorder--mainline--0.1--patch-117 + + * debian/options.debian: this becomes a debian-specific version of + /etc/disorder/options, with alternative URLs for static content. It's + marked as a conffile but the user would be better off putting their + changes into options.user. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/conffiles + debian/options.debian debian/rules.m4 + + +2005-02-26 18:46:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-116 + + Summary: + simpler static content configuration + Revision: + disorder--mainline--0.1--patch-116 + + * templates/options.labels: images.* and links.* are the full URL + (possibly relative to the disorder cgi base URL). + * templates/playing.html, templates/volume.html, templates/recent.html, + templates/help.html, templates/choose.html: fix image links + * templates/stylesheet.html: fix stylesheet URL + * doc/disorder_config.5.in: document URL changes + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + templates/choose.html templates/help.html + templates/options.labels templates/playing.html + templates/recent.html templates/stylesheet.html + templates/volume.html + + +2005-02-24 22:58:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-115 + + Summary: + static (non-embedded) stylesheet + Revision: + disorder--mainline--0.1--patch-115 + + * templates/disorder.css: copied from contents of stylesheet.html + * templates/stylesheet.html: just link to disorder.css + * templates/options.labels: links.css label identifies stylesheet + + new files: + templates/disorder.css + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/Makefile.am templates/options.labels + templates/stylesheet.html + + +2005-02-20 19:29:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-114 + + Summary: + link/image title attributes + Revision: + disorder--mainline--0.1--patch-114 + + * templates/options.labels: new *.*verbose labels used for informative + TITLE attributes on various links and images + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choose.html + templates/credits.html templates/options.labels + templates/playing.html templates/recent.html + templates/topbar.html templates/volume.html + + +2005-02-20 18:34:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-113 + + Summary: + more flexible choice of graphics + Revision: + disorder--mainline--0.1--patch-113 + + * templates/options.labels: individual labels for each graphic. + Currently everything has to be relative to url.static which might not + be ideal. + * templates/playing.html, templates/choose.html, templates/help.html, + templates/recent.html, templates/volume.html: use new labels for + graphics. We discard the width and height attributes which is a shame + but most of the time browsers will have a copy already so it shouldn't + be too problematic. + * doc/disorder_config.5.in: document new labels + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + templates/choose.html templates/help.html + templates/options.labels templates/playing.html + templates/recent.html templates/volume.html + + +2005-02-20 12:39:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-112 + + Summary: + missing bit of patch-107 + Revision: + disorder--mainline--0.1--patch-112 + + * templates/help.html: forgot to includ ethe menu end file in patch-107 + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/help.html + + +2005-02-20 12:35:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-111 + + Summary: + definitions moved from macros to library + Revision: + disorder--mainline--0.1--patch-111 + + * lib/defs.c: make macros defined by configure (VERSION, PKGCONFDIR, etc) + available as library symbols, rather than repeating them everywhere + they are use. + + new files: + lib/defs.c lib/defs.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/configuration.c lib/configuration.h lib/plugin.c + progs/cgi.c progs/dcgi.c progs/disorder.c progs/disorderd.c + progs/dump.c progs/server.c progs/trackname.c + + +2005-02-19 23:48:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-110 + + Summary: + fix search facility + Revision: + disorder--mainline--0.1--patch-110 + + * progs/dcgi.c: unbreak the search facility, which didn't return all the + results for an artist (or album) which contains more than one hit. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-02-19 23:27:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-109 + + Summary: + missing bits from previous changes + Revision: + disorder--mainline--0.1--patch-109 + + * scripts/htmlman: make generated web pages use the new menu + infrastructure. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/htmlman + + +2005-02-19 23:18:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-108 + + Summary: + revert clumsiness + Revision: + disorder--mainline--0.1--patch-108 + + * templates/choose.html: accidentally reverted class change in patch-107. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choose.html + + +2005-02-19 23:10:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-107 + + Summary: + flexible menu infrastructure, new default menu layout + Revision: + disorder--mainline--0.1--patch-107 + + * progs/dcgi.c: @action@ expansion to make determining the current action + easier. + + * templates/options.labels: default 'menu' label to 'topbar' + + * doc/disorder_config.5.in: document 'menu' label and @action@ expansion + * templates/help.html: updated for the above changes + + * templates/topbar.html: new default menu template + * templates/topbarend.html: menu tempates now have a closing half too, + where the credits are output from. + * templates/stylesheet.html: formatting rules for topbar; make title + bigger to stand out against (rather big and friendly) menu items. + + * templates/sidebar.html: open the content div from the sidebar template + * templates/sidebarend.html: final half of sidebar template closes the + content div as well as outputting the credits. + + new files: + templates/sidebarend.html templates/topbar.html + templates/topbarend.html + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/Makefile.am + templates/about.html templates/choose.html + templates/choosealpha.html templates/help.html + templates/options.labels templates/playing.html + templates/prefs.html templates/recent.html + templates/search.html templates/sidebar.html + templates/stylesheet.html templates/volume.html + + +2005-02-19 22:02:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-106 + + Summary: + more powerful management page + Revision: + disorder--mainline--0.1--patch-106 + + * templates/playing.html: put volume control into management page + * templates/options.labels: new playing.volume label; + playing.{random,playing} labels now contain the colon formerly in the + template (for greater flexibility). + * doc/disorder_config.5.in: document playing.volume (and playing.playing + which was formerly missing) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in templates/options.labels + templates/playing.html + + +2005-02-19 21:28:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-105 + + Summary: + volume control bug fixes + visual improvements + Revision: + disorder--mainline--0.1--patch-105 + + * progs/dcgi.c: make act_volume() redirect back to itself when a change + occurs, so that it's safe to reload volume pages + * templates/volume.html: use new graphics for up/down + * templates/stylesheet.html: center volume control + * templates/options.labels: volume.{left,right} set to null on the + assumption that user can deduce that the left and rights input boxes + control the left and right speakers respectively + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + templates/options.labels templates/stylesheet.html + templates/volume.html + + +2005-02-19 21:03:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-104 + + Summary: + more graphical improvements + Revision: + disorder--mainline--0.1--patch-104 + + * images/scratch.png, images/noscratch.png: cleaner scratch button + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/noscratch.png + images/scratch.png + + +2005-02-19 20:37:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-103 + + Summary: + tidy up graphics a little + Revision: + disorder--mainline--0.1--patch-103 + + * images/edit.png: truncate the pencil perpendicular to its long axis, + rather than according to the corner of the bounding square, to + eliminate the visually confusing corner. + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/edit.png + + +2005-02-19 20:32:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-102 + + Summary: + grey out inactive buttons + Revision: + disorder--mainline--0.1--patch-102 + + * progs/dcgi.c: @isfirst@ and @islast@ expansions report whether this is + the first or last (or other) iteration of a recursive template + expansion. + * templates/playing.html: 'grey out' up/down buttons for first/last track + in queue (since they don't do anything useful). + * doc/disorder_config.5.in: document @isfirst@/@islast@ + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c progs/dcgi.h templates/playing.html + + +2005-02-19 20:17:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-101 + + Summary: + ignore whitespace between expansion arguments + Revision: + disorder--mainline--0.1--patch-101 + + * progs/cgi.c: ignore whitespace after '}' and at the start and end of + unquoted template expansion args. + * doc/disorder_config.5.in: document new whitespace rules + * templates/playing.html: missing class attribute on an img + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/cgi.c templates/playing.html + + +2005-02-19 19:05:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-100 + + Summary: + graphical buttons in web interface + Revision: + disorder--mainline--0.1--patch-100 + + * README, README.upgrades: mention appearance of /static + * doc/disorder_config.5.in: document url.static + + * templates/choose.html: graphical button for edit + * templates/help.html: catch up with graphical buttons; tidy up a bit. + * templates/playing.html: graphic buttons for scratch/remove/up/down + * templates/recent.html: graphical button for edit + * templates/stylesheet.html: img.button class for new graphical buttons + + * images/*.png: new graphics + + * debian/options.debian: debian-specific location for static content + * debian/conffiles: options.debian is a conffile + * debian/rules.m4: install static content under /var/www + install options.debian + + * progs/play.h: update copyright date + * examples/Makefile.am: update copyright date + * progs/daemonize.c: update copyright date + + new files: + debian/options.debian images/.arch-ids/=id + images/.arch-ids/down.png.id images/.arch-ids/edit.png.id + images/.arch-ids/nodown.png.id + images/.arch-ids/noscratch.png.id images/.arch-ids/noup.png.id + images/.arch-ids/scratch.png.id images/.arch-ids/up.png.id + images/Makefile.am images/down.png images/edit.png + images/nodown.png images/noscratch.png images/noup.png + images/scratch.png images/up.png + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 Makefile.am README + README.upgrades configure.ac debian/Makefile.am + debian/autorules.m4 debian/conffiles debian/rules.m4 + doc/disorder_config.5.in examples/Makefile.am + progs/daemonize.c progs/play.h templates/choose.html + templates/help.html templates/playing.html + templates/recent.html templates/stylesheet.html + + new directories: + images images/.arch-ids + + +2005-02-19 14:16:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-99 + + Summary: + restore debian/changelog to proper name + Revision: + disorder--mainline--0.1--patch-99 + + * debian/changelog: rename back to the standard name. I can't quite see + from the change history why it was changelog.Debian in the first place. + Hopefuly nothing will break. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/Makefile.am + debian/autorules.m4 + + renamed files: + debian/changelog.Debian + ==> debian/changelog + + +2005-02-18 20:49:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-98 + + Summary: + docs catch up with changes below + Revision: + disorder--mainline--0.1--patch-98 + + * doc/disorder_protocol.5.in: document new 'quitting' state + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + + +2005-02-18 20:27:06 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-97 + + Summary: + mention previous bug fix + Revision: + disorder--mainline--0.1--patch-97 + + * CHANGES: mention previous bug fix + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-02-18 20:15:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-96 + + Summary: + add current track to recently played list on server shutdown + Revision: + disorder--mainline--0.1--patch-96 + + When the recently played list was not preserved across restarts this was + unnecessary. It was left out when it started being preserved. + + * progs/play.c: when quitting, explicitly call player_finished() to get + the track added to the recently played list (and do notifications etc) + * progs/state.c: tell playing module we're quitting rather than just + disabling playing and expecting it to guess + * lib/queue.h: new track state for a track terminated by server shutdown + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/queue.c lib/queue.h + progs/dcgi.c progs/play.c progs/play.h progs/state.c + + +2005-02-18 16:51:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-95 + + Summary: + disorder.monitor python class + Revision: + disorder--mainline--0.1--patch-95 + + * python/disorder.py.in: disorder.monitor class provides cooked access to + event log + * examples/disorder-log: sketch example of using disorder.monitor + + * CHANGES: mention event log changes + + new files: + examples/disorder-log + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + examples/Makefile.am python/disorder.py.in + + +2005-02-18 15:56:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-94 + + Summary: + event log interface + Revision: + disorder--mainline--0.1--patch-94 + + * lib/eventlog.c: new event log interface for sending messages to clients + that issue the 'log' command + * lib/queue.c: event log queue, moved, removed, recent-* messages + * progs/play.c: event log completed, scratched, failed, playing messages + * progs/disorder.c: mention event log in command help + * progs/server.c: use event log interface for 'log' command + + * lib/log.c: don't send log messages to clients; logging interface is + simplified slightly. + * progs/daemonize.c: following simplified logging interface + + * doc/disorder_protocol.5.in: document event log + + * lib/configuration.c: remove left-over debugging printf, oops! + * progs/server.c: set SO_REUSEADDR so we don't have to cool off a bit + before binding to a TCP socket again. + + new files: + lib/eventlog.c lib/eventlog.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_protocol.5.in lib/Makefile.am lib/configuration.c + lib/log.c lib/log.h lib/queue.c progs/daemonize.c + progs/disorder.c progs/play.c progs/server.c + python/disorder.py.in + + +2005-02-17 00:02:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-93 + + Summary: + put README on the web + Revision: + disorder--mainline--0.1--patch-93 + + * scripts/dist: put README on the web + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/dist + + +2005-02-16 23:49:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-92 + + Summary: + update copyright dates for files changed in 2005 + Revision: + disorder--mainline--0.1--patch-92 + + * scripts/check: case independent check + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/copyright + lib/client.c lib/client.h lib/kvp.c lib/kvp.h lib/log.c + lib/log.h lib/table.h plugins/tracklength.c progs/Makefile.am + progs/cgi.h progs/cgimain.c progs/tracks.h scripts/check + templates/Makefile.am templates/choose.html + templates/playing.html templates/recent.html + templates/search.html + + +2005-02-16 23:40:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-91 + + Summary: + unicode search strings + Revision: + disorder--mainline--0.1--patch-91 + + * templates/search.html: use POST with multipart/form-data and an + explicit charset for the search form, rather than GET, so that we can + portably have non-ASCII characters in search terms. + back= URLs still use GET, but we can interpret query strings that + encode non-ASCII characters however we choose, and we choose UTF-8. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/search.html + + +2005-02-16 23:13:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-90 + + Summary: + configurable scratch signal + Revision: + disorder--mainline--0.1--patch-90 + + * lib/configuration.c: new 'signal' configuration item controls the + signal that will be used to interrupt players. Default SIGINT. + * progs/play.c: interrupt tracks using configured signal + + * lib/table.h: document TABLE_FIND() macro + + * doc/disorder_config.5.in: document 'signal' + * doc/disorder.3: document scratching interface. + + I've only listed signals from glibc's <bits/signal.h>; more could easily + be added. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h lib/table.h progs/play.c + + +2005-02-16 22:17:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-89 + + Summary: + make artist/album in playing/recent be links + Revision: + disorder--mainline--0.1--patch-89 + + * progs/dcgi.c: generalize @dirname@ and @basename@ + * templates/playing.html: make artist/album be links + * templates/recent.html: make artist/album be links + * doc/disorder_config.5.in: document extended @dirname@/@basename@ + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/playing.html + templates/recent.html + + +2005-02-16 21:32:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-88 + + Summary: + improved track selection extended to searching, debugged + Revision: + disorder--mainline--0.1--patch-88 + + The previous approach, of recording the directory to go back to, didn't + work for searching and didn't work properly where a regexp argument + reduced the set of tracks listed. The new approach is to specify the + exact URL to return to. + + * progs/dcgi.c: new 'back' CGI argument provides URL to redirect back to + after any operation except 'playing'. We use this to simply act_play(). + New @thisurl@ expansion gives the current page's URL, with a freshened + nonce. + + * templates/choose.html: use new 'back' behaviour; canonicalize name of + nonce arguments. + * templates/help.html: canonicalize name of nonce arguments. + * templates/search.html: use new 'back' behaviour as per choose.html + + * doc/disorder_config.5.in: document the above + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/choose.html + templates/help.html templates/search.html + + +2005-02-06 23:45:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-87 + + Summary: + typo + Revision: + disorder--mainline--0.1--patch-87 + + * doc/disorder_config.5.in: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-02-06 23:44:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-86 + + Summary: + improved track picking interface + Revision: + disorder--mainline--0.1--patch-86 + + This change follows watching a user pick several tracks fromg a single + album without the benefit of an 'open in new tab' client command (the + client they were using had the feature but they didn't know to use it). + + The new behaviour is that when you pick a track to be played you stay on + the same screen, with an indicator appearing beside the track to show + that it is on the queue. + + The old behaviour is still available, by editing templates/choose.html. + + * lib/kvp.c: urlencodestring() provides a more convenient interface for + getting url-encoded strings + * progs/dcgi.c: action=play now takes a back=directory argument to + redirect back into action=choose with a chosen directory. Used to stay + on the same page when choosing multiple tracks. + New @trackstate@ action reports current track state as 'playing', + 'queued' or '' if neither. + * templates/choose.html: use the above to stay on the same page when + choosing tracks while also giving visual feedback + * doc/disorder_config.5.in: document the above changes + * CHANGES: mention recent changes + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/kvp.c lib/kvp.h progs/dcgi.c + templates/choose.html + + +2005-02-06 20:39:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-85 + + Summary: + faster searching + Revision: + disorder--mainline--0.1--patch-85 + + * progs/tracks.c: track_search() retrieves word list for each track from + isearch database, which is rather fewer queries than reconstructing the + word list on the fly. + isearch_words() factors out common code for getting word lists from + isearch.db. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-02-06 18:37:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-84 + + Summary: + inverse search index + Revision: + disorder--mainline--0.1--patch-84 + + * progs/tracks.c: isearch.db is 'inverse' of search db, so we can keep + search db properly up to date. + DBCALL writes to debug output. + get/put write to debug output, report db name + opendb() centralizes database opening + track_getpart() wraps trackname_part(), doing database lookup too + * progs/server.c: use track_getpart() instead of manually checking + database and calling trackname_part(). + * lib/log.c: report file/line in debug output + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/configuration.c + lib/configuration.h lib/log.c lib/log.h progs/cgi.h + progs/server.c progs/tracks.c progs/tracks.h + + +2005-02-06 16:00:55 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-83 + + Summary: + internal documentation + Revision: + disorder--mainline--0.1--patch-83 + + * progs/tracks.c: describe current database tables + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-02-05 19:50:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-82 + + Summary: + tkdisorder improvements + Revision: + disorder--mainline--0.1--patch-82 + + * python/tkdisorder: use a canvas to display queue/recent listings, + showing artist, album and title. A bit slow. Could use timings, + submitters, etc too. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-02-05 18:18:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-81 + + Summary: + avoid accumulating overlarge recently played list + Revision: + disorder--mainline--0.1--patch-81 + + * lib/queue.c: recompute size of 'recent' list on loading it. Previously + it was not recounted but started again from 0 even if it wasn't empty; + so restarting the server effectively bumped the history variable + upwards by whatever the current size of the list was. + * {arch}/=tagging-method: ignore =obj directory + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/queue.c + {arch}/=tagging-method + + +2005-02-05 17:20:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-80 + + Summary: + make tkdisorder use 'part' command + Revision: + disorder--mainline--0.1--patch-80 + + * python/tkdisorder: replace local track parsing with calls to + disorder.client.part() + * python/disorder.py.in: DISORDER_PYTHON_DEBUG can be used to turn on + disorder.py debugging + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + python/tkdisorder + + +2005-02-05 15:33:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-79 + + Summary: + build fixes + Revision: + disorder--mainline--0.1--patch-79 + + * lib/types.h: work around broken apple header file by undefining PRI?MAX + values that GNU C rejects in pedantic mode. + * plugins/tracklength.c: cast strncmp() args to reliably match declaration + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/types.h + plugins/tracklength.c + + +2005-02-05 15:12:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-78 + + Summary: + fix trackname parsing for scratches + Revision: + disorder--mainline--0.1--patch-78 + + * progs/server.c: don't insist that the track exists, as this can break + scratches which aren't required to 'exist' (in this sense) + * progs/trackname.c: new internal program exposes trackname_part() for + testing purposes. + + new files: + progs/trackname.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + progs/server.c + + +2005-02-05 11:54:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-77 + + Summary: + move trackname parsing into server + Revision: + disorder--mainline--0.1--patch-77 + + Moving the trackname parsing into the server allows every client to get + the same logic without having to locally reimplement it. + + * debian/disorder.config: add namepart directives + * examples/config.sample.in: add namepart directives + * doc/disorder_config.5.in: document namepart directive; remove + trackname-part references. + + * lib/trackname.c: trackname_part() function does regexp substitution + based on namepart directives. + * lib/configuration.c: namepart directive + * lib/regsub.c: moved to lib + + * progs/server.c: implement 'part' command + * lib/client.c: add disorder_part() function; report transmitted commands + in debug output. + * doc/disorder_protocol.5.in: document 'part' command + + * progs/disorder.c: expose 'part' command to command line + * doc/disorder.1.in: document 'part' command + + * python/disorder.py.in: expose 'part' command to python + + * progs/cgi.c: abolish trackname-part + * progs/cgimain.c: environment variable DISORDER_DEBUG enables debugging. + * progs/dcgi.c: use disorder_part() to get track name parts + * templates/options: options.trackname is gone + * templates/options.trackname: gone + + * README.upgrades: new upgrading instructions mention this change. + * README: mention README.upgrades + + new files: + README.upgrades lib/trackname.c lib/trackname.h + + removed files: + templates/options.trackname + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 Makefile.am README + debian/disorder.config doc/disorder.1.in + doc/disorder_config.5.in doc/disorder_protocol.5.in + examples/config.sample.in lib/Makefile.am lib/client.c + lib/client.h lib/configuration.c lib/configuration.h + progs/Makefile.am progs/cgi.c progs/cgimain.c progs/dcgi.c + progs/disorder.c progs/server.c python/disorder.py.in + templates/Makefile.am templates/options + + renamed files: + progs/regsub.c + ==> lib/regsub.c + progs/regsub.h + ==> lib/regsub.h + + +2005-02-04 13:29:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-76 + + Summary: + preparations for the future + Revision: + disorder--mainline--0.1--patch-76 + + Prepare for move of DisOrder website to www.greenend.org/rjk/disorder. + Version number to 1.1+dev. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/copyright scripts/check scripts/dist + templates/about.html templates/credits.html + + +2005-02-03 22:26:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-75 + + Summary: + better ChangeLog.d handling + Revision: + disorder--mainline--0.1--patch-75 + + Distribute ChangeLog.d/* by mentioning it in EXTRA_DIST rather than + giving it its own Makefile. + + removed files: + ChangeLog.d/Makefile.am + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am configure.ac + + +2005-02-03 17:07:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-74 + + Summary: + automake bodge + Revision: + disorder--mainline--0.1--patch-74 + + * prepare: make sure Automake adds INSTALL, but doesn't fall over because + of missing ChangeLog. + + modified files: + ChangeLog.d/disorder--mainline--0.1 prepare + + +2005-02-03 00:02:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-73 + + Summary: + fix up dist script + Revision: + disorder--mainline--0.1--patch-73 + + * scripts/dist: install change history files + + + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/dist + + +2005-02-02 23:50:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-72 + + Summary: + release 1.1 + Revision: + disorder--mainline--0.1--patch-72 + + * configure.ac, CHANGES, debian/changelog.Debian: version number to 1.1 + * debian/control: Build-Depends on libdb4.3 then libdb4.2 + * scripts/makedeb: bzip2 + * scripts/check: script to scan for missing copyright dates + * lib/Makefile.am: tie libdisorder shared library version to disorder + version to avoid appearing to promise anything about ABI stability. + + Everything else: missing copyright dates. + + new files: + scripts/check + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog.Debian debian/config debian/control + doc/Makefile.am doc/disorder.3 doc/disorderd.8.in + lib/Makefile.am lib/charset.c lib/charset.h + lib/configuration.c lib/configuration.h lib/disorder.h + lib/hex.c lib/hex.h lib/plugin.c lib/plugin.h lib/queue.h + lib/types.h lib/utf8.h plugins/notify.c plugins/pick.c + progs/cgi.c progs/dcgi.c progs/play.c progs/server.c + progs/state.c progs/tracks.c python/Makefile.am + scripts/Makefile.am scripts/inst scripts/makedeb + scripts/sedfiles.make templates/credits.html + templates/help.html templates/prefs.html + + +2005-02-01 20:09:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-71 + + Summary: + more portability fixes + Revision: + disorder--mainline--0.1--patch-71 + + The aim is to build with CC=gcc -std=c89 -pedantic, though sadly it's not + practical to add -Werror in there as well. + + * configure.ac: turn on -Werror (if that's being used) when checking for + long long, since that's how it'll be used later on. In fact this only + rejects long long for compilers that warn but accept it, but it's + useful to be able to build without long long being available. + However, we disable -Werror if we can't convert function pointers to + object pointers without warning: DisOrder without dlsym() is + essentially hopeless. + * lib/basen.c: rename div to divide (libc conflict) + * lib/configuration.c: variable initializers are not allowed in C89 + * lib/types.h: include <stdlib.h> before redefing atoll() etc + Define PRIxMAX + * progs/server.c: use uintmax_t rather than explicit unsigned long long + to report times in client log messages. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac lib/basen.c + lib/configuration.c lib/types.h progs/server.c + + +2005-01-30 13:08:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-70 + + Summary: + cope with broken darwin headers + Revision: + disorder--mainline--0.1--patch-70 + + 'gcc-3.3 -std=c99' on Darwin turns declaration of strtoll and atoll off, + even though they are in the C99 library clause. + + * plugins/pick.c: cope with undeclared atoll + * lib/types.h: cope with undeclared strtoll/atoll + * configure.ac: check whether strtoll/atoll are declared; include + <sys/types.h> for ssize_t + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac lib/types.h + plugins/pick.c + + +2005-01-29 19:18:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-69 + + Summary: + libdb version checking makes more sense + Revision: + disorder--mainline--0.1--patch-69 + + * progs/tracks.c: reverse sense of libdb 4.2/4.3 tests; now DB42 means + just libdb 4.2, rather than DB43 meaning anything from 4.3 onwards. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 23:49:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-68 + + Summary: + fix botched libdb upgrade + Revision: + disorder--mainline--0.1--patch-68 + + * progs/tracks.c: 4.2 code was bust, restore + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 23:47:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-67 + + Summary: + improved distribution script + Revision: + disorder--mainline--0.1--patch-67 + + * scripts/webman: dead + * scripts/dist: new distribution script installs tarball and web man + pages in right place + + new files: + scripts/dist + + removed files: + scripts/webman + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-01-27 23:37:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-66 + + Summary: + abolish backward compatibility code + Revision: + disorder--mainline--0.1--patch-66 + + * progs/tracks.c: remove upgrade code for ancient database location and + content + * README: db 4.3 and gcc 3.4 work + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + progs/tracks.c + + +2005-01-27 23:20:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-65 + + Summary: + db version 4.3 support + Revision: + disorder--mainline--0.1--patch-65 + + We support 4.2 and 4.3 for now. 4.2 support can be expected to go away + at some point. + + * progs/tracks.c: db4.3 support + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 22:55:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-64 + + Summary: + spelling corrections + Revision: + disorder--mainline--0.1--patch-64 + + * python/disorder.py.in: minor typo fixes + * doc/disorder_config.5.in: typo fix + * progs/disorder.c: typo fix + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/disorder.c + python/disorder.py.in + + +2005-01-25 23:52:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-63 + + Summary: + C dialect strictness + Revision: + disorder--mainline--0.1--patch-63 + + * configure.ac: move _GNU_SOURCE define above library checks, since + <db.h> gets on badly with -std=c89 otherwise. + * lib/configuration.h: 'restrict' is a C99 keyword, don't use it as a + structure member name! + * lib/configuration.c: 'restrict' option field is now 'restrictions' + * progs/server.c: 'restrict' option field is now 'restrictions' + * progs/dcgi.c: 'restrict' option field is now 'restrictions' + * progs/cgi.c: remove extraneous semicolon + * lib/plugin.c: cast to keep gcc --std=c99 -pedantic happy + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/configuration.c lib/configuration.h lib/plugin.c + progs/cgi.c progs/dcgi.c progs/server.c + + +2005-01-25 18:35:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-62 + + Summary: + completion.bash + Revision: + disorder--mainline--0.1--patch-62 + + * scripts/completion.bash: renamed from disorder-bash-completion. + 'disorder' will already be in the path since it is installed to + pkgdatadir. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + scripts/Makefile.am + + renamed files: + scripts/disorder-bash-completion + ==> scripts/completion.bash + + +2005-01-24 19:49:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-61 + + Summary: + bash completion + Revision: + disorder--mainline--0.1--patch-61 + + * scripts/disorder-bash-completion: bash completion for disorder and + disorderd + + new files: + scripts/disorder-bash-completion + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + scripts/Makefile.am + + +2005-01-24 01:47:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-60 + + Summary: + more UTF-8 testing stuff + Revision: + disorder--mainline--0.1--patch-60 + + * lib/charset.c: ucs4cmp, used in testing + * lib/test.c: fix broken UTF-8 testing + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/charset.h lib/test.c + + +2005-01-24 01:42:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-59 + + Summary: + more tests + Revision: + disorder--mainline--0.1--patch-59 + + * lib/charset.c: new ucs42utf8 function, currently only used by tests + * lib/test.c: test hex codec; ucs42utf8; casefold; check the basic + character set looks like ASCII. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/charset.h lib/test.c + + +2005-01-23 19:17:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-58 + + Summary: + eliminate bogus diagnostics + Revision: + disorder--mainline--0.1--patch-58 + + * lib/hex.c: prevoius change to unhexdigit() produced an error on every + call. Oops. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/hex.c + + +2005-01-23 19:08:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-57 + + Summary: + base64 corrections + Revision: + disorder--mainline--0.1--patch-57 + + * lib/test.c: MIME base64 tests + * lib/mime.c: corrected base64 parsing + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mime.c lib/test.c + + +2005-01-23 18:18:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-56 + + Summary: + quoted-printable tests and fixes + Revision: + disorder--mainline--0.1--patch-56 + + * lib/test.c: a few MIME decoding tests + * lib/hex.c: quiet hex digit conversion + * lib/mime.c: corrected decoding of quoted-printable; + eliminate bogus diagnostics + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/hex.c + lib/hex.h lib/mime.c lib/test.c + + +2005-01-23 17:49:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-55 + + Summary: + tests, further UTF-8 improvements + Revision: + disorder--mainline--0.1--patch-55 + + * lib/test.c: tests for UTF-8 decoding and validation + * lib/utf8.h: corrected again, sigh + * lib/Makefile.am: run library tests at build time (not that there are + many of them) + + new files: + lib/test.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am lib/utf8.h + + +2005-01-19 23:09:55 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-54 + + Summary: + finally correct UTF-8 checking + Revision: + disorder--mainline--0.1--patch-54 + + * lib/utf8.h: check for invalid UTF-8 sequences more strictly + parse [U+10000,U+10FFFF] correctly(!) + * lib/utf8.c: validutf8() function checks a string + * progs/cgi.c: use validutf8() instead of wrongly(!) checking manually. + + new files: + lib/utf8.c + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/utf8.h progs/cgi.c + + +2005-01-18 20:49:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-53 + + Summary: + stricter CGI arg checking + Revision: + disorder--mainline--0.1--patch-53 + + * progs/cgi.c: UTF-8 checking of CGI arguments + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/cgi.c + + +2005-01-17 23:39:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-52 + + Summary: + make prefs.html self-consistent + Revision: + disorder--mainline--0.1--patch-52 + + * templates/prefs.html: even/odd styles count from 0, not 1 l-) + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/prefs.html + + +2005-01-17 23:26:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-51 + + Summary: + base64/qp support in MIME parsing + Revision: + disorder--mainline--0.1--patch-51 + + * lib/mime.c: base64 and qp support for MIME body parts. mime_header() + becomes mime_parse() and promises do deal with + content-transfer-encoding for you. + * progs/cgi.c: track mime.c changes + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mime.c lib/mime.h + progs/cgi.c + + +2005-01-17 22:31:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-50 + + Summary: + use multipart/form-data for preference submissions + Revision: + disorder--mainline--0.1--patch-50 + + * progs/cgi.c: accept multipart/form-data POST data. Note that the + character set is assumed to be UTF-8. + * lib/mime.c,: enough MIME support for multipart/form-data support. We + don't do QP or BASE64 yet(!) + * templates/prefs.html: use multi-part/form-data submission for + preference updates. + + new files: + lib/mime.c lib/mime.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/cgi.c templates/prefs.html + + +2005-01-16 19:57:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-49 + + Summary: + python administrivia + Revision: + disorder--mainline--0.1--patch-49 + + * doc/Makefile.am: ship tkdisorder.1 + * doc/tkdisorder.1: refer to 'pydoc disorder' + * doc/disorder.1.in: refer to 'pydoc disorder' + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + doc/disorder.1.in doc/tkdisorder.1 + + +2005-01-16 16:25:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-48 + + Summary: + tkdisorder administrivia + Revision: + disorder--mainline--0.1--patch-48 + + * python/disorder.py.in: copyright message, version number + * python/tkdisorder: copyright message, display version + * scripts/sedfiles.make: version number substitution + * doc/tkdisorder.1: document tkdisorder + + new files: + doc/tkdisorder.1 + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + python/Makefile.am python/disorder.py.in python/tkdisorder + scripts/sedfiles.make + + +2005-01-16 15:20:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-47 + + Summary: + more user-friendly "cooked" preferences interface + Revision: + disorder--mainline--0.1--patch-47 + + * templates/prefs.html: cooked preferences; wider input boxes + * templates/options.labels: new labels for cooked prefs + * templates/help.html: describe cooked preferences interface + * progs/dcgi.c: prefs is now a proper action, @prefs@ just iterates over + the set preferences. @pref@ added. @part@ can now take a track name. + * doc/disorder_config.5.in: document new/modified expansions and action + * CHANGES: mention cooked prefs + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/help.html + templates/options.labels templates/prefs.html + + +2005-01-12 23:24:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-46 + + Summary: + preserve recently played list across server restarts + Revision: + disorder--mainline--0.1--patch-46 + + * lib/queue.c: save/restore recently-played list + * progs/state.c: restore recently-played list at startup + * progs/play.c: save recently-played list after modifying it + * doc/disorderd.8.in: mention new file + * doc/disorder_config.5.in: recent list no longer nuked at startup + * CHANGES: mention the change + + A better approach might be for the mutating queue_*() functions to set a + flag, which is then queried each time round the event loop to determine + when a save is required. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in doc/disorderd.8.in lib/queue.c + lib/queue.h progs/disorder.c progs/play.c progs/state.c + + +2005-01-04 19:56:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-45 + + Summary: + further tkdisorder improvements + Revision: + disorder--mainline--0.1--patch-45 + + * python/tkdisorder: break up QueueWidget into TrackListWidget and + {Queue,Recent}Widget, replacing callback with subclassing. + Add title to main window. + Be more careful about using clients from the right thread. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 16:30:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-44 + + Summary: + 'recent' button + Revision: + disorder--mainline--0.1--patch-44 + + * python/tkdisorder: 'recent' button pops up a window with the last N + tracks. Generalized QueueWidget a bit. MonitorStateThread can have + widgets added to and removed from its notification list. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 14:39:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-43 + + Summary: + scratch button for tkdisorder + Revision: + disorder--mainline--0.1--patch-43 + + * python/tkdisorder: Quit and Scratch buttons + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 14:21:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-42 + + Summary: + more efficient tkdisorder + Revision: + disorder--mainline--0.1--patch-42 + + We could make it more efficient yet by ignoring irrelevant log messages. + + * python/disorder.py.in: warning about disorder.log() return value + * python/tkdisorder: Use disorder.log() to watch for server state changes + rather than plling. Also separate clients for separate threads rather + than locking a single client. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + python/tkdisorder + + +2005-01-03 13:51:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-41 + + Summary: + notifications and logs for queue and playing state changes + Revision: + disorder--mainline--0.1--patch-41 + + * lib/disorder.h: new notify plugin calls report when a track is moved or + removed in the queue + * doc/disorder.3: document new notify calls + * lib/plugin.c: stubs for new notify calls + * lib/plugin.c: stubs for new notify calls + * plugins/notify.c: empty implementations of new calls + + * lib/queue.c: call new notify plugin calls + log queue changes + * progs/play.c: log playing, scratchin and completion of tracks + unconditionally log playing status + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/plugin.c lib/plugin.h lib/queue.c + lib/queue.h plugins/notify.c progs/play.c progs/server.c + + +2005-01-03 00:26:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-40 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-40 + + More copyright dates. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/about.html + + +2005-01-03 00:25:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-39 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-39 + + It's 2005 now. + + modified files: + ChangeLog.d/Makefile.am ChangeLog.d/disorder--mainline--0.1 + Makefile.am README configure.ac debian/rules.m4 + doc/disorder.1.in doc/disorder_config.5.in + doc/disorder_protocol.5.in lib/basen.c lib/basen.h lib/queue.c + prepare progs/disorder.c + + +2005-01-03 00:21:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-38 + + Summary: + Typo fixes + Revision: + disorder--mainline--0.1--patch-38 + + Trivial corrections to various bits of documentation. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README doc/disorder.1.in + doc/disorder_config.5.in doc/disorder_protocol.5.in + + +2005-01-02 23:56:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-37 + + Summary: + automake --foreign + Revision: + disorder--mainline--0.1--patch-37 + + Reduce Automake strictness and delete quietening files. + + removed files: + .arch-ids/ChangeLog.id AUTHORS ChangeLog NEWS + + modified files: + ChangeLog.d/disorder--mainline--0.1 prepare + + +2005-01-02 21:31:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-36 + + Summary: + documentation trivia + Revision: + disorder--mainline--0.1--patch-36 + + * CHANGES: describe recent changes + * doc/disorder.1.in: unify remove/move language and caveat + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + + +2005-01-02 21:16:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-35 + + Summary: + track ID improvements + Revision: + disorder--mainline--0.1--patch-35 + + * progs/disorder.c: report track id in output + * lib/basen.c: arbitrary base printer + * lib/queue.c: queue IDs are now base-62 not hex, making them much + shorter. The serial number is made the most significant word to + increase diversity of adjacently generated IDs. + + new files: + ChangeLog lib/basen.c lib/basen.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/queue.c progs/disorder.c + + +2005-01-02 20:42:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-34 + + Summary: + ChangeLog.d directory + Revision: + disorder--mainline--0.1--patch-34 + + Move changelogs into ChangeLog.d directory. + + * CHANGES: note movement of ChangeLogs + + new files: + .arch-ids/ChangeLog.id ChangeLog ChangeLog.d/.arch-ids/=id + ChangeLog.d/Makefile.am + + modified files: + CHANGES ChangeLog Makefile.am configure.ac debian/rules.m4 + + renamed files: + ChangeLog + ==> ChangeLog.d/disorder--mainline--0.1 + ChangeLog.cvs + ==> ChangeLog.d/cvs--ChangeLog + + new directories: + ChangeLog.d ChangeLog.d/.arch-ids + + +2005-01-02 20:27:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-33 + + Summary: + expose track movement to the command line interface + Revision: + disorder--mainline--0.1--patch-33 + + * progs/disorder.c: implement 'move' command + * doc/disorder.1.in: document 'move' command + + modified files: + ChangeLog doc/disorder.1.in progs/disorder.c + + +2004-12-30 16:21:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-32 + + Summary: + sort 'The Beatles' under B not T + Revision: + disorder--mainline--0.1--patch-32 + + * progs/dcgi.c: filter according to regexp in the web interface rather + than in the server. This is slower but can actually be made correct. + This might be a problem for sites with very large numbers of (in + particular) top-level directories. + part() is renamed tracknamepart() as a no-longer-required build fix, + but it's also a bit clearer what it does, so I left it in. + + * BUGS: remove fixed bugs. + + modified files: + BUGS ChangeLog progs/dcgi.c + + +2004-12-30 14:42:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-31 + + Summary: + strip out libdb backwards compatibility cruft + Revision: + disorder--mainline--0.1--patch-31 + + Anything before libdb 4.2 is now regarded obsolete, and thus the grotty + configure and preprocessor logic to support older versions is removed. + + * progs/dbfixup.h: removed + * progs/tracks.c: strip out db version fixup hackery. Now we only + support libdb 4.2 (and possibly later). + * README: note that libdb 4.1 and earlier won't work + * progs/Makefile.am: dbfixup.h is gone + * configure.ac: remove obsolete libdb feature tests + + + removed files: + progs/dbfixup.h + + modified files: + ChangeLog README configure.ac progs/Makefile.am progs/tracks.c + + +2004-12-30 14:18:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-30 + + Summary: + documentation fixups + Revision: + disorder--mainline--0.1--patch-30 + + * scripts/webman: html man pages are in doc directory now + * scripts/htmlman: fix title of unmunged man pages + * configure.ac: version number to 1.0+dev + + + modified files: + ChangeLog configure.ac scripts/htmlman scripts/webman + + +2004-12-30 12:55:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-29 + + Summary: + release 1.0 + Revision: + disorder--mainline--0.1--patch-29 + + * README: list development-time dependencies + * scripts/Makefile.am: ship sedfiles.make + * debian/rules.m4: disorder.init and disorder.cgi moved + * debian/autorules.m4: debian.changelog moved + + + modified files: + CHANGES ChangeLog README configure.ac debian/autorules.m4 + debian/changelog.Debian debian/rules.m4 scripts/Makefile.am + scripts/makedeb + + +2004-12-05 11:21:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-28 + + Summary: + more post-split tidy-up + Revision: + disorder--mainline--0.1--patch-28 + + * scripts/inst: disorder.cgi is in progs/ now + + modified files: + ChangeLog scripts/inst + + +2004-12-04 17:36:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-27 + + Summary: + more most-split tidy-ups + Revision: + disorder--mainline--0.1--patch-27 + + * debian/changelog.Debian: a saner name + + modified files: + ChangeLog debian/Makefile.am debian/autorules.m4 + + renamed files: + debian/ChangeLog + ==> debian/changelog.Debian + + +2004-12-04 16:18:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-26 + + Summary: + make python optional + Revision: + disorder--mainline--0.1--patch-26 + + Python support should be optional. Implement this by suppressing + recursion into python/ if no Python interpreter is found. Not tested on + a system without Python, please report success/failure in such cases. + + modified files: + ChangeLog Makefile.am README configure.ac + + +2004-12-04 16:06:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-25 + + Summary: + fix up build instructions + Revision: + disorder--mainline--0.1--patch-25 + + * BUGS: note that darwin can't do volume control + * Makefile.am: ship README.darwin + * README: link to mailing lists + catch up with directory split + * README.darwin: caveats + + modified files: + BUGS CHANGES ChangeLog Makefile.am README README.darwin + + renamed files: + inst + ==> scripts/inst + webman + ==> scripts/webman + + +2004-12-04 15:26:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-24 + + Summary: + post-directory-split tidy-ups + Revision: + disorder--mainline--0.1--patch-24 + + * scripts/sedfiles.make: de-dupe seddery into a single file + * debian/autorules.m4: catch up with filename case change + (workaround for automake wackiness) + + new files: + scripts/sedfiles.make + + modified files: + ChangeLog debian/autorules.m4 doc/Makefile.am + examples/Makefile.am python/Makefile.am + + +2004-12-04 13:50:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-23 + + Summary: + split into subdirectories + Revision: + disorder--mainline--0.1--patch-23 + + Almost everything moves out of the root. The new directory structure is: + templates/ web interface template files + scripts/ scripts used in the build process + lib/ libdisorder + progs/ the server and the front-end programs + doc/ man pages + plugins/ the standard plugins + debian/ debian build files + sounds/ standard scratch sounds + python/ python support code + examples/ config/init examples, etc + config.aux/ autotools auxilary files + + new files: + config.aux/.arch-ids/=id doc/.arch-ids/=id doc/Makefile.am + examples/.arch-ids/=id examples/Makefile.am lib/.arch-ids/=id + lib/Makefile.am progs/.arch-ids/=id progs/Makefile.am + python/.arch-ids/=id python/Makefile.am scripts/.arch-ids/=id + scripts/Makefile.am + + modified files: + ChangeLog Makefile.am configure.ac debian/Makefile.am + plugins/Makefile.am prepare + + renamed files: + addr.c + ==> lib/addr.c + addr.h + ==> lib/addr.h + api-client.c + ==> progs/api-client.c + api-client.h + ==> progs/api-client.h + api-server.c + ==> progs/api-server.c + api.c + ==> progs/api.c + asprintf.c + ==> lib/asprintf.c + authhash.c + ==> lib/authhash.c + authhash.h + ==> lib/authhash.h + casefold.h + ==> lib/casefold.h + cgi.c + ==> progs/cgi.c + cgi.h + ==> progs/cgi.h + cgimain.c + ==> progs/cgimain.c + charset.c + ==> lib/charset.c + charset.h + ==> lib/charset.h + client.c + ==> lib/client.c + client.h + ==> lib/client.h + config.sample.in + ==> examples/config.sample.in + configuration.c + ==> lib/configuration.c + configuration.h + ==> lib/configuration.h + daemonize.c + ==> progs/daemonize.c + daemonize.h + ==> progs/daemonize.h + dbfixup.h + ==> progs/dbfixup.h + dcgi.c + ==> progs/dcgi.c + dcgi.h + ==> progs/dcgi.h + debian/changelog + ==> debian/ChangeLog + disorder-dump.8.in + ==> doc/disorder-dump.8.in + disorder.1.in + ==> doc/disorder.1.in + disorder.3 + ==> doc/disorder.3 + disorder.c + ==> progs/disorder.c + disorder.h + ==> lib/disorder.h + disorder.init.in + ==> examples/disorder.init.in + disorder.py.in + ==> python/disorder.py.in + disorder_config.5.in + ==> doc/disorder_config.5.in + disorder_protocol.5.in + ==> doc/disorder_protocol.5.in + disorderd.8.in + ==> doc/disorderd.8.in + disorderd.c + ==> progs/disorderd.c + dump.c + ==> progs/dump.c + event.c + ==> lib/event.c + event.h + ==> lib/event.h + fprintf.c + ==> lib/fprintf.c + hex.c + ==> lib/hex.c + hex.h + ==> lib/hex.h + htmlman + ==> scripts/htmlman + inputline.c + ==> lib/inputline.c + inputline.h + ==> lib/inputline.h + kvp.c + ==> lib/kvp.c + kvp.h + ==> lib/kvp.h + log-impl.h + ==> lib/log-impl.h + log.c + ==> lib/log.c + log.h + ==> lib/log.h + makedeb + ==> scripts/makedeb + mem-impl.h + ==> lib/mem-impl.h + mem.c + ==> lib/mem.c + mem.h + ==> lib/mem.h + mixer.c + ==> lib/mixer.c + mixer.h + ==> lib/mixer.h + play.c + ==> progs/play.c + play.h + ==> progs/play.h + plugin.c + ==> lib/plugin.c + plugin.h + ==> lib/plugin.h + printf.c + ==> lib/printf.c + printf.h + ==> lib/printf.h + queue.c + ==> lib/queue.c + queue.h + ==> lib/queue.h + regsub.c + ==> progs/regsub.c + regsub.h + ==> progs/regsub.h + rescan.c + ==> progs/rescan.c + server.c + ==> progs/server.c + server.h + ==> progs/server.h + sink.c + ==> lib/sink.c + sink.h + ==> lib/sink.h + snprintf.c + ==> lib/snprintf.c + split.c + ==> lib/split.c + split.h + ==> lib/split.h + state.c + ==> progs/state.c + state.h + ==> progs/state.h + syscalls.c + ==> lib/syscalls.c + syscalls.h + ==> lib/syscalls.h + table.c + ==> lib/table.c + table.h + ==> lib/table.h + tkdisorder + ==> python/tkdisorder + tracks.c + ==> progs/tracks.c + tracks.h + ==> progs/tracks.h + types.h + ==> lib/types.h + unicodegc.h + ==> lib/unicodegc.h + utf8.h + ==> lib/utf8.h + vacopy.h + ==> lib/vacopy.h + vector.c + ==> lib/vector.c + vector.h + ==> lib/vector.h + words.c + ==> lib/words.c + words.h + ==> lib/words.h + wstat.c + ==> lib/wstat.c + wstat.h + ==> lib/wstat.h + + new directories: + config.aux config.aux/.arch-ids doc doc/.arch-ids examples + examples/.arch-ids lib lib/.arch-ids progs progs/.arch-ids + python python/.arch-ids scripts scripts/.arch-ids + + +2004-12-03 16:18:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-22 + + Summary: + missing from previous change + Revision: + disorder--mainline--0.1--patch-22 + + * dump.c: missing includes + * disorder.c: missing includes + + modified files: + ChangeLog disorder.c dump.c + + +2004-12-03 16:09:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-21 + + Summary: + fix garbage collection problems under Darwin + Revision: + disorder--mainline--0.1--patch-21 + + Call GC_init() at the start of each program. This is necesary on Darwin and + leaving it out led to crashes. + + * play.c: more idiomatic xmalloc + * README.darwin: where to get ogg123/mpg321. + + modified files: + ChangeLog README.darwin cgimain.c disorder.c disorderd.c + dump.c mem.c mem.h play.c + + +2004-12-03 13:15:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-20 + + Summary: + first pass at Darwin (Mac OS X) port + Revision: + disorder--mainline--0.1--patch-20 + + The code now builds, runs, and plays music (albeit rather tinnily on my + laptop's speakers), but has a tendency to crash e.g. if you scratch three + or four tracks in a row. + + Code changes: + + * asprintf.c: error-checking wrapper for byte_asprintf + * event.c: EPROTO might not exist + * mixer.c: don't know how to set the volume if we don't have + <sys/soundcard.h> + * printf.c: support 'q' length modifier as a synonym for 'll' + * server.c: Darwin's getpeername() doesn't reliably fill in sa_family, + so remember the protocol family ourselves. + * tracks.c: DBT.size is not size_t + pass length back from track_states() correctly + + Build system changes: + + * prepare: look in /sw for the benefit of finkified Darwin. + * Makefile.am: Abandoned the attempt to restrict exported symbols; it is + impractical to do portably. + * configure.ac: Use RJK_CHECK_LIB for iconv as AC_CHECK_LIB is not up to + the job. + * confgure.ac: If fdatasync() is not available use fsync() instead. + * Makefile.am: Only use getopt* sources if necessary, and disable -Werror + in that case (since the code doesn't compile cleanly + enough) + + new files: + README.darwin + + removed files: + disorder.vs + + modified files: + ChangeLog Makefile.am README addr.c asprintf.c cgi.c cgimain.c + client.c configuration.c configure.ac dcgi.c event.c mixer.c + plugin.c prepare printf.c printf.h queue.c server.c + sounds/slap.ogg state.c tracks.c wstat.c + + +2004-12-01 23:51:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-19 + + Summary: + DisOrder 0.13 + Revision: + disorder--mainline--0.1--patch-19 + + New release. + + modified files: + CHANGES ChangeLog configure.ac debian/changelog + + +2004-12-01 23:41:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-18 + + Summary: + fix crash on queue_find() of non-existent tracks + Revision: + disorder--mainline--0.1--patch-18 + + * queue.c: queue_find() was returning &qhead on error, which seriously + confuses its callers; for instance it could lead to the 'fake' + root node in the queue being removed, with hilarious + consequences when the queue was written back out. + Changed queue_find() to correctly return 0 on error. + + modified files: + ChangeLog queue.c + + +2004-10-31 13:56:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-17 + + Summary: + change version to 0.12+dev + Revision: + disorder--mainline--0.1--patch-17 + + 0.12 branch is disorder--release-0-12--0.1. + + modified files: + ChangeLog configure.ac + + +2004-10-31 13:50:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-16 + + Summary: + delete non-working configs/ directory + Revision: + disorder--mainline--0.1--patch-16 + + + removed files: + configs/.arch-ids/=id configs/disorder-0.12.arch + configs/disorder.arch + + modified files: + ChangeLog + + removed directories: + configs configs/.arch-ids + + +2004-10-31 13:34:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-15 + + Summary: + config for release 0.12 + Revision: + disorder--mainline--0.1--patch-15 + + Add configs for + disorder main development line + disorder-0.12 release 0.12 + + new files: + configs/.arch-ids/=id configs/disorder-0.12.arch + configs/disorder.arch + + modified files: + ChangeLog + + new directories: + configs configs/.arch-ids + + +2004-10-30 18:52:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-14 + + Summary: + release 0.12 + Revision: + disorder--mainline--0.1--patch-14 + + ChangeLog is now an arch changelog; old changes can be found in + ChangeLog.cvs. + + Release 0.12. + + new files: + ChangeLog + + modified files: + CHANGES Makefile.am configure.ac debian/changelog + debian/rules.m4 + + renamed files: + ChangeLog + ==> ChangeLog.cvs + + +2004-10-30 18:37:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-13 + + Summary: + tidy up disorder.py + Revision: + disorder--mainline--0.1--patch-13 + + Remove a bogus debugging print + + Add an example to the disorder.log() documentation + + modified files: + disorder.py.in + + +2004-10-30 18:33:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-12 + + Summary: + log command could crash server + Revision: + disorder--mainline--0.1--patch-12 + + Log output to a conn was never stopped even when the client no longer + wanted it. So not only would bogus log messages be sent if the conn was + kept open, once it was closed there would be a crash. + + Fixed by storing the log output in the conn and closing it at the right + point. + + modified files: + CHANGES server.c + + +2004-10-30 18:26:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-11 + + Summary: + support 'log' in python client + Revision: + disorder--mainline--0.1--patch-11 + + New disorder.log method supports log command. + + modified files: + disorder.py.in + + +2004-10-29 19:50:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-10 + + Summary: + more detailed dependencies table + Revision: + disorder--mainline--0.1--patch-10 + + List version numbers for build dependencies. + + modified files: + README + + +2004-10-29 19:39:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-9 + + Summary: + deprecate libdb before 4.2 + Revision: + disorder--mainline--0.1--patch-9 + + No code changes, but no promises regarding older libdb versions. + + modified files: + README + + +2004-10-29 19:38:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-8 + + Summary: + fix debian build rules + Revision: + disorder--mainline--0.1--patch-8 + + Cope with debian/rules being made outside ${srcdir}. + + modified files: + debian/Makefile.am debian/autorules.m4 + + +2004-10-29 15:15:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-7 + + Summary: + enable/disable from web templates + Revision: + disorder--mainline--0.1--patch-7 + + Add enable, disable, disable-now actions and @enabled@ expansion, and + documented them. The playing.html has an example usage in a comment, not + enabled because we find {enable,disable}-random much more useful in + practice. + + modified files: + dcgi.c disorder_config.5.in templates/options.labels + templates/playing.html + + +2004-10-28 23:05:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-6 + + Summary: + typo fix in error message + Revision: + disorder--mainline--0.1--patch-6 + + + modified files: + dcgi.c + + +2004-10-28 23:04:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-5 + + Summary: + add missing documentation + Revision: + disorder--mainline--0.1--patch-5 + + Describe various missing labels and the action= values. + + modified files: + disorder_config.5.in + + +2004-10-28 22:52:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-4 + + Summary: + add random play enable/disable buttons + Revision: + disorder--mainline--0.1--patch-4 + + @random-enabled@ expansion reports current state as a boolean + + random-enable and random-disable actions do the obvious thing + + New labels provide the text. playing.html template includes the buttons + in management mode. + + + modified files: + CHANGES dcgi.c disorder_config.5.in templates/help.html + templates/options.labels templates/playing.html + + +2004-10-28 21:04:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-3 + + Summary: + Fix file permissions mangled in import + Revision: + disorder--mainline--0.1--patch-3 + + Added executable bit trampled by my add-arch-tag script. + + modified files: + debian/config debian/postinst debian/postrm debian/prerm + htmlman inst makedeb prepare tkdisorder webman + + +2004-10-28 20:45:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-2 + + Summary: + Further error corrections for import + Revision: + disorder--mainline--0.1--patch-2 + + Sort out confusion over aclocal.m4/acinclude.m4 + + + new files: + acinclude.m4 + + removed files: + aclocal.m4 + + modified files: + {arch}/=tagging-method + + +2004-10-28 20:02:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-1 + + Summary: + Fix missing files in import + Revision: + disorder--mainline--0.1--patch-1 + + Fix =tagging-method to not make debian/*.m4 and configure.ac(!) precious + Add arch-tags to add new files. + + new files: + configure.ac debian/autorules.m4 debian/rules.m4 + + modified files: + {arch}/=tagging-method + + +2004-10-28 19:19:21 GMT Richard Kettlewell <rjk@greenend.org.uk> base-0 + + Summary: + import from cvs tag disorder--mainline--0_1--base-0 + Revision: + disorder--mainline--0.1--base-0 + + Add arch-tag lines to almost all files + 'arch add' binary files and files without known comment syntax: + sounds/scratch.ogg + sounds/slap.ogg + debian/conffiles + debian/templates + Omitted .cvsignore files. + + Make various generated files (whether made by automake, prepare, etc) + precious. + + new files: + AUTHORS BUGS CHANGES ChangeLog DESIGN2 Makefile.am NEWS README + README.streams TODO aclocal.m4 addr.c addr.h api-client.c + api-client.h api-server.c api.c asprintf.c authhash.c + authhash.h casefold.h cgi.c cgi.h cgimain.c charset.c + charset.h client.c client.h config.sample.in configuration.c + configuration.h daemonize.c daemonize.h dbfixup.h dcgi.c + dcgi.h debian/Makefile.am debian/README.Debian + debian/changelog debian/conffiles debian/config debian/control + debian/copyright debian/disorder.config debian/htaccess + debian/postinst debian/postrm debian/prerm debian/templates + disorder-dump.8.in disorder.1.in disorder.3 disorder.c + disorder.h disorder.init.in disorder.py.in disorder.vs + disorder_config.5.in disorder_protocol.5.in disorderd.8.in + disorderd.c dump.c event.c event.h fprintf.c hex.c hex.h + htmlman inputline.c inputline.h inst kvp.c kvp.h log-impl.h + log.c log.h makedeb mem-impl.h mem.c mem.h mixer.c mixer.h + play.c play.h plugin.c plugin.h plugins/Makefile.am + plugins/exec.c plugins/fs.c plugins/mad.c plugins/madshim.h + plugins/notify.c plugins/pick.c plugins/shell.c + plugins/tracklength.c prepare printf.c printf.h queue.c + queue.h regsub.c regsub.h rescan.c server.c server.h sink.c + sink.h snprintf.c sounds/Makefile.am sounds/scratch.ogg + sounds/slap.ogg split.c split.h state.c state.h syscalls.c + syscalls.h table.c table.h templates/Makefile.am + templates/about.html templates/choose.html + templates/choosealpha.html templates/credits.html + templates/help.html templates/options + templates/options.columns templates/options.labels + templates/options.trackname templates/options.transform + templates/playing.html templates/prefs.html + templates/recent.html templates/search.html + templates/sidebar.html templates/stdhead.html + templates/stylesheet.html templates/volume.html tkdisorder + tracks.c tracks.h types.h unicodegc.h utf8.h vacopy.h vector.c + vector.h webman words.c words.h wstat.c wstat.h + + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..401a14c --- /dev/null +++ b/Makefile.am @@ -0,0 +1,27 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +EXTRA_DIST=TODO CHANGES README.streams BUGS ChangeLog.d \ +README.upgrades README.client +SUBDIRS=@subdirs@ + +echo-distdir: + @echo $(distdir) +# arch-tag:7e0566bae866a64c2f998d4253581ce0 diff --git a/README b/README new file mode 100644 index 0000000..64d4940 --- /dev/null +++ b/README @@ -0,0 +1,334 @@ +DisOrder +======== + +This program is used to play random and chosen tracks from a collection of +digital audio files (for instance MP3 and OGG files). If you just set it going +it plays random tracks from your collection, but you can also ask for specific +tracks to be played, either via a command line program or a web interface, and +you can 'scratch' the current track. + +See CHANGES for details of recent changes to DisOrder. + +Currently it only runs on Linux. It could probably be ported to other UNIX +variants in some cases without too much effort. Things you will need: + +Build dependencies: + Name Tested Notes + libdb 4.3.21 4.2 and earlier won't work + libgc 6.3 + libvorbisfile 1.0.1 + libpcre 4.5 need UTF-8 support + libmad 0.15.1b + libgcrypt 1.2.0 + libao 0.8.6 + libasound 1.0.8 + Python 2.3 (optional) + GNU C 3.3, 3.4 + +"Tested" means I've built against that version; earlier or later versions will +often work too. + +Runtime dependencies: + * Players: + + ogg123 and mpg321 work for me, but you could potentially use others. + * Web server: + + Apache 1.3.x works for me, but anything that supports CGI and + authentication should be suitable. + +Development dependencies (only developers will need these): + Automake 1.9.4 AM_PATH_PYTHON not good enough in 1.7 + Autoconf 2.59 + Libtool 1.5.6 1.4 not good enough + Arch 1.3-1 + +Mailing lists: + http://www.chiark.greenend.org.uk/mailman/listinfo/sgo-software-discuss + - discussion of DisOrder (and other software), bug reports, etc + http://www.chiark.greenend.org.uk/mailman/listinfo/sgo-software-announce + - announcements of new versions of DisOrder + + +Installation +============ + + "This place'd be a paradise tomorrow, if every department had a supervisor + with a machine-gun" + +NOTE: If you are upgrading from an earlier version, see README.upgrades. + +1. Build the software. Do something like this: + + ./configure --sysconfdir=/etc --localstatedir=/var + make + + See INSTALL for more details about driving configure. The precise set of + options you pass to configure is up to you, if you like configuration being + in /usr/local/etc or wherever then that should work. + + If you only want to build a subset of DisOrder, specify one or more of the + following options: + --without-server Don't build server or web interface + --without-gtk Don't build GTK+ client (Disobedience) + --without-python Don't build Python support + + See README.client for setting up a standalone client. + +2. Install it. Most of the installation is done via the install target: + + make installdirs install + + The CGI interface has to be installed separately, and you must use Libtool + to install it. For instance: + + ./libtool --mode=install install -m 755 progs/disorder.cgi /usr/local/lib/cgi-bin/disorder + + Depending on how your system is configured you may need to link the disorder + libao driver into the right directory: + + ln -s /usr/local/lib/ao/plugins-2/libdisorder.so /usr/lib/ao/plugins-2/. + +3. Create a 'jukebox' user and group, with the jukebox group being the default + group of the jukebox user. The server will run as this user and group. + Check that this user can read your music files and write to the audio + device, e.g. by playing a track. The exact name doesn't matter, it could be + 'jukebox' or 'disorder' or 'fred' or whatever. + + Do not use a general-purpose user or group, you must create ones + specifically for DisOrder. + +4. Create /etc/disorder/config. Start from examples/config.sample and adapt it + to your own requirements. In particular, you should: + * edit the 'player' commands to reflect the software you have installed. + * edit the 'collection' command to identify the location(s) of your own + digital audio files. These commands also specify the encoding of + filenames, which you should be sure to get right as recovery from an + error here can be painful (see BUGS). + * edit the 'scratch' commands to supply scratch sounds (or delete them if + you don't want any). + * edit the 'trust' command to reflect the user the web interface will + eventually run as. + * edit the 'url' command to give the URL of the web interface. + * add or remove 'stopword' entries as necessary (these words won't take + part in track name searches from the web interface). + + See disorder_config(5) for more details. + +5. Create /etc/disorder/config.private. This should be readable only by the + jukebox group: + + touch /etc/disorder/config.private + chown root:jukebox /etc/disorder/config.private + chmod 640 /etc/disorder/config.private + + Set up a username and password for root, for example with line like this: + + allow root somepassword + + Use (for instance) pwgen(1) to create the password. DO NOT use your root + password - this is a password to give root access to the server, not to give + access to the root login. + + See disorderd(8) and disorder_config(5) for more details. + +6. Make sure the server is started at boot time. On many Linux systems, + examples/disorder.init should be more or less suitable; install it in + /etc/init.d, adapting it as necessary, and make appropriate links from + /etc/rc[0-6].d. If you have a BSD style init then you are on your own. + +7. Make sure the state directory (/var/disorder or /usr/local/var/disorder or + as determined by configure) exists and is writable by the jukebox user. + + mkdir -m 755 /var/disorder + chown disorder:root /var/disorder + +8. Start the server, for instance: + + /etc/init.d/disorder start + + By default disorderd logs to daemon.*; check your syslog.conf to see where + this ends up and look for log messages from disorderd there. If it didn't + start up correctly there should be an error message. Correct the problem + and try again. + +9. After a minute it should start to play something. Try scratching it, as any + of the users you set up in step 5: + + disorder scratch + + The track should stop playing, and (if you set any up) a scratch sound play. + +10. Add any other users you want to config.private. Each user's password + should be stored in a file in their home directory, ~/.disorder/passwd, + which should be readable only by them, and should take the form of a single + line: + + password MYPASSWORD + + (root doesn't need this as the client can read it out of config.private + when running as root.) + + Note that the server must be reloaded (e.g. by 'disorder reconfigure') + when new users are added. + + Alternatively the administrator can create /etc/disorder/config.USERNAME + containing the same thing as above. It can either be owned by the user and + mode 400, or owned by root and the user's group (if you have per-user + groups) and mode 440. + + You can use 'disorder authorize' to automatically pick passwords and + create these files. + +11. Optionally source completion.bash from /etc/profile or similar, for + example: + + . /usr/local/share/disorder/completion.bash + + This provides completion over disorder command and option names. + + +Web Interface +============= + + "Thought I was a gonner baby, but I'm bullet proof" + +These instructions assumes you are using Apache 1.3.x. + +You need to configure a number of things to make this work: + +1. If you want to have a 'jukebox' virtual host, modify the DNS (or hosts file + if you are somehow reading this in the 1980s) accordingly and use a fragment + such as this one: + + <VirtualHost HOSTNAME> + DocumentRoot /home/jukebox/public_html + ServerName jukebox.DOMAIN + ServerAlias jukebox + ServerAdmin webmaster@DOMAIN + ErrorLog /var/log/apache/jukebox/error.log + TransferLog /var/log/apache/jukebox/access.log + Alias /static/ /usr/local/share/disorder/static/ + </VirtualHost> + + /static/ should point to the 'static' directory installed by DisOrder. If + you don't want to use the name 'static' then you can change the url.static + label in the web interface configuration to your preferred URL; see + disorder_config(5) for details. + + Don't forget to reload Apache after modifying its configuration. + + Separate logging is not required but I find it convenient. Up to you. + +2. disorder.cgi assumes it is subject to access control (and in particular uses + the username to report who did what). Here's how I configured Apache, given + the above VirtualHost settings: + + <Directory /home/jukebox> + Require valid-user + AuthType basic + AuthName jukebox + AuthUserFile /home/jukebox/http.users + </Directory> + + Adjust this according to wherever you're going to install disorder.cgi and + its expected URL. + + Don't forget to reload apache after modifying its configuration. If you got + it wrong, fix it and restart Apache. + +3. Create the password file configured above. Something like this: + + # htpasswd -b -c /home/jukebox/http.users myusername mypassword + Adding password for user myusername + # htpasswd -b /home/jukebox/http.users othername otherpass + Adding password for user othername + +4. The jukebox must be configured to trust the web user. I added the following + line to my /etc/disorder/config: + + trust www-data + + This might not be the same on your system! You have to specify the user + that the CGI script runs as, whatever that is. + +5. Install disorder.cgi in an appropriate location. Remember to make it + executable. With the above configuration I installed it as + ~jukebox/public_html/index.cgi. + +6. Give www-data (or whatever user it is) a password and edit + /etc/disorder/config.private accordingly. This file should be mode 640 and + owned by root:jukebox. The line should look something like this: + + allow www-data MYPASSWORD + + After editing the config file, you must make the daemon re-read it: + + disorder reconfigure + +7. Teach www-data its password, by putting it in /etc/disorder/config.www-data. + This file should be mode 640 and owned by root:www-data. + + password MYPASSWORD + + (You could also use ~www-data/.disorder/passwd for this but on some systems + the web server user's home directory is inside the document root, which + would have rather unfortunate consequences.) + +8. Try it out. You should be asked for a username and password that you + configured earlier, and be shown details of what is playing and what other + tracks have been configured for future play. + +9. Some features take time to start working, for instance those involving + reporting the length of tracks. This is because the server starts up as + quickly as possible even if the full track data has not yet been gathered; + the track data is then calculated in the background. + +10. If you run into problems, always look at the appropriate error log; the + message you see in your web browser will usually not be sufficient to + diagnose the problem all by itself. + +11. If you have a huge number of top level directories, then you might find + that the 'Choose' page is unreasonably large. If so add the following line + to /etc/disorder/options.user: + label sidebar.choosewhich choosealpha + + This will make 'Choose' be a link for each letter of the 26-letter Roman + alphabet; follow the link and you just get the directories which start with + that letter. The "*" link at the end gives you directories which don't + start with a letter. + + You can copy choosealpha.html to /etc/disorder and edit it to change the + set of initial choices to anything that can be expressed with regexps. The + regexps must be URL-encoded UTF-8 PCRE regexps. + + +Copyright +========= + + "Nothing but another drug, a licence that you buy and sell" + +DisOrder - select and play digital audio files +Copyright (C) 2003, 2004, 2005, 2006 Richard Kettlewell +Portions extracted from MPG321, http://mpg321.sourceforge.net/ + Copyright (C) 2001 Joe Drew + Copyright (C) 2000-2001 Robert Leslie +Binaries may derive extra copyright owners through linkage (binary distributors +are expected to do their own legwork) + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 59 Temple +Place, Suite 330, Boston, MA 02111-1307 USA + +Local Variables: +mode:text +fill-column:79 +End: +arch-tag:e7058f9442f954f3dd51523a1e805c32 diff --git a/README.client b/README.client new file mode 100644 index 0000000..9a2ff74 --- /dev/null +++ b/README.client @@ -0,0 +1,47 @@ +Setting up a standalone DisOrder client +======================================= + +Although DisOrder can still only play from a single computer, it is possible to +control it over the network if the server has a 'listen PORT' directive in its +configuration file. + +There is currently no standard DisOrder port number. + + +1. Configure the software with --without-server (and, optionally, +--without-python) and build and install it. Set up a stub config in +/etc/disorder/config (or /usr/local/etc/disorder/config if you didn't +set a nondefault --prefix) with the following contents: + + connect jukebox PORT + +where PORT is the same as 'listen PORT' on the server. + + +2. Copy the password file for each user to /etc/disorder/config.USER, +the contents being: + + password PASSWORD + +Alternatvely, each user can use ~/.disorder/passwd, with the same contents. If +the DisOrder username differs from the local username then use a 'username' +directive. + + +3. Test by issuing 'disorder playing'. + + +4. Run 'disobedience' for the GUI client. + + +The web interface could in principle be made to work on a separate +machine from the main server, though it is unlikely to be efficient +and at the moment it is built whenever the server is, so you will have +to unpick them a bit yourself if you wish to do this. + + +Local Variables: +mode:text +fill-column:79 +End: +# arch-tag:jEmzIKHdvK6GSjnax7Kp5Q diff --git a/README.raw b/README.raw new file mode 100644 index 0000000..cf2ca0c --- /dev/null +++ b/README.raw @@ -0,0 +1,55 @@ +* DisOrder Raw Format Players + +** Purpose + +The purpose of raw format players is: + + * Support pausing of playing tracks, with the audio device closed when not + in active use. + + * Eliminate the inter-track gap. + + * Perhaps in the future support network play. + +** Usage + +To use raw format, use the execraw module and make the command choose the +"disorder" libao driver. + +You should pass the "fragile" option to ogg123. This is because ogg123 ignores +write errors! + +mpg321 does not appear to have this bug. + +For _non_ raw players it is advisable to use the new --wait-for-device option. +This repeatedly tries to open the audio device before starting the player +proper. It times out after a couple of seconds. + +See disorder_config(5) and the example configuration file for further +information and examples. + +** Low-Level Details + +Raw format players are started slightly differently to normal ones. Before +they are executed a pipe is created and one end passed to a special speaker +process, which is spawned by the main server at startup. The file descriptor +of the player's end is identified by $DISORDER_RAW_FD. + +The expected data format is a ao_sample_format structure followed by the raw +sample data. However, this may be changed without notice in future versions of +DisOrder. If you need a stable interface here for some reason then get in +touch. + +Raw format players may be started before the track is to be played, and (if the +track is then removed from the queue before it reaches the head) terminated +before the track ever reaches a physical speaker. The point of this is to +allow audio data to be ready to play the moment the previous track end, without +having to wait for the player to start up. There is no way for a player to +tell that this is going on. + +Local Variables: +mode:outline +fill-column:79 +End: + +# arch-tag:FDgaJ8rznoa6TnmXxU1iPw diff --git a/README.streams b/README.streams new file mode 100644 index 0000000..4e6e425 --- /dev/null +++ b/README.streams @@ -0,0 +1,32 @@ +DisOrder and Internet Streams +============================= + +DisOrder doesn't have any built-in support for playing streams but you can make +it do so. I use the following in my configuration file: + + player /export/radio/*.oggradio shell 'xargs ogg123 -q < "$TRACK"' + collection fs iso-8859-1 /export/radio + +After setting this up you'll need to re-read the config file and provoke a +rescan: + + disorder reconfigure rescan /export/radio + +/export/radio contains a file for each stream, containing the URL to use: + + lyonesse$ cat /export/radio/CUR1350.oggradio + http://cur.chu.cam.ac.uk:8000/cur.ogg + +You'll probably want to prevent random play of streams: + + disorder set /export/radio/CUR1350.oggradio pick_at_random 0 + +You can then queue a stream like any other track. It won't automatically +interrupt the playing track, you have to scratch it manually. Go back to +normal play by scratching the stream. + +Local Variables: +mode:text +fill-column:79 +End: +arch-tag:ae95108d51c55288c4f6da4102343cd5 diff --git a/README.upgrades b/README.upgrades new file mode 100644 index 0000000..6e279e7 --- /dev/null +++ b/README.upgrades @@ -0,0 +1,112 @@ +* Upgrading DisOrder + +The general procedure is: + + * stop the old daemon, e.g. with + /etc/init.d/disorder stop + * build and install the new version as described in the README + * update the configuration files (see below) + * start the new daemon, e.g. with + /etc/init.d/disorder start + +The rest of this file describes things you must pay attention to when +upgrading between particular versions. Minor versions are not +explicitly mentioned; a version number like 1.1 implicitly includes +all 1.1.x versions. + +* 1.5 -> 1.6 + +** 'transform' and 'namepart' directives + +'transform' has moved from the web options to the main configuration file, so +that they can be used by other interfaces. The syntax and semantics are +unchanged. + +More importantly however both 'transform' and 'namepart' are now optional, with +sensible defaults being built in. So if you were already using the default +values you can just delete all instances of both. + +** enabled' and 'random_enabled' directives + +These have been removed. Instead the state persists from one run of the server +to the next. + +* 1.3 -> 1.4 + +** Raw Format Decoders + +You will probably want reconfigure your install to use the new facilities +(although the old way works fine). See the example configuration file and +README.raw for more details. + +Depending on how your system is configured you may need to link the disorder +libao driver into the right directory: + + ln -s /usr/local/lib/ao/plugins-2/libdisorder.so /usr/lib/ao/plugins-2/. + +* 1.2 -> 1.3 + +** Server Environment + +It is important that $sbindir is on the server's path. The example init script +guarantees this. You may need to modify the installed one. You will get +"deadlock manager unexpectedly terminated" if you get this wrong. + +** namepart directives + +These have changed in three ways. + +Firstly they have changed to substitute in a more convenient way. Instead of +matches for the regexp being substituted back into the original track name, the +replacement string now completely replaces it. Given the usual uses of +namepart, this is much more convenient. If you've stuck with the defaults no +changes should be needed for this. + +Secondly they are matched against the track name with the collection root +stripped off. + +Finally you will need to add an extra line to your config file as follows for +the new track aliasing mechanisms to work properly: + +namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +* 1.1 -> 1.2 + +** Web Interface Changes + +The web interface now includes static content as well as templates. +The static content must be given a name visible to HTTP clients which +maps to its location in the real filesystem. + +The README suggests using a rule in httpd.conf to make /static in the +HTTP namespace point to /usr/local/share/disorder/static, which is +where DisOrder installs its static content (by default). +Alternatively you can set the url.static label to the base URL of the +static content. + +** Configuration File Changes + +The trackname-part web interface directive has now gone, and the +options.trackname file with it. + +It is replaced by a new namepart directive in the main configuration +file. This has exactly the same syntax as trackname-part, only the +name and location have changed. + +The reason for the change is to allow track name parsing to be +centrally configured, rather than every interface to DisOrder having +to implement it locally. + +If you do not install new namepart directives into the main +configuration file then track titles will show up blank. + +If you do not remove the trackname-part directives from the web +interface configuration then you will get error messages in the web +server's error log. + +Local Variables: +mode:outline +fill-column:79 +End: + +# arch-tag:j+OBlcYYyUdGBVbVXVgXew diff --git a/TODO b/TODO new file mode 100644 index 0000000..704fa3e --- /dev/null +++ b/TODO @@ -0,0 +1,26 @@ +-*-outline-*- + +* plugins + +** configuration + +Allow plugins to be configured via the main config file somehow. + +* web interface + +** language choice + +Parse HTTP_ACCEPT_LANGUAGE and use it to choose template subdirectory. +I might leave this until I hear that someone actually wants a +multilingual jukebox. + +** rearrange queue + +Needs thought on how to design the interface. + +** improve volume control + +** templates + +Build defaults into program to save file IO. +arch-tag:d8e9783460cc93d13e90d8b8e3f1d481 diff --git a/acinclude.m4 b/acinclude.m4 new file mode 100644 index 0000000..03fd6ca --- /dev/null +++ b/acinclude.m4 @@ -0,0 +1,105 @@ +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +AC_DEFUN([RJK_FIND_GC_H],[ + AC_CACHE_CHECK([looking for <gc.h>],[rjk_cv_gc_h],[ + AC_PREPROC_IFELSE([ + #include <gc.h> + ], + [rjk_cv_gc_h="on default include path"],[ + oldCPPFLAGS="${CPPFLAGS}" + for dir in /usr/include/gc /usr/local/include/gc; do + if test "x$GCC" = xyes; then + CPPFLAGS="${oldCPPFLAGS} -isystem $dir" + else + CPPFLAGS="${oldCPPFLAGS} -I$dir" + fi + AC_PREPROC_IFELSE([ + #include <gc.h> + ], + [rjk_cv_gc_h=$dir;break],[rjk_cv_gc_h="not found"]) + done + CPPFLAGS="${oldCPPFLAGS}" + ]) + ]) + case "$rjk_cv_gc_h" in + "not found" ) + missing_headers="$missing_headers gc.h" + ;; + /* ) + if test "x$GCC" = xyes; then + CPPFLAGS="${CPPFLAGS} -isystem $rjk_cv_gc_h" + else + CPPFLAGS="${CPPFLAGS} -I$rjk_cv_gc_h" + fi + ;; + esac +]) + +AC_DEFUN([RJK_CHECK_LIB],[ + AC_CACHE_CHECK([for $2 in -l$1],[rjk_cv_lib_$1_$2],[ + save_LIBS="$LIBS" + LIBS="${LIBS} -l$1" + AC_LINK_IFELSE([AC_LANG_PROGRAM([$3],[$2;])], + [rjk_cv_lib_$1_$2=yes], + [rjk_cv_lib_$1_$2=no]) + LIBS="$save_LIBS" + ]) + if test $rjk_cv_lib_$1_$2 = yes; then + $4 + else + $5 + fi +]) + +AC_DEFUN([RJK_REQUIRE_PCRE_UTF8],[ + AC_CACHE_CHECK([whether libpcre was built with UTF-8 support], + [rjk_cv_pcre_utf8],[ + save_LIBS="$LIBS" + LIBS="$LIBS $1" + AC_RUN_IFELSE([AC_LANG_PROGRAM([ + #include <pcre.h> + #include <stdio.h> + ], + [ + pcre *r; + const char *errptr; + int erroffset; + + r = pcre_compile("\x80\x80", PCRE_UTF8, + &errptr, &erroffset, 0); + if(!r) { + fprintf(stderr, "pcre_compile: %s at %d", + errptr, erroffset); + exit(0); + } else { + fprintf(stderr, "accepted bogus UTF-8 string\n"); + exit(1); + } + ])], + [rjk_cv_pcre_utf8=yes], + [rjk_cv_pcre_utf8=no], + [AC_MSG_ERROR([cross-compiling, cannot check libpcre behaviour])]) + LIBS="$save_LIBS" + ]) + if test $rjk_cv_pcre_utf8 = no; then + AC_MSG_ERROR([please rebuild your pcre library with --enable-utf8]) + fi +]) +# arch-tag:d09b2112a218009313949a279401a5b4 diff --git a/clients/Makefile.am b/clients/Makefile.am new file mode 100644 index 0000000..5cdb0cb --- /dev/null +++ b/clients/Makefile.am @@ -0,0 +1,70 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_PROGRAMS=disorder disorderfm +noinst_PROGRAMS=test-eclient filename-bytes + +AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib + +disorder_SOURCES=disorder.c authorize.c authorize.h +disorder_LDADD=$(LIBOBJS) ../lib/libdisorder.la +disorder_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la + +disorderfm_SOURCES=disorderfm.c +disorderfm_LDADD=$(LIBOBJS) ../lib/libdisorder.la +disorderfm_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la + +filename_bytes_SOURCES=filename-bytes.c + +test_eclient_SOURCES=test-eclient.c +test_eclient_LDADD=../lib/libdisorder.la +test_eclient_DEPENDENCIES=../lib/libdisorder.la + +install-exec-hook: + $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir) + +check: check-help check-completions + +# check everything has working --help +check-help: all + ./disorder --version > /dev/null + ./disorder --help > /dev/null + ./disorder --help-commands > /dev/null + +# check that the command completions are up to date +check-completions: + ./disorder --help-commands \ + | awk '/^ [a-z]/ { print $$1 }' \ + | sort > ,commands + ( set -e;completions() { \ + for x; do \ + case $$x in\ + quack ) ;;\ + [a-z]* ) echo $$x; ;;\ + esac;\ + done;\ + }; \ + complete() { if [ "$$7" = disorder ]; then completions $$6; fi }; \ + . ${top_srcdir}/scripts/completion.bash )\ + | sort > ,completions + diff -u ,commands ,completions + +CLEANFILES=,commands ,completions +# arch-tag:3wdM7iS0B+n8makCG+YcAg diff --git a/clients/authorize.c b/clients/authorize.c new file mode 100644 index 0000000..e38aed4 --- /dev/null +++ b/clients/authorize.c @@ -0,0 +1,104 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <pwd.h> +#include <gcrypt.h> +#include <errno.h> +#include <unistd.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdio.h> + +#include "authorize.h" +#include "log.h" +#include "configuration.h" +#include "printf.h" +#include "hex.h" + +int authorize(const char *user) { + uint8_t pwbin[10]; + const struct passwd *pw, *jbpw; + gid_t jbgid; + char *c, *t, *pwhex; + int fd; + FILE *fp; + + if(!(jbpw = getpwnam(config->user))) + fatal(0, "cannot find user %s", config->user); + jbgid = jbpw->pw_gid; + if(!(pw = getpwnam(user))) + fatal(0, "no such user as %s", user); + if((c = config_userconf(0, pw)) && access(c, F_OK) == 0) { + error(0, "%s already exists", c); + return -1; + } + if((c = config_usersysconf(pw)) && access(c, F_OK) == 0) { + error(0, "%s already exists", c); + return -1; + } + byte_xasprintf(&t, "%s.new", c); + gcry_randomize(pwbin, sizeof pwbin, GCRY_STRONG_RANDOM); + pwhex = hex(pwbin, sizeof pwbin); + + /* create config.USER, to end up with mode 440 user:jukebox */ + if((fd = open(t, O_WRONLY|O_CREAT|O_EXCL, 0600)) < 0) + fatal(errno, "error creating %s", t); + if(fchown(fd, pw->pw_uid, -1) < 0) + fatal(errno, "error chowning %s", t); + if(fchmod(fd, 0400) < 0) + fatal(errno, "error chmoding %s", t); + if(!(fp = fdopen(fd, "w"))) + fatal(errno, "error calling fdopen"); + if(fprintf(fp, "password %s\n", pwhex) < 0 + || fclose(fp) < 0) + fatal(errno, "error writing to %s", t); + if(rename(t, c) < 0) + fatal(errno, "error renaming %s to %s", t, c); + + /* append to config.private. We might create it along the way (though this + * is unlikely) in which case it had better be 640 root:jukebox */ + if(!(c = config_private())) + fatal(0, "cannot determine private config file"); + if((fd = open(c, O_WRONLY|O_APPEND|O_CREAT, 0600)) < 0) + fatal(errno, "error opening %s", c); + if(fchown(fd, 0, jbgid) < 0) + fatal(errno, "error chowning %s", c); + if(fchmod(fd, 0640) < 0) + fatal(errno, "error chmoding %s", t); + if(!(fp = fdopen(fd, "a"))) + fatal(errno, "error calling fdopen"); + if(fprintf(fp, "allow %s %s\n", user, pwhex) < 0 + || fclose(fp) < 0) + fatal(errno, "error appending to %s", c); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:BHATzWNN/1ccK/g2pbA63Q */ diff --git a/clients/authorize.h b/clients/authorize.h new file mode 100644 index 0000000..0098b09 --- /dev/null +++ b/clients/authorize.h @@ -0,0 +1,35 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef AUTHORIZE_H +#define AUTHORIZE_H + +int authorize(const char *user); + +#endif /* AUTHORIZE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:2mW0Quc+PMX1C3BWywYFsQ */ diff --git a/clients/disorder.c b/clients/disorder.c new file mode 100644 index 0000000..46e2c2d --- /dev/null +++ b/clients/disorder.c @@ -0,0 +1,554 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <getopt.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <locale.h> +#include <time.h> +#include <stddef.h> +#include <unistd.h> +#include <assert.h> + +#include "configuration.h" +#include "syscalls.h" +#include "log.h" +#include "queue.h" +#include "client.h" +#include "wstat.h" +#include "table.h" +#include "charset.h" +#include "kvp.h" +#include "split.h" +#include "sink.h" +#include "plugin.h" +#include "mem.h" +#include "defs.h" +#include "authorize.h" +#include "vector.h" + +static int auto_reconfigure; + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "config", required_argument, 0, 'c' }, + { "debug", no_argument, 0, 'd' }, + { "help-commands", no_argument, 0, 'H' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Usage:\n" + " disorder [OPTIONS] COMMAND ...\n" + "Options:\n" + " --help, -h Display usage message\n" + " --help-commands, -H List commands\n" + " --version, -V Display version number\n" + " --config PATH, -c PATH Set configuration file\n" + " --debug, -d Turn on debugging\n"); + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorder version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +static void cf_version(disorder_client *c, + char attribute((unused)) **argv) { + char *v; + + if(disorder_version(c, &v)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(v))); +} + +static void print_queue_entry(const struct queue_entry *q) { + if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track))); + if(q->id) xprintf(" id %s\n", nullcheck(utf82mb(q->id))); + if(q->submitter) xprintf(" submitted by %s at %s", + nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + if(q->played) xprintf(" played at %s", ctime(&q->played)); + if(q->state == playing_started + || q->state == playing_paused) xprintf(" %lds so far", q->sofar); + else if(q->expected) xprintf(" might start at %s", ctime(&q->expected)); + if(q->scratched) xprintf(" scratched by %s\n", + nullcheck(utf82mb(q->scratched))); + else xprintf(" %s\n", playing_states[q->state]); + if(q->wstat) xprintf(" %s\n", wstat(q->wstat)); +} + +static void cf_playing(disorder_client *c, + char attribute((unused)) **argv) { + struct queue_entry *q; + + if(disorder_playing(c, &q)) exit(EXIT_FAILURE); + if(q) + print_queue_entry(q); + else + xprintf("nothing\n"); +} + +static void cf_play(disorder_client *c, char **argv) { + while(*argv) + if(disorder_play(c, *argv++)) exit(EXIT_FAILURE); +} + +static void cf_remove(disorder_client *c, char **argv) { + if(disorder_remove(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_disable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_disable(c)) exit(EXIT_FAILURE); +} + +static void cf_enable(disorder_client *c, char attribute((unused)) **argv) { + if(disorder_enable(c)) exit(EXIT_FAILURE); +} + +static void cf_scratch(disorder_client *c, + char **argv) { + if(disorder_scratch(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_shutdown(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_shutdown(c)) exit(EXIT_FAILURE); +} + +static void cf_reconfigure(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_reconfigure(c)) exit(EXIT_FAILURE); +} + +static void cf_rescan(disorder_client *c, char attribute((unused)) **argv) { + if(disorder_rescan(c)) exit(EXIT_FAILURE); +} + +static void cf_somequeue(disorder_client *c, + int (*fn)(disorder_client *c, + struct queue_entry **qp)) { + struct queue_entry *q; + + if(fn(c, &q)) exit(EXIT_FAILURE); + while(q) { + print_queue_entry(q); + q = q->next; + } +} + +static void cf_recent(disorder_client *c, char attribute((unused)) **argv) { + cf_somequeue(c, disorder_recent); +} + +static void cf_queue(disorder_client *c, char attribute((unused)) **argv) { + cf_somequeue(c, disorder_queue); +} + +static void cf_quack(disorder_client attribute((unused)) *c, + char attribute((unused)) **argv) { + xprintf("\n" + " .------------------.\n" + " | Naath is a babe! |\n" + " `---------+--------'\n" + " \\\n" + " >0\n" + " (<)'\n" + "~~~~~~~~~~~~~~~~~~~~~~\n" + "\n"); +} + +static void cf_somelist(disorder_client *c, char **argv, + int (*fn)(disorder_client *c, + const char *arg, const char *re, + char ***vecp, int *nvecp)) { + char **vec; + const char *re; + + if(argv[1]) + re = xstrdup(argv[1] + 1); + else + re = 0; + if(fn(c, argv[0], re, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static int isarg_regexp(const char *s) { + return s[0] == '~'; +} + +static void cf_dirs(disorder_client *c, + char **argv) { + cf_somelist(c, argv, disorder_directories); +} + +static void cf_files(disorder_client *c, char **argv) { + cf_somelist(c, argv, disorder_files); +} + +static void cf_allfiles(disorder_client *c, char **argv) { + cf_somelist(c, argv, disorder_allfiles); +} + +static void cf_get(disorder_client *c, char **argv) { + char *value; + + if(disorder_get(c, argv[0], argv[1], &value)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(value))); +} + +static void cf_length(disorder_client *c, char **argv) { + long length; + + if(disorder_length(c, argv[0], &length)) exit(EXIT_FAILURE); + xprintf("%ld\n", length); +} + +static void cf_set(disorder_client *c, char **argv) { + if(disorder_set(c, argv[0], argv[1], argv[2])) exit(EXIT_FAILURE); +} + +static void cf_unset(disorder_client *c, char **argv) { + if(disorder_unset(c, argv[0], argv[1])) exit(EXIT_FAILURE); +} + +static void cf_prefs(disorder_client *c, char **argv) { + struct kvp *k; + + if(disorder_prefs(c, argv[0], &k)) exit(EXIT_FAILURE); + for(; k; k = k->next) + xprintf("%s = %s\n", + nullcheck(utf82mb(k->name)), nullcheck(utf82mb(k->value))); +} + +static void cf_search(disorder_client *c, char **argv) { + char **results; + int nresults, n; + + if(disorder_search(c, *argv, &results, &nresults)) exit(EXIT_FAILURE); + for(n = 0; n < nresults; ++n) + xprintf("%s\n", nullcheck(utf82mb(results[n]))); +} + +static void cf_random_disable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_random_disable(c)) exit(EXIT_FAILURE); +} + +static void cf_random_enable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_random_enable(c)) exit(EXIT_FAILURE); +} + +static void cf_stats(disorder_client *c, + char attribute((unused)) **argv) { + char **vec; + + if(disorder_stats(c, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static void cf_get_volume(disorder_client *c, + char attribute((unused)) **argv) { + int l, r; + + if(disorder_get_volume(c, &l, &r)) exit(EXIT_FAILURE); + xprintf("%d %d\n", l, r); +} + +static void cf_set_volume(disorder_client *c, + char **argv) { + if(disorder_set_volume(c, atoi(argv[0]), atoi(argv[1]))) exit(EXIT_FAILURE); +} + +static void cf_become(disorder_client *c, + char **argv) { + if(disorder_become(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_log(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_log(c, sink_stdio("stdout", stdout))) exit(EXIT_FAILURE); +} + +static void cf_move(disorder_client *c, + char **argv) { + long n; + int e; + + if((e = xstrtol(&n, argv[1], 0, 10))) + fatal(e, "cannot convert '%s'", argv[1]); + if(n > INT_MAX || n < INT_MIN) + fatal(e, "%ld out of range", n); + if(disorder_move(c, argv[0], (int)n)) exit(EXIT_FAILURE); +} + +static void cf_part(disorder_client *c, + char **argv) { + char *s; + + if(disorder_part(c, &s, argv[0], argv[1], argv[2])) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(s))); +} + +static int isarg_filename(const char *s) { + return s[0] == '/'; +} + +static void cf_authorize(disorder_client attribute((unused)) *c, + char **argv) { + if(!authorize(argv[0])) + auto_reconfigure = 1; +} + +static void cf_resolve(disorder_client *c, + char **argv) { + char *track; + + if(disorder_resolve(c, &track, argv[0])) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(track))); +} + +static void cf_pause(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_pause(c)) exit(EXIT_FAILURE); +} + +static void cf_resume(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_resume(c)) exit(EXIT_FAILURE); +} + +static void cf_tags(disorder_client *c, + char attribute((unused)) **argv) { + char **vec; + + if(disorder_tags(c, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static void cf_get_global(disorder_client *c, char **argv) { + char *value; + + if(disorder_get_global(c, argv[0], &value)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(value))); +} + +static void cf_set_global(disorder_client *c, char **argv) { + if(disorder_set_global(c, argv[0], argv[1])) exit(EXIT_FAILURE); +} + +static void cf_unset_global(disorder_client *c, char **argv) { + if(disorder_unset_global(c, argv[0])) exit(EXIT_FAILURE); +} + +static const struct command { + const char *name; + int min, max; + void (*fn)(disorder_client *c, char **); + int (*isarg)(const char *); + const char *argstr, *desc; +} commands[] = { + { "allfiles", 1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]", + "List all files and directories in DIR" }, + { "authorize", 1, 1, cf_authorize, 0, "USER", + "Authorize USER to connect to the server" }, + { "become", 1, 1, cf_become, 0, "USER", + "Become user USER" }, + { "dirs", 1, 2, cf_dirs, isarg_regexp, "DIR [~REGEXP]", + "List directories in DIR" }, + { "disable", 0, 0, cf_disable, 0, "", + "Disable play" }, + { "disable-random", 0, 0, cf_random_disable, 0, "", + "Disable random play" }, + { "enable", 0, 0, cf_enable, 0, "", + "Enable play" }, + { "enable-random", 0, 0, cf_random_enable, 0, "", + "Enable random play" }, + { "files", 1, 2, cf_files, isarg_regexp, "DIR [~REGEXP]", + "List files in DIR" }, + { "get", 2, 2, cf_get, 0, "TRACK NAME", + "Get a preference value" }, + { "get-global", 1, 1, cf_get_global, 0, "NAME", + "Get a global preference value" }, + { "get-volume", 0, 0, cf_get_volume, 0, "", + "Get the current volume" }, + { "length", 1, 1, cf_length, 0, "TRACK", + "Get the length of TRACK in seconds" }, + { "log", 0, 0, cf_log, 0, "", + "Copy event log to stdout" }, + { "move", 2, 2, cf_move, 0, "TRACK DELTA", + "Move a track in the queue" }, + { "part", 3, 3, cf_part, 0, "TRACK CONTEXT PART", + "Find a track name part" }, + { "pause", 0, 0, cf_pause, 0, "", + "Pause the currently playing track" }, + { "play", 1, INT_MAX, cf_play, isarg_filename, "TRACKS...", + "Add TRACKS to the end of the queue" }, + { "playing", 0, 0, cf_playing, 0, "", + "Report the playing track" }, + { "prefs", 1, 1, cf_prefs, 0, "TRACK", + "Display all the preferences for TRACK" }, + { "quack", 0, 0, cf_quack, 0, 0, 0 }, + { "queue", 0, 0, cf_queue, 0, "", + "Display the current queue" }, + { "random-disable", 0, 0, cf_random_disable, 0, "", + "Disable random play" }, + { "random-enable", 0, 0, cf_random_enable, 0, "", + "Enable random play" }, + { "recent", 0, 0, cf_recent, 0, "", + "Display recently played track" }, + { "reconfigure", 0, 0, cf_reconfigure, 0, "", + "Reconfigure the daemon" }, + { "remove", 1, 1, cf_remove, 0, "TRACK", + "Remove a track from the queue" }, + { "rescan", 0, 0, cf_rescan, 0, "", + "Rescan for new tracks" }, + { "resolve", 1, 1, cf_resolve, 0, "TRACK", + "Resolve alias for TRACK" }, + { "resume", 0, 0, cf_resume, 0, "", + "Resume after a pause" }, + { "scratch", 0, 0, cf_scratch, 0, "", + "Scratch the currently playing track" }, + { "scratch-id", 1, 1, cf_scratch, 0, "ID", + "Scratch the currently playing track" }, + { "search", 1, 1, cf_search, 0, "WORDS", + "Display tracks matching all the words" }, + { "set", 3, 3, cf_set, 0, "TRACK NAME VALUE", + "Set a preference value" }, + { "set-global", 2, 2, cf_set_global, 0, "NAME VALUE", + "Set a global preference value" }, + { "set-volume", 2, 2, cf_set_volume, 0, "LEFT RIGHT", + "Set the volume" }, + { "shutdown", 0, 0, cf_shutdown, 0, "", + "Shut down the daemon" }, + { "stats", 0, 0, cf_stats, 0, "", + "Display server statistics" }, + { "tags", 0, 0, cf_tags, 0, "", + "List known tags" }, + { "unset", 2, 2, cf_unset, 0, "TRACK NAME", + "Unset a preference" }, + { "unset-global", 1, 1, cf_unset_global, 0, "NAME", + "Unset a global preference" }, + { "version", 0, 0, cf_version, 0, "", + "Display the server version" }, +}; + +static void help_commands(void) { + unsigned n, max = 0, l; + + xprintf("Command summary:\n"); + for(n = 0; n < sizeof commands / sizeof *commands; ++n) { + if(!commands[n].desc) continue; + l = strlen(commands[n].name); + if(*commands[n].argstr) + l += strlen(commands[n].argstr) + 1; + if(l > max) + max = l; + } + for(n = 0; n < sizeof commands / sizeof *commands; ++n) { + if(!commands[n].desc) continue; + l = strlen(commands[n].name); + if(*commands[n].argstr) + l += strlen(commands[n].argstr) + 1; + xprintf(" %s%s%s%*s %s\n", commands[n].name, + *commands[n].argstr ? " " : "", + commands[n].argstr, + max - l, "", + commands[n].desc); + } + xfclose(stdout); + exit(0); +} + +int main(int argc, char **argv) { + int n, i, j; + disorder_client *c = 0; + const char *s; + int status = 0; + struct vector args; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + while((n = getopt_long(argc, argv, "hVc:dHL", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'H': help_commands(); + case 'V': version(); + case 'c': configfile = optarg; break; + case 'd': debugging = 1; break; + default: fatal(0, "invalid option"); + } + } + if(config_read()) fatal(0, "cannot read configuration"); + if(!(c = disorder_new(1))) exit(EXIT_FAILURE); + s = config_get_file("socket"); + if(disorder_connect(c)) exit(EXIT_FAILURE); + n = optind; + /* accumulate command args */ + while(n < argc) { + if((i = TABLE_FIND(commands, struct command, name, argv[n])) < 0) + fatal(0, "unknown command '%s'", argv[n]); + if(n + commands[i].min >= argc) + fatal(0, "missing arguments to '%s'", argv[n]); + n++; + vector_init(&args); + for(j = 0; j < commands[i].min; ++j) + vector_append(&args, nullcheck(mb2utf8(argv[n + j]))); + for(; j < commands[i].max + && n + j < argc + && commands[i].isarg(argv[n + j]); ++j) + vector_append(&args, nullcheck(mb2utf8(argv[n + j]))); + vector_terminate(&args); + commands[i].fn(c, args.vec); + n += j; + } + if(auto_reconfigure) { + assert(c != 0); + if(disorder_reconfigure(c)) exit(EXIT_FAILURE); + } + if(c && disorder_close(c)) exit(EXIT_FAILURE); + if(fclose(stdout) < 0) fatal(errno, "error closing stdout"); + return status; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0ff200f4c42e9b04dd781fa89c6129f6 */ diff --git a/clients/disorderfm.c b/clients/disorderfm.c new file mode 100644 index 0000000..0fd4fa0 --- /dev/null +++ b/clients/disorderfm.c @@ -0,0 +1,414 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <getopt.h> +#include <unistd.h> +#include <locale.h> +#include <fcntl.h> +#include <errno.h> +#include <dirent.h> +#include <sys/stat.h> +#include <langinfo.h> +#include <string.h> +#include <fnmatch.h> + +#include "syscalls.h" +#include "log.h" +#include "printf.h" +#include "charset.h" +#include "defs.h" +#include "mem.h" + +/* Arguments etc ----------------------------------------------------------- */ + +typedef int copyfn(const char *from, const char *to); +typedef int mkdirfn(const char *dir, mode_t mode); + +/* Input and output directories */ +static const char *source, *destination; + +/* Function used to copy or link a file */ +static copyfn *copier = link; + +/* Function used to make a directory */ +static mkdirfn *dirmaker = mkdir; + +/* Various encodings */ +static const char *fromencoding, *toencoding, *tagencoding; + +/* Directory for untagged files */ +static const char *untagged; + +/* Extract tag information? */ +static int extracttags; + +/* Windows-friendly filenames? */ +static int windowsfriendly; + +/* Native character encoding (i.e. from LC_CTYPE) */ +static const char *nativeencoding; + +/* Count of errors */ +static long errors; + +/* Included/excluded filename patterns */ +static struct pattern { + struct pattern *next; + const char *pattern; + int type; +} *patterns, **patterns_end = &patterns; + +static int default_inclusion = 1; + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "debug", no_argument, 0, 'd' }, + { "from", required_argument, 0, 'f' }, + { "to", required_argument, 0, 't' }, + { "include", required_argument, 0, 'i' }, + { "exclude", required_argument, 0, 'e' }, + { "extract-tags", no_argument, 0, 'E' }, + { "tag-encoding", required_argument, 0, 'T' }, + { "untagged", required_argument, 0, 'u' }, + { "windows-friendly", no_argument, 0, 'w' }, + { "link", no_argument, 0, 'l' }, + { "symlink", no_argument, 0, 's' }, + { "copy", no_argument, 0, 'c' }, + { "no-action", no_argument, 0, 'n' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Usage:\n" +" disorderfm [OPTIONS] SOURCE DESTINATION\n" +"Options:\n" +" --from, -f ENCODING Source encoding\n" +" --to, -t ENCODING Destination encoding\n" +"If neither --from nor --to are specified then no encoding translation is\n" +"performed. If only one is specified then the other defaults to the current\n" +"locale's encoding.\n" +" --windows-friendly, -w Replace illegal characters with '_'\n" +" --include, -i PATTERN Include files matching a glob pattern\n" +" --exclude, -e PATTERN Include files matching a glob pattern\n" +"--include and --exclude may be used multiple times. They are checked in\n" +"order and the first match wins. If --include is ever used then nonmatching\n" +"files are excluded, otherwise they are included.\n" +" --link, -l Link files from source to destination (default)\n" +" --symlink, -s Symlink files from source to destination\n" +" --copy, -c Copy files from source to destination\n" +" --no-action, -n Just report what would be done\n" +" --debug, -d Debug mode\n" +" --help, -h Display usage message\n" +" --version, -V Display version number\n"); + /* TODO: tag extraction stuff when implemented */ + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorderfm version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +/* Utilities --------------------------------------------------------------- */ + +/* Copy FROM to TO. Has the same signature as link/symlink. */ +static int copy(const char *from, const char *to) { + int fdin, fdout; + char buffer[4096]; + int n; + + if((fdin = open(from, O_RDONLY)) < 0) + fatal(errno, "error opening %s", from); + if((fdout = open(to, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0) + fatal(errno, "error opening %s", to); + while((n = read(fdin, buffer, sizeof buffer)) > 0) { + if(write(fdout, buffer, n) < 0) + fatal(errno, "error writing to %s", to); + } + if(n < 0) fatal(errno, "error reading %s", from); + if(close(fdout) < 0) fatal(errno, "error closing %s", to); + xclose(fdin); + return 0; +} + +static int nocopy(const char *from, const char *to) { + xprintf("%s -> %s\n", + any2mb(fromencoding, from), + any2mb(toencoding, to)); + return 0; +} + +static int nomkdir(const char *dir, mode_t attribute((unused)) mode) { + xprintf("mkdir %s\n", any2mb(toencoding, dir)); + return 0; +} + +/* Name translation -------------------------------------------------------- */ + +static int bad_windows_char(int c) { + switch(c) { + default: + return 0; + /* Documented as bad by MS */ + case '<': + case '>': + case ':': + case '"': + case '\\': + case '|': + /* Not documented as bad by MS but Samba mangles anyway? */ + case '*': + return 1; + } +} + +/* Return the translated form of PATH */ +static char *nametrans(const char *path) { + char *t = any2any(fromencoding, toencoding, path); + + if(windowsfriendly) { + /* See: + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp?frame=true&hidetoc=true */ + /* List of forbidden names */ + static const char *const devicenames[] = { + "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", + "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "CLOCK$" + }; +#define NDEVICENAMES (sizeof devicenames / sizeof *devicenames) + char *s; + size_t n, l; + + /* Certain characters are just not allowed. We replace them with + * underscores. */ + for(s = t; *s; ++s) + if(bad_windows_char((unsigned char)*s)) + *s = '_'; + /* Trailing spaces and dots are not allowed. We just strip them. */ + while(s > t && (s[-1] == ' ' || s[-1] == '.')) + --s; + *s = 0; + /* Reject device names */ + if((s = strchr(t, '.'))) l = s - t; + else l = 0; + for(n = 0; n < NDEVICENAMES; ++n) + if(l == strlen(devicenames[n]) && !strncasecmp(devicenames[n], t, l)) + break; + if(n < NDEVICENAMES) + byte_xasprintf(&t, "_%s", t); + } + return t; +} + +/* The file walker --------------------------------------------------------- */ + +/* Visit file or directory PATH relative to SOURCE. SOURCE is a null pointer + * at the top level. + * + * PATH is something we extracted from the filesystem so by assumption is in + * the FROM encoding, which might _not_ be the same as the current locale's + * encoding. + * + * For most errors we carry on as best we can. + */ +static void visit(const char *path, const char *destpath) { + const struct pattern *p; + struct stat sb; + /* fullsourcepath is the full source pathname for PATH */ + char *fullsourcepath; + /* fulldestpath will be the full destination pathname */ + char *fulldestpath; + /* String to use in error messags. We convert to the current locale; this + * may be somewhat misleading but is necessary to avoid getting EILSEQ in + * error messages. */ + char *errsourcepath, *errdestpath; + + D(("visit %s", path ? path : "NULL")); + + /* Set up all the various path names */ + if(path) { + byte_xasprintf(&fullsourcepath, "%s/%s", + source, path); + byte_xasprintf(&fulldestpath, "%s/%s", + destination, destpath); + byte_xasprintf(&errsourcepath, "%s/%s", + source, any2mb(fromencoding, path)); + byte_xasprintf(&errdestpath, "%s/%s", + destination, any2mb(toencoding, destpath)); + for(p = patterns; p; p = p->next) + if(fnmatch(p->pattern, path, FNM_PATHNAME) == 0) + break; + if(p) { + /* We found a matching pattern */ + if(p->type == 'e') { + D(("%s matches %s therefore excluding", + path, p->pattern)); + return; + } + } else { + /* We did not find a matching pattern */ + if(!default_inclusion) { + D(("%s matches nothing and not including by default", path)); + return; + } + } + } else { + fullsourcepath = errsourcepath = (char *)source; + fulldestpath = errdestpath = (char *)destination; + } + + /* The destination directory might be a subdirectory of the source + * directory. In that case we'd better not descend into it when we encounter + * it in the source. */ + if(!strcmp(fullsourcepath, destination)) { + info("%s matches destination directory, not recursing", errsourcepath); + return; + } + + /* Find out what kind of file we're dealing with */ + if(stat(fullsourcepath, &sb) < 0) { + error(errno, "cannot stat %s", errsourcepath ); + ++errors; + return; + } + if(S_ISREG(sb.st_mode)) { + if(copier != nocopy) + if(unlink(fulldestpath) < 0 && errno != ENOENT) { + error(errno, "cannot remove %s", errdestpath); + ++errors; + return; + } + if(copier(fullsourcepath, fulldestpath) < 0) { + error(errno, "cannot link %s to %s", errsourcepath, errdestpath); + ++errors; + return; + } + } else if(S_ISDIR(sb.st_mode)) { + DIR *dp; + struct dirent *de; + char *childpath, *childdestpath; + + /* We create the directory on the destination side. If it already exists, + * that's fine. */ + if(dirmaker(fulldestpath, 0777) < 0 && errno != EEXIST) { + error(errno, "cannot mkdir %s", errdestpath); + ++errors; + return; + } + /* We read the directory and visit all the files in it in any old order. */ + if(!(dp = opendir(fullsourcepath))) { + error(errno, "cannot open directory %s", errsourcepath); + ++errors; + return; + } + while(((errno = 0), (de = readdir(dp)))) { + if(!strcmp(de->d_name, ".") + || !strcmp(de->d_name, "..")) continue; + if(path) { + byte_xasprintf(&childpath, "%s/%s", path, de->d_name); + byte_xasprintf(&childdestpath, "%s/%s", + destpath, nametrans(de->d_name)); + } else { + childpath = de->d_name; + childdestpath = nametrans(de->d_name); + } + visit(childpath, childdestpath); + } + if(errno) fatal(errno, "error reading directory %s", errsourcepath); + closedir(dp); + } else { + /* We don't handle special files, but we'd better warn the user. */ + info("ignoring %s", errsourcepath); + } +} + +int main(int argc, char **argv) { + int n; + struct pattern *p; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + while((n = getopt_long(argc, argv, "hVdf:t:i:e:ET:u:wlscn", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'V': version(); + case 'd': debugging = 1; break; + case 'f': fromencoding = optarg; break; + case 't': toencoding = optarg; break; + case 'i': + case 'e': + p = xmalloc(sizeof *p); + p->type = n; + p->pattern = optarg; + p->next = 0; + *patterns_end = p; + patterns_end = &p->next; + if(n == 'i') default_inclusion = 0; + break; + case 'E': extracttags = 1; break; + case 'T': tagencoding = optarg; break; + case 'u': untagged = optarg; break; + case 'w': windowsfriendly = 1; break; + case 'l': copier = link; break; + case 's': copier = symlink; break; + case 'c': copier = copy; break; + case 'n': copier = nocopy; dirmaker = nomkdir; break; + default: fatal(0, "invalid option"); + } + } + if(optind == argc) fatal(0, "missing SOURCE and DESTINATION arguments"); + else if(optind + 1 == argc) fatal(0, "missing DESTINATION argument"); + else if(optind + 2 != argc) fatal(0, "redundant extra arguments"); + if(extracttags) fatal(0, "--extract-tags is not implemented yet"); /* TODO */ + if(tagencoding && !extracttags) + fatal(0, "--tag-encoding without --extra-tags does not make sense"); + if(untagged && !extracttags) + fatal(0, "--untagged without --extra-tags does not make sense"); + source = argv[optind]; + destination = argv[optind + 1]; + nativeencoding = nl_langinfo(CODESET); + if(fromencoding || toencoding) { + if(!fromencoding) fromencoding = nativeencoding; + if(!toencoding) toencoding = nativeencoding; + } + if(!tagencoding) tagencoding = nativeencoding; + visit(0, 0); + xfclose(stdout); + if(errors) fprintf(stderr, "%ld errors\n", errors); + return !!errors; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:YWy+lwnCOS0d8Q5hjJ5gyQ */ diff --git a/clients/filename-bytes.c b/clients/filename-bytes.c new file mode 100644 index 0000000..67ce1fd --- /dev/null +++ b/clients/filename-bytes.c @@ -0,0 +1,42 @@ +/* Grotty program to print out the bytes making up filenames in some + * directory */ + +#include "config.h" + +#include <dirent.h> +#include <stdio.h> +#include <ctype.h> + +int main(int attribute((unused)) argc, char **argv) { + DIR *dp; + struct dirent *de; + int n; + + if(!(dp = opendir(argv[1]))) return -1; + while((de = readdir(dp))) { + for(n = 0; de->d_name[n]; ++n) { + printf("%02x", (unsigned char)de->d_name[n]); + if(n) putchar(' '); + } + putchar('\n'); + for(n = 0; de->d_name[n]; ++n) { + if(isprint((unsigned char)de->d_name[n])) + printf(" %c", (unsigned char)de->d_name[n]); + else + printf(" "); + if(n) putchar(' '); + } + putchar('\n'); + } + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:UBGSjLfIc10t/LG6EKQprw */ diff --git a/clients/test-eclient.c b/clients/test-eclient.c new file mode 100644 index 0000000..67900ad --- /dev/null +++ b/clients/test-eclient.c @@ -0,0 +1,189 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/select.h> +#include <stdio.h> +#include <assert.h> +#include <errno.h> +#include <stdlib.h> +#include <time.h> + +#include "queue.h" +#include "mem.h" +#include "log.h" +#include "eclient.h" +#include "configuration.h" +#include "syscalls.h" +#include "wstat.h" +#include "charset.h" + +/* TODO: a more comprehensive test */ + +static fd_set rfd, wfd; +static int maxfd; +static disorder_eclient *clients[1024]; +static char **tracks; +static disorder_eclient *c; +static char u_value; +static int quit; + +static const char *modes[] = { "none", "read", "write", "read write" }; + +static void cb_comms_error(void *u, const char *msg) { + assert(u == &u_value); + fprintf(stderr, "! comms error: %s\n", msg); +} + +static void cb_protocol_error(void *u, + void attribute((unused)) *v, + int attribute((unused)) code, + const char *msg) { + assert(u == &u_value); + fprintf(stderr, "! protocol error: %s\n", msg); +} + +static void cb_poll(void *u, disorder_eclient *c_, int fd, unsigned mode) { + assert(u == &u_value); + assert(fd >= 0); + assert(fd < 1024); /* bodge */ + fprintf(stderr, " poll callback %d %s\n", fd, modes[mode]); + if(mode & DISORDER_POLL_READ) + FD_SET(fd, &rfd); + else + FD_CLR(fd, &rfd); + if(mode & DISORDER_POLL_WRITE) + FD_SET(fd, &wfd); + else + FD_CLR(fd, &wfd); + clients[fd] = mode ? c_ : 0; + if(fd > maxfd) maxfd = fd; +} + +static void cb_report(void attribute((unused)) *u, + const char attribute((unused)) *msg) { +} + +static const disorder_eclient_callbacks callbacks = { + cb_comms_error, + cb_protocol_error, + cb_poll, + cb_report +}; + +/* cheap plastic event loop */ +static void loop(void) { + int n; + + while(!quit) { + fd_set r = rfd, w = wfd; + n = select(maxfd + 1, &r, &w, 0, 0); + if(n < 0) { + if(errno == EINTR) continue; + fatal(errno, "select"); + } + for(n = 0; n <= maxfd; ++n) + if(clients[n] && (FD_ISSET(n, &r) || FD_ISSET(n, &w))) + disorder_eclient_polled(clients[n], + ((FD_ISSET(n, &r) ? DISORDER_POLL_READ : 0) + |(FD_ISSET(n, &w) ? DISORDER_POLL_WRITE : 0))); + } + printf(". quit\n"); +} + +static void done(void) { + printf(". done\n"); + disorder_eclient_close(c); + quit = 1; +} + +static void play_completed(void *v) { + assert(v == tracks); + printf("* played: %s\n", *tracks); + ++tracks; + if(*tracks) { + if(disorder_eclient_play(c, *tracks, play_completed, tracks)) + exit(1); + } else + done(); +} + +static void version_completed(void *v, const char *value) { + printf("* version: %s\n", value); + if(v) { + if(*tracks) { + if(disorder_eclient_play(c, *tracks, play_completed, tracks)) + exit(1); + } else + done(); + } +} + +/* TODO: de-dupe with disorder.c */ +static void print_queue_entry(const struct queue_entry *q) { + if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track))); + if(q->id) xprintf(" id %s\n", nullcheck(utf82mb(q->id))); + if(q->submitter) xprintf(" submitted by %s at %s", + nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + if(q->played) xprintf(" played at %s", ctime(&q->played)); + if(q->state == playing_started + || q->state == playing_paused) xprintf(" %lds so far", q->sofar); + else if(q->expected) xprintf(" might start at %s", ctime(&q->expected)); + if(q->scratched) xprintf(" scratched by %s\n", + nullcheck(utf82mb(q->scratched))); + else xprintf(" %s\n", playing_states[q->state]); + if(q->wstat) xprintf(" %s\n", wstat(q->wstat)); +} + +static void recent_completed(void *v, struct queue_entry *q) { + assert(v == 0); + for(; q; q = q->next) + print_queue_entry(q); + if(disorder_eclient_version(c, version_completed, (void *)"")) exit(1); +} + +int main(int argc, char **argv) { + assert(argc > 0); + mem_init(1); + debugging = 0; /* turn on for even more verbosity */ + if(config_read()) fatal(0, "config_read failed"); + tracks = &argv[1]; + c = disorder_eclient_new(&callbacks, &u_value); + assert(c != 0); + /* stack up several version commands to test pipelining */ + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_recent(c, recent_completed, 0)) exit(1); + loop(); + exit(0); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:JRDbPXSlG3t9Urek88LwCg */ diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..4895914 --- /dev/null +++ b/configure.ac @@ -0,0 +1,383 @@ +# Process this file with autoconf to produce a configure script. +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +AC_INIT(disorder, 1.5.1+, richard+disorder@sfere.greenend.org.uk) +AC_CONFIG_AUX_DIR([config.aux]) +AM_INIT_AUTOMAKE(disorder, 1.5.1+) +AC_CONFIG_SRCDIR([server/disorderd.c]) +AM_CONFIG_HEADER([config.h]) + +# What we want to build +want_server=yes +want_gtk=yes +want_python=yes + +# Checks for programs. +AC_PROG_CC +AC_SET_MAKE +if test "x$GCC" = xyes; then + gcc_werror=-Werror +else + gcc_werror="" +fi + +AC_ARG_WITH([server], + [AS_HELP_STRING([--without-server], + [do not build server])], + [want_server=$withval]) +AC_ARG_WITH([gtk], + [AS_HELP_STRING([--without-gtk], + [do not build GTK+ client])], + [want_gtk=$withval]) +AC_ARG_WITH([python], + [AS_HELP_STRING([--without-python], + [do not build Python support])], + [want_python=$withval]) + +subdirs="scripts lib clients doc examples debian" + +if test $want_server = yes; then + subdirs="${subdirs} server plugins driver templates sounds images" +fi +if test $want_python = yes; then + AM_PATH_PYTHON + subdirs="${subdirs} python" +fi +if test $want_gtk = yes; then + subdirs="${subdirs} disobedience" + if test $want_server = no; then + subdirs="${subdirs} images" + fi +fi +AC_SUBST([subdirs]) + +# libtool config +AC_LIBTOOL_DLOPEN +AC_DISABLE_STATIC + +AC_PROG_LIBTOOL + +missing_libraries="" +missing_headers="" +missing_functions="" + +AC_DEFINE(_GNU_SOURCE, 1, [required for e.g. strsignal]) + +# Macs might have libraries under fink's root +AC_PATH_PROG([FINK],[fink],[none],[$PATH:/sw/bin]) +if test "x$FINK" != xnone; then + AC_CACHE_CHECK([fink install directory],[rjk_cv_finkprefix],[ + rjk_cv_finkprefix="`echo "$FINK" | sed 's,/bin/fink$,,'`" + ]) + CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include" + if test $want_server = yes; then + CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/db4" + fi + LDFLAGS="${LDFLAGS} -L${rjk_cv_finkprefix}/lib" +fi + +# Checks for libraries. +# We save up a list of missing libraries that we can't do without +# and report them all at once. +AC_CHECK_LIB(gc, GC_malloc, [AC_SUBST(LIBGC,[-lgc])], + [missing_libraries="$missing_libraries libgc"]) +AC_CHECK_LIB(gcrypt, gcry_md_open, + [AC_SUBST(LIBGCRYPT,[-lgcrypt])], + [missing_libraries="$missing_libraries libgcrypt"]) +AC_CHECK_LIB(pcre, pcre_compile, + [AC_SUBST(LIBPCRE,[-lpcre])], + [missing_libraries="$missing_libraries libpcre"]) +if test $want_server = yes; then + RJK_CHECK_LIB(db, db_create, [#include <db.h>], + [AC_SUBST(LIBDB,[-ldb])], + [missing_libraries="$missing_libraries libdb"]) + AC_CHECK_LIB(vorbis, vorbis_info_clear, + [:], + [missing_libraries="$missing_libraries libvorbis"]) + AC_CHECK_LIB(vorbisfile, ov_open, + [AC_SUBST(LIBVORBISFILE,["-lvorbisfile -lvorbis"])], + [missing_libraries="$missing_libraries libvorbisfile"], + [-lvorbis]) + AC_CHECK_LIB(mad, mad_stream_init, + [AC_SUBST(LIBMAD,[-lmad])], + [missing_libraries="$missing_libraries libmad"]) + AC_CHECK_LIB([ao], [ao_initialize], + [AC_SUBST(LIBAO,[-lao])], + [missing_libraries="$missing_libraries libao"]) + AC_CHECK_LIB([asound], [snd_pcm_open], + [AC_SUBST(LIBASOUND,[-lasound])], + [missing_libraries="$missing_libraries libasound"]) +fi + +if test $want_gtk = yes; then + AM_PATH_GLIB_2_0([],[],[missing_libraries="$missing_libraries libglib"]) + AM_PATH_GTK_2_0([],[],[missing_libraries="$missing_libraries libgtk"]) +fi + +# Some platforms have iconv already +AC_CHECK_FUNC(iconv_open, [:], + [RJK_CHECK_LIB(iconv, iconv_open, [#include <iconv.h>], + [AC_SUBST(LIBICONV,[-liconv])], + [missing_functions="$missing_functions iconv_open"])]) +AC_CHECK_FUNC([gethostbyname],[:],[ + AC_CHECK_LIB(nsl,gethostbyname, + [AC_SUBST(LIBNSL,[-lnsl])], + [missing_functions="$missing_functions gethostbyname"])]) +AC_CHECK_FUNC([socket],[:],[ + AC_CHECK_LIB(socket,socket, + [AC_SUBST(LIBSOCKET,[-lsocket])], + [missing_functions="$missing_functions socket"])]) +AC_CHECK_FUNC([dlopen],[:],[ + AC_CHECK_LIB(dl,dlopen, + [AC_SUBST(LIBDL,[-ldl])], + [missing_functions="$missing_functions dlopen"])]) + +if test ! -z "$missing_libraries"; then + AC_MSG_ERROR([missing libraries:$missing_libraries]) +fi + +# We require that libpcre support UTF-8 +RJK_REQUIRE_PCRE_UTF8([-lpcre]) + +# Checks for header files. +RJK_FIND_GC_H +AC_CHECK_HEADERS([inttypes.h]) +# Compilation will fail if any of these headers are missing, so we +# check for them here and fail early. +# We don't bother checking very standard stuff +if test $want_server = yes; then + AC_CHECK_HEADERS([db.h],[:],[ + missing_headers="$missing_headers $ac_header" + ]) + AC_CHECK_HEADERS([sys/soundcard.h]) dnl can cope without +fi +AC_CHECK_HEADERS([dlfcn.h gcrypt.h \ + getopt.h iconv.h langinfo.h \ + pcre.h sys/ioctl.h \ + syslog.h unistd.h],[:],[ + missing_headers="$missing_headers $ac_header" +]) + +if test ! -z "$missing_headers"; then + AC_MSG_ERROR([missing headers:$missing_headers]) +fi + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_SIZE_T +AC_C_INLINE +AC_CHECK_TYPES([struct sockaddr_in6],,,[AC_INCLUDES_DEFAULT +#include <netinet/in.h>]) + +# enable -Werror when we check for certain characteristics: + +old_CFLAGS="${CFLAGS}" +CFLAGS="${CFLAGS} $gcc_werror" +AC_CHECK_TYPES([long long,uint32_t,uint8_t,intmax_t,uintmax_t]) + +# Some GCC invocations warn for converting function pointers to void *. +# This is fair enough, as it's technically forbidden, but we use dlsym() +# which can pretty much only exist if object and function pointers are +# interconvertable. So we disable -Werror if need be. +if test ! -z "$gcc_werror"; then + AC_CACHE_CHECK([whether function pointers can be converted to void * without a warning], + [rjk_cv_function_pointer_cast],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT + void somefunction(void);], + [(void *)somefunction])], + [rjk_cv_function_pointer_cast=yes], + [rjk_cv_function_pointer_cast=no])]) + if test $rjk_cv_function_pointer_cast = no; then + gcc_werror="" + fi +fi + +CFLAGS="${old_CFLAGS}" + +# gcrypt maintainers keep changing everything. Design your interface +# first, then implement it once, rather than getting it wrong three or +# four times and shipping between each attempt. +AC_CACHE_CHECK([for hash handle type in <grypt.h>], + [rjk_cv_gcrypt_hash_handle],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [gcry_md_hd_t h;])], + [rjk_cv_gcrypt_hash_handle=gcry_md_hd_t],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [GcryMDHd h;])], + [rjk_cv_gcrypt_hash_handle=GcryMDHd], + [rjk_cv_gcrypt_hash_handle=GCRY_MD_HD])])]) +AC_DEFINE_UNQUOTED([gcrypt_hash_handle],[$rjk_cv_gcrypt_hash_handle], + [libgcrypt hash handle type]) + +AC_CACHE_CHECK([for gcry_error_t in <grypt.h>], + [rjk_cv_have_gcry_error_t],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [gcry_error_t e;])], + [rjk_cv_have_gcry_error_t=yes], + [rjk_cv_have_gcry_error_t=no])]) +if test $rjk_cv_have_gcry_error_t = yes; then + AC_DEFINE([HAVE_GCRY_ERROR_T],1,[define if <gcrypt.h> defines gcry_error_t]) +fi + +# Checks for functions +if test $ac_cv_type_long_long = yes; then + AC_CHECK_FUNCS([atoll strtoll],[:],[ + missing_functions="$missing_functions $ac_func" + ]) + # Darwin sometimes fails to declare strtoll (e.g. if you ask for -std=c99) + AC_CACHE_CHECK([whether strtoll is declared in <stdlib.h>], + [rjk_cv_strtoll_declared],[ + AC_EGREP_HEADER([strtoll], [stdlib.h], + [rjk_cv_strtoll_declared=yes], + [rjk_cv_strtoll_declared=no])]) + if test $rjk_cv_strtoll_declared = yes; then + AC_DEFINE([DECLARES_STRTOLL],[1],[define if <stdlib.h> declares strtoll]) + fi + AC_CACHE_CHECK([whether atoll is declared in <stdlib.h>], + [rjk_cv_atoll_declared],[ + AC_EGREP_HEADER([atoll], [stdlib.h], + [rjk_cv_atoll_declared=yes], + [rjk_cv_atoll_declared=no])]) + if test $rjk_cv_atoll_declared = yes; then + AC_DEFINE([DECLARES_ATOLL],[1],[define if <stdlib.h> declares atoll]) + fi +fi +AC_CHECK_FUNCS([ioctl nl_langinfo strsignal],[:],[ + missing_functions="$missing_functions $ac_func" +]) +# fsync will do if fdatasync not available +AC_CHECK_FUNCS([fdatasync],[:],[ + AC_CHECK_FUNCS([fsync], + [AC_DEFINE([fdatasync],[fsync],[define fdatasync to fsync if not available])], + [missing_functions="$missing_functions fdatasync"])]) +if test ! -z "$missing_functions"; then + AC_MSG_ERROR([missing functions:$missing_functions]) +fi +if test $want_server = yes; then + # <db.h> had better be version 3 or later + AC_CACHE_CHECK([db.h version],[rjk_cv_db_version],[ + AC_PREPROC_IFELSE([ + #include <db.h> + #ifndef DB_VERSION_MAJOR + # error cannot determine db version + #endif + #if DB_VERSION_MAJOR < 4 + # error inadequate db version + #endif + #if DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR < 2 + # error inadequate db version + #endif + ], + [rjk_cv_db_version=ok], + [rjk_cv_db_version=inadequate]) + ]) + if test $rjk_cv_db_version != ok; then + AC_MSG_ERROR([need db.h version at least 4.2]) + fi +fi + +if test "x$GCC" = xyes; then + # a reasonable default set of warnings + CC="${CC} -Wall -W -Wpointer-arith -Wbad-function-cast \ + -Wwrite-strings -Wmissing-prototypes \ + -Wmissing-declarations -Wnested-externs" + + # Fix up GTK+ and GLib compiler flags + GTK_CFLAGS="`echo \"$GTK_CFLAGS\"|sed 's/-I/-isystem /g'`" + GLIB_CFLAGS="`echo \"$GLIB_CFLAGS\"|sed 's/-I/-isystem /g'`" + + # GCC 2.95 doesn't know to ignore warnings from system headers + AC_CACHE_CHECK([whether -Werror is usable], + rjk_cv_werror, [ + save_CFLAGS="${CFLAGS}" + CFLAGS="${CFLAGS} ${GTK_CFLAGS} -Werror" + AC_TRY_COMPILE([#include <gtk/gtk.h>], + [], + [rjk_cv_werror=yes], + [rjk_cv_werror=no]) + CFLAGS="${save_CFLAGS}" + ]) + if test $rjk_cv_werror = no; then + gcc_werror='' + fi + CC="${CC} $gcc_werror" + + # for older GCCs that don't know %ju (etc) + AC_CACHE_CHECK([checking whether -Wno-format is required], + rjk_cv_noformat, + AC_TRY_COMPILE([#include <stdio.h> +#include <stdint.h> +], + [printf("%ju", (uintmax_t)0);], + [rjk_cv_noformat=no], + [rjk_cv_noformat=yes])) + if test $rjk_cv_noformat = yes; then + CC="${CC} -Wno-format" + fi + + AC_CACHE_CHECK([checking whether -Wshadow is OK], + rjk_cv_shadow, + oldCC="${CC}" + CC="${CC} -Wshadow" + [AC_TRY_COMPILE([ +#include <unistd.h> +#include <vorbis/vorbisfile.h> +], + [], + [rjk_cv_shadow=yes], + [rjk_cv_shadow=no]) + CC="${oldCC}"]) + if test $rjk_cv_shadow = yes; then + CC="${CC} -Wshadow" + fi + + +fi + +AH_BOTTOM([#ifdef __GNUC__ +# define attribute(x) __attribute__(x) +#else +# define attribute(x) +#endif]) + +AC_CONFIG_FILES([Makefile + templates/Makefile + images/Makefile + scripts/Makefile + lib/Makefile + server/Makefile + clients/Makefile + disobedience/Makefile + doc/Makefile + plugins/Makefile + driver/Makefile + debian/Makefile + sounds/Makefile + python/Makefile + examples/Makefile]) +AC_OUTPUT +# arch-tag:cb633d20520a61a924cd528cef926ec1 diff --git a/debian/Makefile.am b/debian/Makefile.am new file mode 100644 index 0000000..896e39c --- /dev/null +++ b/debian/Makefile.am @@ -0,0 +1,35 @@ +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +noinst_SCRIPTS=rules + +autorules_m4=${srcdir}/autorules.m4 +rules_m4=${srcdir}/rules.m4 + +rules: ${autorules_m4} ${rules_m4} + rm -f rules.tmp + m4 -P ${autorules_m4} ${rules_m4} > rules.tmp + chmod 555 rules.tmp + mv -f rules.tmp rules + +EXTRA_DIST=README.Debian autorules.m4 config control copyright \ + disorder.config htaccess postinst prerm rules.m4 templates \ + rules conffiles postrm changelog options.debian +# arch-tag:1b7bd457d3d740887311435ded0c5eac diff --git a/debian/README.Debian b/debian/README.Debian new file mode 100644 index 0000000..93b2c99 --- /dev/null +++ b/debian/README.Debian @@ -0,0 +1,37 @@ +Debian package for DisOrder +=========================== + +To get the web interface working: + +1) Make sure you /etc/apache/access.conf allows AuthConfig for + /usr/lib/cgi-bin. For instance: + + <Directory /usr/lib/cgi-bin> + AllowOverride AuthConfig + Options ExecCGI FollowSymLinks + </Directory> + +2) If you want to use digest authentication, make sure your Apache has + mod_auth_digest enabled and edit + /usr/lib/cgi-bin/disorder/.htaccess accordingly (it has been made a + conffile). + +3) Remember to reload apache if you've changed anything. + +4) Add users to /etc/disorder/http.users. For instance: + + # htpasswd -b /etc/disorder/http.users USERNAME PASSWORD + + Or with digest authentication: + + # htdigest /etc/disorder/http.users USER + Adding password for USER in realm jukebox. + New password: + Re-type new password: + +5) Test it at http://YOURHOSTNAME/cgi-bin/disorder/disorder + + If it doesn't work, always look at the web server error log. + + -- Richard Kettlewell <rjk@greenend.org.uk>, Sun May 22 14:14:11 2005 +arch-tag:e325cf2983484c130b1ca1df78331fa6 diff --git a/debian/autorules.m4 b/debian/autorules.m4 new file mode 100644 index 0000000..d04393b --- /dev/null +++ b/debian/autorules.m4 @@ -0,0 +1,119 @@ +#! /usr/bin/make -f +# +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +# This file was generated automatically - edit rules.m4 instead +# + +INSTALL=install +CONFIGURE=--prefix=/usr + +m4_divert(-1)m4_dnl + +m4_changequote([,]) + +m4_define([build], [[build]: +m4_syscmd([test -f ../configure || test -f ../config.status])m4_dnl +m4_ifelse(m4_sysval,0,[ ./configure ${CONFIGURE} +])m4_dnl + $(MAKE) prefix=/usr])m4_dnl + +m4_define([binary], [[binary]: [binary]-arch [binary]-indep +[binary]-arch: _archpkgs +[binary]-indep: _indeppkgs]) + +m4_define([anypkg], [m4_define([_package], $1)m4_dnl +m4_define([cleanup], cleanup [cleanpkg-$1])m4_dnl +cleanpkg-$1: + rm -rf debian/$1 + +pkg-$1: [build] + rm -rf debian/$1 + mkdir -p debian/$1 + mkdir -p debian/$1/DEBIAN + mkdir -p debian/$1/usr/share/doc/$1 + cp debian/copyright \ + debian/$1/usr/share/doc/$1/copyright + cp debian/changelog \ + debian/$1/usr/share/doc/$1/changelog.Debian + gzip -9 debian/$1/usr/share/doc/$1/copyright \ + debian/$1/usr/share/doc/$1/changelog.Debian +$2 dpkg-gencontrol -isp -p$1 -Pdebian/$1 -Tdebian/substvars.$1 + chown -R root:root debian/$1 + chmod -R g-ws debian/$1 + dpkg --[build] debian/$1 .. +]) + +m4_define([_target], + [m4_ifelse([$2],[],[$1],[$2])]) + +m4_define([install_usrbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/usr/bin/_target([$1],[$2])]) + +m4_define([install_usrsbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/usr/sbin/_target([$1],[$2])]) + +m4_define([install_bin], + [$(INSTALL) -m 755 $1 \ + debian/_package/bin/_target([$1],[$2])]) + +m4_define([install_sbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/sbin/_target([$1],[$2])]) + +m4_define([_mansect], + [m4_patsubst([$1], [^.*\.\([^.]*\)], [\1])]) + +m4_define([install_usrman], + [$(INSTALL) -m 644 $1 \ + debian/_package/usr/share/man/man[]_mansect(_target([$1],[$2]))/_target([$1],[$2]) + gzip -9 debian/_package/usr/share/man/man[]_mansect(_target([$1],[$2]))/_target([$1],[$2])]) + +m4_define([install_manlink], + [ln -s ../man[]_mansect([$1])/$1.gz \ + debian/_package/usr/man/man[]_mansect([$2])/$2.gz]) + +m4_define([archpkg], [m4_define([_archpkgs], _archpkgs pkg-$1)m4_dnl +anypkg([$1],[$2])]) + +m4_define([indeppkg], [m4_define([_indeppkgs], _indeppkgs pkg-$1)m4_dnl +anypkg([$1],[$2])]) + +m4_define([clean], [[clean]: cleanup + -$(MAKE) distclean + rm -f config.cache + rm -f debian/files + rm -f debian/substvars.*]) + +m4_define([cleanup], []) + +m4_define([_archpkgs], []) + +m4_define([_indeppkgs], []) + +m4_define([regenerate], [debian/rules: debian/autorules.m4 debian/rules.m4 + rm -f debian/rules.tmp + m4 -P debian/autorules.m4 debian/rules.m4 > debian/rules.tmp + chmod 555 debian/rules.tmp + mv -f debian/rules.tmp debian/rules +]) + +m4_divert(0)m4_dnl +# arch-tag:c5871f9bd46d5d2e2ed7302cbcaccd4b diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..3cb4a8a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,90 @@ +disorder (1.5+dev) unstable; urgency=low + + * Intermediate version number + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 17 Apr 2006 19:19:58 +0100 + +disorder (1.5.1) unstable; urgency=low + + * Release 1.5.1 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 25 Mar 2006 18:22:42 +0000 + +disorder (1.5) unstable; urgency=low + + * Release 1.5 + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 20 Mar 2006 22:32:33 +0000 + +disorder (1.4) unstable; urgency=low + + * Release 1.4 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sun, 23 Oct 2005 13:55:17 +0100 + +disorder (1.3) unstable; urgency=low + + * release 1.3 + + -- Richard Kettlewell <rjk@greenend.org.uk> Thu, 16 Jun 2005 21:35:22 +0100 + +disorder (1.2) unstable; urgency=low + + * release 1.2 + + -- Richard Kettlewell <rjk@greenend.org.uk> Fri, 11 Mar 2005 20:00:23 +0000 + +disorder (1.1) unstable; urgency=low + + * release 1.1 + + -- Richard Kettlewell <rjk@greenend.org.uk> Wed, 2 Feb 2005 22:43:47 +0000 + +disorder (1.0) unstable; urgency=low + + * release 1.0 + + -- Richard Kettlewell <rjk@greenend.org.uk> Thu, 30 Dec 2004 12:02:51 +0000 + +disorder (0.13) unstable; urgency=low + + * release 0.13 + + -- Richard Kettlewell <rjk@greenend.org.uk> Wed, 1 Dec 2004 23:48:15 +0000 + +disorder (0.12) unstable; urgency=low + + * release 0.12 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 30 Oct 2004 19:50:33 +0100 + +disorder (0.11) unstable; urgency=low + + * release 0.11 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 25 Sep 2004 17:46:55 +0100 + +disorder (0.10) unstable; urgency=low + + * release 0.10 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 17 Apr 2004 15:29:19 +0100 + +disorder (0.9+dev) unstable; urgency=low + + * intermediate version + + -- Richard Kettlewell <rjk@greenend.org.uk> Fri, 2 Apr 2004 19:50:58 +0100 + +disorder (0.9) unstable; urgency=low + + * release 0.9 + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 15 Mar 2004 20:06:39 +0000 + +disorder (0.8.99+20040315) unstable; urgency=low + + * Debianized + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 15 Mar 2004 13:04:02 +0000 +# arch-tag:eb29952826b3a0485b83a4996062a601 diff --git a/debian/conffiles b/debian/conffiles new file mode 100644 index 0000000..7d1aa67 --- /dev/null +++ b/debian/conffiles @@ -0,0 +1,4 @@ +/etc/disorder/config +/etc/disorder/options +/etc/init.d/disorder +/usr/lib/cgi-bin/disorder/.htaccess diff --git a/debian/config b/debian/config new file mode 100755 index 0000000..3d95bc7 --- /dev/null +++ b/debian/config @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +. /usr/share/debconf/confmodule + +db_input high disorder/roots || true +db_input high disorder/encoding || true +db_input medium disorder/scratches || true +db_input medium disorder/server-name || true +db_go || true + +db_get disorder/roots || true +roots="$RET" +db_get disorder/scratches || true +scratches="$RET" +db_get disorder/encoding || true +encoding="$RET" +db_get disorder/server-name || true +server_name="$RET" + +mkdir -p /etc/disorder +cat > /etc/disorder/conf.debconf.new <<EOF +# created automatically from debconf information +# do not edit manually +# run 'dpkg-reconfigure disorder' instead +EOF +for r in $roots; do + echo "collection fs $encoding $r" >> /etc/disorder/conf.debconf.new +done +for s in $scratches; do + echo "scratch $s" >> /etc/disorder/conf.debconf.new +done +echo "url http://$server_name/cgi-bin/disorder/disorder" >> /etc/disorder/conf.debconf.new + +mv /etc/disorder/conf.debconf.new /etc/disorder/conf.debconf +# arch-tag:80ca629b164394d9806d6e8378205951 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..1ea9dad --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Source: disorder +Maintainer: Richard Kettlewell <richard+disorder@sfere.greenend.org.uk> +Priority: optional +Standards-Version: 3.0.1.0 +Build-Depends: libgc6-dev | libgc-dev, libgcrypt-dev, libdb4.3-dev, libpcre3-dev, libvorbis-dev, libmad0-dev, libasound2-dev, libao-dev, python + +Package: disorder +Architecture: any +Section: sound +Priority: extra +Depends: httpd,pwgen,mpg321,vorbis-tools,sox,${shlibs:Depends} +Conflicts: jukebox +Description: Play random or selected digital audio files continuously + Controlled from the command line or via a web-based interface. +# arch-tag:747c10dcef548c0ba4ac60fc8864f4fa diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..6731eeb --- /dev/null +++ b/debian/copyright @@ -0,0 +1,25 @@ +Package home page: + http://www.greenend.org.uk/rjk/disorder/ + +DisOrder - select and play digital audio files +Copyright (C) 2003, 2004, 2005 Richard Kettlewell +Portions extracted from MPG321, http://mpg321.sourceforge.net/ + Copyright (C) 2001 Joe Drew + Copyright (C) (C) 2000-2001 Robert Leslie +Binaries may derive extra copyright owners through linkage +(binary distributors are expected to do their own legwork) + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# arch-tag:5e1b9b57d7568564f88a0a7894c9ca99 diff --git a/debian/disorder.config b/debian/disorder.config new file mode 100644 index 0000000..2f44f50 --- /dev/null +++ b/debian/disorder.config @@ -0,0 +1,50 @@ +# player programs +player *.mp3 execraw mpg321 -q -o disorder +player *.ogg execraw ogg123 -q -d disorder -o fragile:1 +player *.wav shell play + +# don't leave a gap between tracks +gap 0 + +# trust the web user and root +trust www-data root + +# run as user jukebox +user jukebox + +# volume control +mixer /dev/mixer +channel pcm + +# stopwords (i.e. ignored words) for the track search facility +stopword 01 02 03 04 05 06 07 08 09 10 +stopword 1 2 3 4 5 6 7 8 9 +stopword 11 12 13 14 15 16 17 18 19 20 +stopword 21 22 23 24 25 26 27 28 29 30 +stopword the a an and to too in on of we i am as im for is + +# namepart and transform are now filled in by default if you do not supply +# them. However if you supply any namepart directives then you will not +# get any defaults at all, so you must supply the full set. Similarly, +# if you supply any transform directives then you must supply the full set. + +# Parsing of track names for the currently playing track, the recently +# played list and the queue. +#namepart title "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#namepart title "/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort +#namepart album "/([^/]+)/[^/]+$" "$1" * +#namepart artist "/([^/]+)/[^/]+/[^/]+$" "$1" * +# used in alias construction +#namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +# Transformations of directory and filenames for the track choice screen +#transform track "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#transform track "^.*/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort + +#transform dir "^.*/([^/]+)$" "$1" * +#transform dir "^(the) ([^/]*)" "$2, $1" sort i +#transform dir "[[:punct:]]" "" sort g + +# include debconf configuration +include /etc/disorder/conf.debconf +# arch-tag:0c5987c73419c0551559cc72c3efa252 diff --git a/debian/htaccess b/debian/htaccess new file mode 100644 index 0000000..1f0c3a3 --- /dev/null +++ b/debian/htaccess @@ -0,0 +1,5 @@ +Require valid-user +AuthType basic +AuthName jukebox +AuthUserFile /etc/disorder/http.users +# arch-tag:2c90f33e2ed8bd26f2a0091113abfe0b diff --git a/debian/options.debian b/debian/options.debian new file mode 100644 index 0000000..ea164b2 --- /dev/null +++ b/debian/options.debian @@ -0,0 +1,27 @@ +# debian-specific disorder options +include options.labels + +# default columns +include options.columns + +# trackname transformations - supply your own or keep the default +include options.transform + +label images.enabled /disorder/tick.png +label images.disabled /disorder/cross.png +label images.scratch /disorder/cross.png +label images.noscratch /disorder/nocross.png +label images.up /disorder/up.png +label images.noup /disorder/noup.png +label images.down /disorder/down.png +label images.nodown /disorder/nodown.png +label images.edit /disorder/edit.png +label images.upall /disorder/upup.png +label images.noupall /disorder/noupup.png +label images.downall /disorder/downdown.png +label images.nodownall /disorder/nodowndown.png +label links.css /disorder/disorder.css + +# user overrides - you supply this +include options.user +# arch-tag:N/cCdPUdzmiFgcl69+Gj4g diff --git a/debian/postinst b/debian/postinst new file mode 100755 index 0000000..2ffe148 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,98 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +. /usr/share/debconf/confmodule + +add_jukebox_user() { + adduser --quiet --system --group --shell /bin/sh --home /var/lib/disorder \ + --no-create-home jukebox +} + +configure_init_d() { + update-rc.d disorder defaults 92 19 > /dev/null +} + +restart_server() { + /etc/init.d/disorder restart +} + +case "$1" in +configure ) + if grep -q ^jukebox: /etc/passwd; then + : + else + add_jukebox_user + fi + if test ! -f /etc/disorder/config.private; then + # pwgen in debian stable has insane exit status + set +e + rootpw=`pwgen 16 1` + webpw=`pwgen 16 1` + set -e + if test -z "$rootpw" || test -z "$webpw"; then + echo "$0: pwgen failed" 1>&2 + exit 1 + fi + u=`umask` + umask 077 + + echo allow root "$rootpw" > /etc/disorder/config.private.new + echo allow www-data "$webpw" >> /etc/disorder/config.private.new + chgrp jukebox /etc/disorder/config.private.new + chmod 640 /etc/disorder/config.private.new + mv /etc/disorder/config.private.new /etc/disorder/config.private + + if test ! -f /etc/disorder/config.www-data; then + echo password "$webpw" > /etc/disorder/config.www-data.new + chgrp www-data /etc/disorder/config.www-data.new + chmod 640 /etc/disorder/config.www-data.new + mv /etc/disorder/config.www-data.new /etc/disorder/config.www-data + fi + umask $u + fi + + if test ! -f /etc/disorder/http.users; then + u=`umask` + umask 077 + touch /etc/disorder/http.users + chgrp www-data /etc/disorder/http.users + chmod 640 /etc/disorder/http.users + umask $u + fi + chown jukebox:jukebox /var/lib/disorder + configure_init_d + restart_server + if test ! -e /etc/disorder/http.users; then + touch /etc/disorder/http.users + fi + db_stop + ldconfig -n /usr/lib + ;; +abort-upgrade ) + /etc/init.d/disorder restart + ;; +reconfigure ) + /etc/init.d/disorder reload + ;; +esac +# arch-tag:ec773fd84ef6c95eca2e7287d2db9878 diff --git a/debian/postrm b/debian/postrm new file mode 100755 index 0000000..1c09f1c --- /dev/null +++ b/debian/postrm @@ -0,0 +1,27 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +set -e +case "$1" in +remove ) + ldconfig -n /usr/lib + ;; +esac +# arch-tag:829be82824d0c6a903228934b29939e8 diff --git a/debian/prerm b/debian/prerm new file mode 100755 index 0000000..a15ed80 --- /dev/null +++ b/debian/prerm @@ -0,0 +1,23 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +set -e +/etc/init.d/disorder stop +# arch-tag:99c16a42035fb0fb74a828543a666eb3 diff --git a/debian/rules.m4 b/debian/rules.m4 new file mode 100644 index 0000000..df6a051 --- /dev/null +++ b/debian/rules.m4 @@ -0,0 +1,73 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +CONFIGURE=--prefix=/usr --sysconfdir=/etc --localstatedir=/var/lib --mandir=/usr/share/man +LIBTOOL=./libtool + +build + +archpkg([disorder], [ m4_dnl + $(MAKE) DESTDIR=`pwd`/debian/disorder staticdir=/var/www/disorder installdirs install + mkdir -m 755 -p debian/disorder/etc/disorder + mkdir -m 755 -p debian/disorder/etc/init.d + mkdir -m 755 -p debian/disorder/usr/lib/cgi-bin/disorder + mkdir -m 755 -p debian/disorder/var/lib/disorder + mkdir -m 755 -p debian/disorder/usr/share/doc/disorder/ChangeLog.d + $(INSTALL) -m 755 examples/disorder.init \ + debian/disorder/etc/init.d/disorder + $(INSTALL) -m 644 debian/disorder.config \ + debian/disorder/etc/disorder/config + $(INSTALL) -m 644 debian/options.debian \ + debian/disorder/etc/disorder/options + $(LIBTOOL) --mode=install $(INSTALL) -m 555 server/disorder.cgi \ + $(shell pwd)/debian/disorder/usr/lib/cgi-bin/disorder/disorder + dpkg-shlibdeps -Tdebian/substvars.disorder \ + debian/disorder/usr/bin/* \ + debian/disorder/usr/lib/cgi-bin/disorder/* \ + debian/disorder/usr/sbin/* \ + debian/disorder/usr/lib/*.so* \ + debian/disorder/usr/lib/disorder/*.so* + $(INSTALL) -m 444 debian/htaccess \ + debian/disorder/usr/lib/cgi-bin/disorder/.htaccess + $(INSTALL) -m 444 CHANGES README debian/README.Debian \ + BUGS README.* \ + debian/disorder/usr/share/doc/disorder/. + $(INSTALL) -m 444 ChangeLog.d/*--* \ + debian/disorder/usr/share/doc/disorder/ChangeLog.d + $(INSTALL) -m 444 COPYING debian/disorder/usr/share/doc/disorder/GPL + gzip -9f debian/disorder/usr/share/doc/disorder/ChangeLog.d/*--* \ + debian/disorder/usr/share/doc/disorder/CHANGES \ + debian/disorder/usr/share/doc/disorder/README \ + debian/disorder/usr/share/doc/disorder/README.* \ + debian/disorder/usr/share/doc/disorder/BUGS \ + debian/disorder/usr/share/doc/disorder/GPL + $(INSTALL) -m 555 debian/postinst debian/prerm debian/postrm \ + debian/config \ + debian/conffiles \ + debian/disorder/DEBIAN/. + $(INSTALL) -m 444 debian/templates debian/disorder/DEBIAN/. +]) + +binary + +clean + +regenerate +# arch-tag:be5fb519cf2e3214d82bdae4c9af2b17 diff --git a/debian/templates b/debian/templates new file mode 100644 index 0000000..a6712d1 --- /dev/null +++ b/debian/templates @@ -0,0 +1,28 @@ +Template: disorder/roots +Type: string +Default: +Description: Audio directories + Enter the list of directories that contain audio files (e.g. MP3 or OGG + files) separated by spaces. + +Template: disorder/encoding +Type: string +Default: ISO-8859-1 +Description: Filesystem encoding + Enter your filesystem's character encoding. Check rather than + guessing if you are not sure. + +Template: disorder/scratches +Type: string +Default: /usr/share/disorder/slap.ogg /usr/share/disorder/scratch.ogg +Description: Scratch files + Enter a list of files to be played when a track is scratched, separated + by spaces. Leave this blank if you don't want any scratch sounds. + All filenames should be absolute path names. + +Template: disorder/server-name +Type: string +Default: localhost +Description: Web server hostname + Enter the hostname that the web interface will appear under. + diff --git a/disobedience/Makefile.am b/disobedience/Makefile.am new file mode 100644 index 0000000..cfbba08 --- /dev/null +++ b/disobedience/Makefile.am @@ -0,0 +1,47 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_PROGRAMS=disobedience + +AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib +AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS) + +disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c \ + choose.c misc.c style.h control.c properties.c menu.c +disobedience_LDADD=../lib/libdisorder.la +disobedience_LDFLAGS=$(GTK_LIBS) + +install-exec-hook: + $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir) + +check: check-help + +disobedience.o: style.h + +style.h: ${srcdir}/disobedience.rc ${top_srcdir}/scripts/text2c + ${top_srcdir}/scripts/text2c style ${srcdir}/disobedience.rc > style.h.tmp + mv style.h.tmp style.h + +EXTRA_DIST=disobedience.rc + +# check everything has working --help +check-help: all + ./disobedience --version > /dev/null +# arch-tag:SILYIkQB3aPIe8z+Ng994A diff --git a/disobedience/TODO b/disobedience/TODO new file mode 100644 index 0000000..fa1dee6 --- /dev/null +++ b/disobedience/TODO @@ -0,0 +1,6 @@ +properties + make return be the same as OK +search + select tracks by tag + +# arch-tag:MyZQrifgPbO8ip4FqZo1eg diff --git a/disobedience/choose.c b/disobedience/choose.c new file mode 100644 index 0000000..369c601 --- /dev/null +++ b/disobedience/choose.c @@ -0,0 +1,999 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Choose track ------------------------------------------------------------ */ + +/* We don't use the built-in tree widgets because they require that you know + * the children of a node on demand, and we have to wait for the server to tell + * us. */ + +/* Types */ + +struct choosenode; + +struct displaydata { + guint width; /* total width required */ + guint height; /* total height required */ +}; + +/* instantiate the node vector type */ +VECTOR_TYPE(nodevector, struct choosenode *, xrealloc) + +struct choosenode { + struct choosenode *parent; /* parent node */ + const char *path; /* full path or 0 */ + const char *sort; /* sort key */ + const char *display; /* display name */ + int pending; /* pending resolve queries */ + unsigned flags; +#define CN_EXPANDABLE 0x0001 /* node is expandable */ +#define CN_EXPANDED 0x0002 /* node is expanded */ +/* Expandable items are directories; non-expandable ones are files */ +#define CN_DISPLAYED 0x0004 /* widget is displayed in layout */ +#define CN_SELECTED 0x0008 /* node is selected */ + struct nodevector children; /* vector of children */ + void (*fill)(struct choosenode *); /* request child fill or 0 for leaf */ + GtkWidget *container; /* the container for this row */ + GtkWidget *hbox; /* the hbox for this row */ + GtkWidget *arrow; /* arrow widget or 0 */ + GtkWidget *label; /* text label for this node */ + GtkWidget *marker; /* queued marker */ +}; + +struct menuitem { + /* Parameters */ + const char *name; /* name */ + + /* Callbacks */ + void (*activate)(GtkMenuItem *menuitem, gpointer user_data); + /* Called to activate the menu item. The user data is the choosenode the + * pointer is over. */ + + gboolean (*sensitive)(struct choosenode *cn); + /* Called to determine whether the menu item should be sensitive. TODO */ + + /* State */ + gulong handlerid; /* signal handler ID */ + GtkWidget *w; /* menu item widget */ +}; + +/* Variables */ + +static GtkWidget *chooselayout; +static GtkWidget *searchentry; /* search terms */ +static struct choosenode *root; +static struct choosenode *realroot; +static GtkWidget *menu; /* our popup menu */ +static struct choosenode *last_click; /* last clicked node for selection */ +static int files_visible; /* total files visible */ +static int files_selected; /* total files selected */ +static int search_in_flight; /* a search is underway */ +static int search_obsolete; /* the current search is void */ +static char **searchresults; /* search results */ +static int nsearchresults; /* number of results */ + +/* Forward Declarations */ + +static struct choosenode *newnode(struct choosenode *parent, + const char *path, + const char *display, + const char *sort, + unsigned flags, + void (*fill)(struct choosenode *)); +static void fill_root_node(struct choosenode *cn); +static void fill_letter_node(struct choosenode *cn); +static void fill_directory_node(struct choosenode *cn); +static void got_files(void *v, int nvec, char **vec); +static void got_resolved_file(void *v, const char *track); +static void got_dirs(void *v, int nvec, char **vec); + +static void expand_node(struct choosenode *cn); +static void contract_node(struct choosenode *cn); +static void updated_node(struct choosenode *cn, int redisplay); + +static void display_selection(struct choosenode *cn); +static void clear_selection(struct choosenode *cn); + +static void redisplay_tree(void); +static struct displaydata display_tree(struct choosenode *cn, int x, int y); +static void undisplay_tree(struct choosenode *cn); +static void initiate_search(void); +static void delete_widgets(struct choosenode *cn); + +static void clicked_choosenode(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data); + +static void activate_play(GtkMenuItem *menuitem, gpointer user_data); +static void activate_remove(GtkMenuItem *menuitem, gpointer user_data); +static void activate_properties(GtkMenuItem *menuitem, gpointer user_data); + +static gboolean sensitive_play(struct choosenode *cn); +static gboolean sensitive_remove(struct choosenode *cn); +static gboolean sensitive_properties(struct choosenode *cn); + +static struct menuitem menuitems[] = { + { "Play", activate_play, sensitive_play, 0, 0 }, + { "Remove", activate_remove, sensitive_remove, 0, 0 }, + { "Properties", activate_properties, sensitive_properties, 0, 0 }, +}; + +#define NMENUITEMS (int)(sizeof menuitems / sizeof *menuitems) + +/* Maintaining the data structure ------------------------------------------ */ + +/* Create a new node */ +static struct choosenode *newnode(struct choosenode *parent, + const char *path, + const char *display, + const char *sort, + unsigned flags, + void (*fill)(struct choosenode *)) { + struct choosenode *const n = xmalloc(sizeof *n); + + D(("newnode %s %s", path, display)); + if(flags & CN_EXPANDABLE) + assert(fill); + else + assert(!fill); + n->parent = parent; + n->path = path; + n->display = display; + n->sort = sort; + n->flags = flags; + nodevector_init(&n->children); + n->fill = fill; + if(parent) + nodevector_append(&parent->children, n); + return n; +} + +/* Fill the root */ +static void fill_root_node(struct choosenode *cn) { + int ch; + char *name; + struct callbackdata *cbd; + + D(("fill_root_node")); + if(choosealpha) { + if(!cn->children.nvec) { /* Only need to do this once */ + for(ch = 'A'; ch <= 'Z'; ++ch) { + byte_xasprintf(&name, "%c", ch); + newnode(cn, "<letter>", name, name, CN_EXPANDABLE, fill_letter_node); + } + newnode(cn, "<letter>", "*", "~", CN_EXPANDABLE, fill_letter_node); + } + updated_node(cn, 1); + } else { + /* More de-duping possible here */ + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, "", 0, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, "", 0, cbd); + } +} + +/* Clear all the children of CN */ +static void clear_children(struct choosenode *cn) { + int n; + + D(("clear_children %s", cn->path)); + /* Recursively clear subtrees */ + for(n = 0; n < cn->children.nvec; ++n) { + clear_children(cn->children.vec[n]); + if(cn->children.vec[n]->container) { + if(cn->children.vec[n]->arrow) + gtk_widget_destroy(cn->children.vec[n]->arrow); + gtk_widget_destroy(cn->children.vec[n]->label); + if(cn->children.vec[n]->marker) + gtk_widget_destroy(cn->children.vec[n]->marker); + gtk_widget_destroy(cn->children.vec[n]->hbox); + gtk_widget_destroy(cn->children.vec[n]->container); + } + } + cn->children.nvec = 0; +} + +/* Fill a child node */ +static void fill_letter_node(struct choosenode *cn) { + const char *regexp; + struct callbackdata *cbd; + + D(("fill_letter_node %s", cn->display)); + switch(cn->display[0]) { + default: + byte_xasprintf((char **)®exp, "^(the )?%c", tolower(cn->display[0])); + break; + case 'T': + regexp = "^(?!the [^t])t"; + break; + case '*': + regexp = "^[^a-z]"; + break; + } + /* TODO: caching */ + /* TODO: de-dupe against fill_directory_node */ + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + clear_children(cn); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, "", regexp, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, "", regexp, cbd); +} + +/* Called with a list of files just below some node */ +static void got_files(void *v, int nvec, char **vec) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode; + int n; + + D(("got_files %d files for %s", nvec, cn->path)); + /* Complicated by the need to resolve aliases. We can save a bit of effort + * by re-using cbd though. */ + cn->pending = nvec; + for(n = 0; n < nvec; ++n) + disorder_eclient_resolve(client, got_resolved_file, vec[n], cbd); +} + +static void got_resolved_file(void *v, const char *track) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode, *file_cn; + + file_cn = newnode(cn, track, + trackname_transform("track", track, "display"), + trackname_transform("track", track, "sort"), + 0/*flags*/, 0/*fill*/); + /* Only bother updating when we've got the lot */ + if(--cn->pending == 0) + updated_node(cn, 1); +} + +/* Called with a list of directories just below some node */ +static void got_dirs(void *v, int nvec, char **vec) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode; + int n; + + D(("got_dirs %d dirs for %s", nvec, cn->path)); + for(n = 0; n < nvec; ++n) + newnode(cn, vec[n], + trackname_transform("dir", vec[n], "display"), + trackname_transform("dir", vec[n], "sort"), + CN_EXPANDABLE, fill_directory_node); + updated_node(cn, 1); +} + +/* Fill a child node */ +static void fill_directory_node(struct choosenode *cn) { + struct callbackdata *cbd; + + D(("fill_directory_node %s", cn->path)); + /* TODO: caching */ + /* TODO: de-dupe against fill_letter_node */ + assert(report_label != 0); + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + cn->children.nvec = 0; + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, cn->path, 0, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, cn->path, 0, cbd); +} + +/* Expand a node */ +static void expand_node(struct choosenode *cn) { + D(("expand_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* If node is already expanded do nothing. */ + if(cn->flags & CN_EXPANDED) return; + /* We mark the node as expanded and request that it fill itself. When it has + * completed it will called updated_node() and we can redraw at that + * point. */ + cn->flags |= CN_EXPANDED; + /* TODO: visual feedback */ + cn->fill(cn); +} + +/* Contract a node */ +static void contract_node(struct choosenode *cn) { + D(("contract_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* If node is already contracted do nothing. */ + if(!(cn->flags & CN_EXPANDED)) return; + cn->flags &= ~CN_EXPANDED; + /* Clear selection below this node */ + clear_selection(cn); + /* We can contract a node immediately. */ + redisplay_tree(); +} + +/* qsort callback for ordering choosenodes */ +static int compare_choosenode(const void *av, const void *bv) { + const struct choosenode *const *aa = av, *const *bb = bv; + const struct choosenode *a = *aa, *b = *bb; + + return compare_tracks(a->sort, b->sort, + a->display, b->display, + a->path, b->path); +} + +/* Called when an expandable node is updated. */ +static void updated_node(struct choosenode *cn, int redisplay) { + D(("updated_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* It might be that the node has been de-expanded since we requested the + * update. In that case we ignore this notification. */ + if(!(cn->flags & CN_EXPANDED)) return; + /* Sort children */ + qsort(cn->children.vec, cn->children.nvec, sizeof (struct choosenode *), + compare_choosenode); + if(redisplay) + redisplay_tree(); +} + +/* Searching --------------------------------------------------------------- */ + +static int compare_track_for_qsort(const void *a, const void *b) { + return compare_path(*(char **)a, *(char **)b); +} + +/* Return true iff FILE is a child of DIR */ +static int is_child(const char *dir, const char *file) { + const size_t dlen = strlen(dir); + + return (!strncmp(file, dir, dlen) + && file[dlen] == '/' + && strchr(file + dlen + 1, '/') == 0); +} + +/* Return true iff FILE is a descendant of DIR */ +static int is_descendant(const char *dir, const char *file) { + const size_t dlen = strlen(dir); + + return !strncmp(file, dir, dlen) && file[dlen] == '/'; +} + +/* Called to fill a node in the search results tree */ +static void fill_search_node(struct choosenode *cn) { + int n; + const size_t plen = strlen(cn->path); + const char *s; + char *dir, *last = 0; + + D(("fill_search_node %s", cn->path)); + /* We depend on the search results being sorted as by compare_path(). */ + cn->children.nvec = 0; + for(n = 0; n < nsearchresults; ++n) { + /* We only care about descendants of CN */ + if(!is_descendant(cn->path, searchresults[n])) + continue; + s = strchr(searchresults[n] + plen + 1, '/'); + if(s) { + /* We've identified a subdirectory of CN. */ + dir = xstrndup(searchresults[n], s - searchresults[n]); + if(!last || strcmp(dir, last)) { + /* Not a duplicate */ + last = dir; + newnode(cn, dir, + trackname_transform("dir", dir, "display"), + trackname_transform("dir", dir, "sort"), + CN_EXPANDABLE, fill_search_node); + } + } else { + /* We've identified a file in CN */ + newnode(cn, searchresults[n], + trackname_transform("track", searchresults[n], "display"), + trackname_transform("track", searchresults[n], "sort"), + 0/*flags*/, 0/*fill*/); + } + } + updated_node(cn, 1); +} + +/* This is called from eclient with a (possibly empty) list of search results, + * and also from initiate_seatch with an always empty list to indicate that + * we're not searching for anything in particular. */ +static void search_completed(void attribute((unused)) *v, + int nvec, char **vec) { + struct choosenode *cn; + int n; + const char *dir; + + search_in_flight = 0; + if(search_obsolete) { + /* This search has been obsoleted by user input since it started. + * Therefore we throw away the result and search again. */ + search_obsolete = 0; + initiate_search(); + } else { + if(nvec) { + /* We will replace the choose tree with a tree structured view of search + * results. First we must disabled the choose tree's widgets. */ + delete_widgets(root); + /* Put the tracks into order, grouped by directory. They'll probably + * come back this way anyway in current versions of the server, but it's + * cheap not to rely on it (compared with the massive effort we expend + * later on) */ + qsort(vec, nvec, sizeof(char *), compare_track_for_qsort); + searchresults = vec; + nsearchresults = nvec; + cn = root = newnode(0/*parent*/, "", "Search results", "", + CN_EXPANDABLE|CN_EXPANDED, fill_search_node); + /* Construct the initial tree. We do this in a single pass and expand + * everything, so you can actually see your search results. */ + for(n = 0; n < nsearchresults; ++n) { + /* Firstly we might need to go up a few directories to each an ancestor + * of this track */ + while(!is_descendant(cn->path, searchresults[n])) { + /* We report the update on each node the last time we see it (With + * display=0, the main purpose of this is to get the order of the + * children right.) */ + updated_node(cn, 0); + cn = cn->parent; + } + /* Secondly we might need to insert some new directories */ + while(!is_child(cn->path, searchresults[n])) { + /* Figure out the subdirectory */ + dir = xstrndup(searchresults[n], + strchr(searchresults[n] + strlen(cn->path) + 1, + '/') - searchresults[n]); + cn = newnode(cn, dir, + trackname_transform("dir", dir, "display"), + trackname_transform("dir", dir, "sort"), + CN_EXPANDABLE|CN_EXPANDED, fill_search_node); + } + /* Finally we can insert the track as a child of the current + * directory */ + newnode(cn, searchresults[n], + trackname_transform("track", searchresults[n], "display"), + trackname_transform("track", searchresults[n], "sort"), + 0/*flags*/, 0/*fill*/); + } + while(cn) { + /* Update all the nodes back up to the root */ + updated_node(cn, 0); + cn = cn->parent; + } + /* Now it's worth displaying the tree */ + redisplay_tree(); + } else if(root != realroot) { + delete_widgets(root); + root = realroot; + redisplay_tree(); + } + } +} + +static void initiate_search(void) { + char *terms, *e; + + /* Find out what the user is after */ + terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(searchentry))); + /* Strip leading and trailing space */ + while(*terms == ' ') ++terms; + e = terms + strlen(terms); + while(e > terms && e[-1] == ' ') --e; + *e = 0; + /* If a search is already underway then mark it as obsolete. We'll revisit + * when it returns. */ + if(search_in_flight) { + search_obsolete = 1; + return; + } + if(*terms) { + /* There's still something left. Initiate the search. */ + if(disorder_eclient_search(client, search_completed, terms, 0)) { + /* The search terms are bad! We treat this as if there were no search + * terms at all. Some kind of feedback would be handy. */ + fprintf(stderr, "bad terms [%s]\n", terms); /* TODO */ + search_completed(0, 0, 0); + } else { + search_in_flight = 1; + } + } else { + /* No search terms - we want to see all tracks */ + search_completed(0, 0, 0); + } +} + +/* Called when the cancel search button is clicked */ +static void clearsearch_clicked(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_entry_set_text(GTK_ENTRY(searchentry), ""); +} + +/* Display functions ------------------------------------------------------- */ + +/* Delete all the widgets in the tree */ +static void delete_widgets(struct choosenode *cn) { + int n; + + if(cn->container) { + gtk_widget_destroy(cn->container); + cn->container = 0; + } + for(n = 0; n < cn->children.nvec; ++n) + delete_widgets(cn->children.vec[n]); + cn->flags &= ~(CN_DISPLAYED|CN_SELECTED); + files_selected = 0; +} + +/* Update the display */ +static void redisplay_tree(void) { + struct displaydata d; + guint oldwidth, oldheight; + + D(("redisplay_tree")); + /* We'll count these up empirically each time */ + files_selected = 0; + files_visible = 0; + /* Correct the layout and find out how much space it uses */ + d = display_tree(root, 0, 0); + /* We must set the total size or scrolling will not work (it wouldn't be hard + * for GtkLayout to figure it out for itself but presumably you're supposed + * to be able to have widgets off the edge of the layuot.) + * + * There is a problem: if we shrink the size then the part of the screen that + * is outside the new size but inside the old one is not updated. I think + * this is arguably bug in GTK+ but it's easy to force a redraw if this + * region is nonempty. + */ + gtk_layout_get_size(GTK_LAYOUT(chooselayout), &oldwidth, &oldheight); + if(oldwidth > d.width || oldheight > d.height) + gtk_widget_queue_draw(chooselayout); + gtk_layout_set_size(GTK_LAYOUT(chooselayout), d.width, d.height); + /* Notify the main menu of any recent changes */ + menu_update(-1); +} + +/* Make sure all displayed widgets from CN down exist and are in their proper + * place and return the vertical space used. */ +static struct displaydata display_tree(struct choosenode *cn, int x, int y) { + int n, aw; + GtkRequisition req; + struct displaydata d, cd; + GdkPixbuf *pb; + + D(("display_tree %s %d,%d", cn->path, x, y)); + + /* An expandable item contains an arrow and a text label. When you press the + * button it flips its expand state. + * + * A non-expandable item has just a text label and no arrow. + */ + if(!cn->container) { + /* Widgets need to be created */ + cn->hbox = gtk_hbox_new(FALSE, 1); + if(cn->flags & CN_EXPANDABLE) { + cn->arrow = gtk_arrow_new(cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN + : GTK_ARROW_RIGHT, + GTK_SHADOW_NONE); + cn->marker = 0; + } else { + cn->arrow = 0; + if((pb = find_image("notes.png"))) + cn->marker = gtk_image_new_from_pixbuf(pb); + } + cn->label = gtk_label_new(cn->display); + if(cn->arrow) + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->arrow); + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->label); + if(cn->marker) + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->marker); + cn->container = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(cn->container), cn->hbox); + g_signal_connect(cn->container, "button-release-event", + G_CALLBACK(clicked_choosenode), cn); + g_signal_connect(cn->container, "button-press-event", + G_CALLBACK(clicked_choosenode), cn); + g_object_ref(cn->container); + gtk_widget_set_name(cn->label, "choose"); + gtk_widget_set_name(cn->container, "choose"); + /* Show everything by default */ + gtk_widget_show_all(cn->container); + } + assert(cn->container); + /* Make sure the icon is right */ + if(cn->flags & CN_EXPANDABLE) + gtk_arrow_set(GTK_ARROW(cn->arrow), + cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN : GTK_ARROW_RIGHT, + GTK_SHADOW_NONE); + else if(cn->marker) + /* Make sure the queued marker is right */ + /* TODO: doesn't always work */ + (queued(cn->path) ? gtk_widget_show : gtk_widget_hide)(cn->marker); + /* Put the widget in the right place */ + if(cn->flags & CN_DISPLAYED) + gtk_layout_move(GTK_LAYOUT(chooselayout), cn->container, x, y); + else { + gtk_layout_put(GTK_LAYOUT(chooselayout), cn->container, x, y); + cn->flags |= CN_DISPLAYED; + } + /* Set the widget's selection status */ + if(!(cn->flags & CN_EXPANDABLE)) + display_selection(cn); + /* Find the size used so we can get vertical positioning right. */ + gtk_widget_size_request(cn->container, &req); + d.width = x + req.width; + d.height = y + req.height; + if(cn->flags & CN_EXPANDED) { + /* We'll offset children by the size of the arrow whatever it might be. */ + assert(cn->arrow); + gtk_widget_size_request(cn->arrow, &req); + aw = req.width; + for(n = 0; n < cn->children.nvec; ++n) { + cd = display_tree(cn->children.vec[n], x + aw, d.height); + if(cd.width > d.width) + d.width = cd.width; + d.height = cd.height; + } + } else { + for(n = 0; n < cn->children.nvec; ++n) + undisplay_tree(cn->children.vec[n]); + } + if(!(cn->flags & CN_EXPANDABLE)) { + ++files_visible; + if(cn->flags & CN_SELECTED) + ++files_selected; + } + /* report back how much space we used */ + D(("display_tree %s %d,%d total size %dx%d", cn->path, x, y, + d.width, d.height)); + return d; +} + +/* Remove widgets for newly hidden nodes */ +static void undisplay_tree(struct choosenode *cn) { + int n; + + D(("undisplay_tree %s", cn->path)); + /* Remove this widget from the display */ + if(cn->flags & CN_DISPLAYED) { + gtk_container_remove(GTK_CONTAINER(chooselayout), cn->container); + cn->flags ^= CN_DISPLAYED; + } + /* Remove children too */ + for(n = 0; n < cn->children.nvec; ++n) + undisplay_tree(cn->children.vec[n]); +} + +/* Selection --------------------------------------------------------------- */ + +static void display_selection(struct choosenode *cn) { + /* Need foreground and background colors */ + gtk_widget_set_state(cn->label, (cn->flags & CN_SELECTED + ? GTK_STATE_SELECTED : GTK_STATE_NORMAL)); + gtk_widget_set_state(cn->container, (cn->flags & CN_SELECTED + ? GTK_STATE_SELECTED : GTK_STATE_NORMAL)); +} + +/* Set the selection state of a widget. Directories can never be selected, we + * just ignore attempts to do so. */ +static void set_selection(struct choosenode *cn, int selected) { + unsigned f = selected ? CN_SELECTED : 0; + + D(("set_selection %d %s", selected, cn->path)); + if(!(cn->flags & CN_EXPANDABLE) && (cn->flags & CN_SELECTED) != f) { + cn->flags ^= CN_SELECTED; + /* Maintain selection count */ + if(selected) + ++files_selected; + else + --files_selected; + display_selection(cn); + /* Update main menu sensitivity */ + menu_update(-1); + } +} + +/* Recursively clear all selection bits from CN down */ +static void clear_selection(struct choosenode *cn) { + int n; + + set_selection(cn, 0); + for(n = 0; n < cn->children.nvec; ++n) + clear_selection(cn->children.vec[n]); +} + +/* User actions ------------------------------------------------------------ */ + +/* Clicked on something */ +static void clicked_choosenode(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data) { + struct choosenode *cn = user_data; + int ind, last_ind, n; + + D(("clicked_choosenode %s", cn->path)); + if(event->type == GDK_BUTTON_RELEASE + && event->button == 1) { + /* Left click */ + if(cn->flags & CN_EXPANDABLE) { + /* This is a directory. Flip its expansion status. */ + if(cn->flags & CN_EXPANDED) + contract_node(cn); + else + expand_node(cn); + last_click = 0; + } else { + /* This is a file. Adjust selection status */ + /* TODO the basic logic here is essentially the same as that in queue.c. + * Can we share code at all? */ + switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + case 0: + clear_selection(root); + set_selection(cn, 1); + last_click = cn; + break; + case GDK_CONTROL_MASK: + set_selection(cn, !(cn->flags & CN_SELECTED)); + last_click = cn; + break; + case GDK_SHIFT_MASK: + case GDK_SHIFT_MASK|GDK_CONTROL_MASK: + if(last_click && last_click->parent == cn->parent) { + /* Figure out where the current and last clicks are in the list */ + ind = last_ind = -1; + for(n = 0; n < cn->parent->children.nvec; ++n) { + if(cn->parent->children.vec[n] == cn) + ind = n; + if(cn->parent->children.vec[n] == last_click) + last_ind = n; + } + /* Test shouldn't ever fail, but still */ + if(ind >= 0 && last_ind >= 0) { + if(!(event->state & GDK_CONTROL_MASK)) { + for(n = 0; n < cn->parent->children.nvec; ++n) + set_selection(cn->parent->children.vec[n], 0); + } + if(ind > last_ind) + for(n = last_ind; n <= ind; ++n) + set_selection(cn->parent->children.vec[n], 1); + else + for(n = ind; n <= last_ind; ++n) + set_selection(cn->parent->children.vec[n], 1); + if(event->state & GDK_CONTROL_MASK) + last_click = cn; + } + } + break; + } + } + } else if(event->type == GDK_BUTTON_RELEASE + && event->button == 2) { + /* Middle click - play the pointed track */ + if(!(cn->flags & CN_EXPANDABLE)) { + clear_selection(root); + set_selection(cn, 1); + gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue"); + disorder_eclient_play(client, cn->path, 0, 0); + last_click = 0; + } + } else if(event->type == GDK_BUTTON_PRESS + && event->button == 3) { + /* Right click. Pop up a menu. */ + /* If the current file isn't selected, switch the selection to just that. + * (If we're looking at a directory then leave the selection alone.) */ + if(!(cn->flags & CN_EXPANDABLE) && !(cn->flags & CN_SELECTED)) { + clear_selection(root); + set_selection(cn, 1); + last_click = cn; + } + /* Set the item sensitivity and callbacks */ + for(n = 0; n < NMENUITEMS; ++n) { + if(menuitems[n].handlerid) + g_signal_handler_disconnect(menuitems[n].w, + menuitems[n].handlerid); + gtk_widget_set_sensitive(menuitems[n].w, + menuitems[n].sensitive(cn)); + menuitems[n].handlerid = g_signal_connect + (menuitems[n].w, "activate", G_CALLBACK(menuitems[n].activate), cn); + } + /* Pop up the menu */ + gtk_widget_show_all(menu); + gtk_menu_popup(GTK_MENU(menu), 0, 0, 0, 0, + event->button, event->time); + } +} + +static void searchentry_changed(GtkEditable attribute((unused)) *editable, + gpointer attribute((unused)) user_data) { + initiate_search(); +} + +/* Menu items -------------------------------------------------------------- */ + +static void recurse_selected(struct choosenode *cn, struct vector *v) { + int n; + + if(cn->flags & CN_EXPANDABLE) { + if(cn->flags & CN_EXPANDED) + for(n = 0; n < cn->children.nvec; ++n) + recurse_selected(cn->children.vec[n], v); + } else { + if((cn->flags & CN_SELECTED) && cn->path) + vector_append(v, (char *)cn->path); + } +} + +static char **gather_selected(int *ntracks) { + struct vector v; + + vector_init(&v); + recurse_selected(root, &v); + vector_terminate(&v); + if(ntracks) *ntracks = v.nvec; + return v.vec; +} + +static void activate_play(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + char **tracks = gather_selected(0); + int n; + + gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue"); + for(n = 0; tracks[n]; ++n) + disorder_eclient_play(client, tracks[n], 0, 0); +} + +static void activate_remove(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + /* TODO remove all selected tracks */ +} + +static void activate_properties(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + int ntracks; + char **tracks = gather_selected(&ntracks); + + properties(ntracks, tracks); +} + +static gboolean sensitive_play(struct choosenode attribute((unused)) *cn) { + return !!files_selected; +} + +static gboolean sensitive_remove(struct choosenode attribute((unused)) *cn) { + return FALSE; /* not implemented yet */ +} + +static gboolean sensitive_properties(struct choosenode attribute((unused)) *cn) { + return !!files_selected; +} + +/* Main menu plumbing ------------------------------------------------------ */ + +static int choose_properties_sensitive(GtkWidget attribute((unused)) *w) { + return !!files_selected; +} + +static int choose_selectall_sensitive(GtkWidget attribute((unused)) *w) { + return FALSE; /* TODO */ +} + +static void choose_properties_activate(GtkWidget attribute((unused)) *w) { + activate_properties(0, 0); +} + +static void choose_selectall_activate(GtkWidget attribute((unused)) *w) { + /* TODO */ +} + +static const struct tabtype tabtype_choose = { + choose_properties_sensitive, + choose_selectall_sensitive, + choose_properties_activate, + choose_selectall_activate, +}; + +/* Public entry points ----------------------------------------------------- */ + +/* Create a track choice widget */ +GtkWidget *choose_widget(void) { + int n; + GtkWidget *scrolled; + GtkWidget *vbox, *hbox, *clearsearch; + + /* + * +--vbox-------------------------------------------------------+ + * | +-hbox----------------------------------------------------+ | + * | | searchentry | clearsearch | | + * | +---------------------------------------------------------+ | + * | +-scrolled------------------------------------------------+ | + * | | +-chooselayout------------------------------------++--+ | | + * | | | Tree structure is manually layed out in here ||^^| | | + * | | | || | | | + * | | | || | | | + * | | | || | | | + * | | | ||vv| | | + * | | +-------------------------------------------------++--+ | | + * | | +-------------------------------------------------+ | | + * | | |< >| | | + * | | +-------------------------------------------------+ | | + * | +---------------------------------------------------------+ | + * +-------------------------------------------------------------+ + */ + + /* Text entry box for search terms */ + searchentry = gtk_entry_new(); + g_signal_connect(searchentry, "changed", G_CALLBACK(searchentry_changed), 0); + + /* Cancel button to clear the search */ + clearsearch = gtk_button_new_from_stock(GTK_STOCK_CANCEL); + g_signal_connect(G_OBJECT(clearsearch), "clicked", + G_CALLBACK(clearsearch_clicked), 0); + + /* hbox packs the search box and the cancel button together on a line */ + hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/); + gtk_box_pack_start(GTK_BOX(hbox), searchentry, + TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/); + gtk_box_pack_end(GTK_BOX(hbox), clearsearch, + FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/); + + /* chooselayout contains the currently visible subset of the track + * namespace */ + chooselayout = gtk_layout_new(0, 0); + root = newnode(0/*parent*/, "<root>", "All files", "", + CN_EXPANDABLE, fill_root_node); + realroot = root; + expand_node(root); /* will call redisplay_tree */ + /* Create the popup menu */ + menu = gtk_menu_new(); + g_signal_connect(menu, "destroy", G_CALLBACK(gtk_widget_destroyed), &menu); + for(n = 0; n < NMENUITEMS; ++n) { + menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name); + gtk_menu_attach(GTK_MENU(menu), menuitems[n].w, 0, 1, n, n + 1); + } + /* The layout is scrollable */ + scrolled = scroll_widget(GTK_WIDGET(chooselayout), "choose"); + + /* The scrollable layout and the search hbox go together in a vbox */ + vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/); + gtk_box_pack_start(GTK_BOX(vbox), hbox, + FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/); + gtk_box_pack_end(GTK_BOX(vbox), scrolled, + TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/); + + g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_choose); + return vbox; +} + +/* Called when something we care about here might have changed */ +void choose_update(void) { + redisplay_tree(); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:A5KX3X9SR8Pl57VRLSnCng */ diff --git a/disobedience/client.c b/disobedience/client.c new file mode 100644 index 0000000..d03e9de --- /dev/null +++ b/disobedience/client.c @@ -0,0 +1,176 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* GSource subclass for disorder_eclient */ +struct eclient_source { + GSource gsource; + disorder_eclient *client; + time_t last_poll; + GPollFD pollfd; +}; + +/* Called before FDs are polled to choose a timeout. We ask for a 3s + * timeout and every 10s or so we force a dispatch. */ +static gboolean gtkclient_prepare(GSource *source, + gint *timeout) { + const struct eclient_source *esource = (struct eclient_source *)source; + D(("gtkclient_prepare")); + if(time(0) > esource->last_poll + 10) + return TRUE; /* timed out */ + *timeout = 3000/*milliseconds*/; + return FALSE; /* please poll */ +} + +/* Check whether we're ready to dispatch. */ +static gboolean gtkclient_check(GSource *source) { + const struct eclient_source *esource = (struct eclient_source *)source; + D(("gtkclient_check fd=%d events=%x revents=%x", + esource->pollfd.fd, esource->pollfd.events, esource->pollfd.revents)); + return esource->pollfd.fd != -1 && esource->pollfd.revents != 0; +} + +/* Dispatch */ +static gboolean gtkclient_dispatch(GSource *source, + GSourceFunc attribute((unused)) callback, + gpointer attribute((unused)) user_data) { + struct eclient_source *esource = (struct eclient_source *)source; + unsigned revents, mode; + + D(("gtkclient_dispatch")); + revents = esource->pollfd.revents & esource->pollfd.events; + mode = 0; + if(revents & (G_IO_IN|G_IO_HUP|G_IO_ERR)) + mode |= DISORDER_POLL_READ; + if(revents & (G_IO_OUT|G_IO_HUP|G_IO_ERR)) + mode |= DISORDER_POLL_WRITE; + time(&esource->last_poll); + disorder_eclient_polled(esource->client, mode); + return TRUE; /* ??? not documented */ +} + +/* Table of callbacks for GSource subclass */ +static const GSourceFuncs sourcefuncs = { + gtkclient_prepare, + gtkclient_check, + gtkclient_dispatch, + 0, + 0, + 0, +}; + +/* Tell the mainloop what we need */ +static void gtkclient_poll(void *u, + disorder_eclient attribute((unused)) *c, + int fd, unsigned mode) { + struct eclient_source *esource = u; + GSource *source = u; + + D(("gtkclient_poll fd=%d mode=%x", fd, mode)); + /* deconfigure the source if currently configured */ + if(esource->pollfd.fd != -1) { + D(("calling g_source_remove_poll")); + g_source_remove_poll(source, &esource->pollfd); + esource->pollfd.fd = -1; + esource->pollfd.events = 0; + } + /* install new settings */ + if(fd != -1 && mode) { + esource->pollfd.fd = fd; + esource->pollfd.events = 0; + if(mode & DISORDER_POLL_READ) + esource->pollfd.events |= G_IO_IN | G_IO_HUP | G_IO_ERR; + if(mode & DISORDER_POLL_WRITE) + esource->pollfd.events |= G_IO_OUT | G_IO_ERR; + /* reconfigure the source */ + D(("calling g_source_add_poll")); + g_source_add_poll(source, &esource->pollfd); + } +} + +/* Report a communication-level error. It will be automatically retried. */ +static void gtkclient_comms_error(void attribute((unused)) *u, + const char *msg) { + D(("gtkclient_comms_error %s", msg)); + gtk_label_set_text(GTK_LABEL(report_label), msg); +} + +/* Report a protocol error. It will not be retried. We offer a callback to + * the submitter of the original command and if none is supplied we pop up an + * error box. */ +static void gtkclient_protocol_error(void attribute((unused)) *u, + void *v, + int code, + const char *msg) { + struct callbackdata *cbd = v; + + D(("gtkclient_protocol_error %s", msg)); + if(cbd && cbd->onerror) + cbd->onerror(cbd, code, msg); + else + popup_protocol_error(code, msg); +} + +static void gtkclient_report(void attribute((unused)) *u, + const char *msg) { + if(!msg) + /* We're idle - clear the report line */ + gtk_label_set_text(GTK_LABEL(report_label), ""); +} + +void popup_protocol_error(int attribute((unused)) code, + const char *msg) { + gtk_label_set_text(GTK_LABEL(report_label), msg); + popup_error(msg); +} + +/* Table of eclient callbacks */ +static const disorder_eclient_callbacks gtkclient_callbacks = { + gtkclient_comms_error, + gtkclient_protocol_error, + gtkclient_poll, + gtkclient_report +}; + +/* Create an eclient using the GLib main loop */ +disorder_eclient *gtkclient(void) { + GSource *source; + struct eclient_source *esource; + + D(("gtkclient")); + source = g_source_new((GSourceFuncs *)&sourcefuncs, + sizeof (struct eclient_source)); + esource = (struct eclient_source *)source; + esource->pollfd.fd = -1; + esource->client = disorder_eclient_new(>kclient_callbacks, source); + g_source_attach(source, 0); + return esource->client; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:32Qju8BYS5FZvqbPHElgcg */ diff --git a/disobedience/control.c b/disobedience/control.c new file mode 100644 index 0000000..159ab5d --- /dev/null +++ b/disobedience/control.c @@ -0,0 +1,316 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Forward declartions ----------------------------------------------------- */ + +struct icon; + +static void update_pause(const struct icon *); +static void update_play(const struct icon *); +static void update_scratch(const struct icon *); +static void update_random_enable(const struct icon *); +static void update_random_disable(const struct icon *); +static void update_enable(const struct icon *); +static void update_disable(const struct icon *); +static void clicked_icon(GtkButton *, gpointer); + +static double left(double v, double b); +static double right(double v, double b); +static double volume(double l, double r); +static double balance(double l, double r); + +static void volume_adjusted(GtkAdjustment *a, gpointer user_data); +static gchar *format_volume(GtkScale *scale, gdouble value); +static gchar *format_balance(GtkScale *scale, gdouble value); + +/* Control bar ------------------------------------------------------------- */ + +static int suppress_set_volume; +/* Guard against feedback loop in volume control */ + +static struct icon { + const char *icon; + const char *tip; + void (*clicked)(GtkButton *button, gpointer userdata); + void (*update)(const struct icon *i); + int (*action)(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); + GtkWidget *button; +} icons[] = { + { "pause.png", "Pause playing track", clicked_icon, update_pause, + disorder_eclient_pause, 0 }, + { "play.png", "Resume playing track", clicked_icon, update_play, + disorder_eclient_resume, 0 }, + { "cross.png", "Cancel playing track", clicked_icon, update_scratch, + disorder_eclient_scratch_playing, 0 }, + { "random.png", "Enable random play", clicked_icon, update_random_enable, + disorder_eclient_random_enable, 0 }, + { "randomcross.png", "Disable random play", clicked_icon, update_random_disable, + disorder_eclient_random_disable, 0 }, + { "notes.png", "Enable play", clicked_icon, update_enable, + disorder_eclient_enable, 0 }, + { "notescross.png", "Disable play", clicked_icon, update_disable, + disorder_eclient_disable, 0 }, +}; +#define NICONS (int)(sizeof icons / sizeof *icons) + +GtkAdjustment *volume_adj, *balance_adj; + +/* Create the control bar */ + GtkWidget *control_widget(void) { + GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox; + GtkWidget *content; + GdkPixbuf *pb; + GtkWidget *v, *b; + GtkTooltips *tips = gtk_tooltips_new(); + int n; + + D(("control_widget")); + for(n = 0; n < NICONS; ++n) { + icons[n].button = gtk_button_new(); + if((pb = find_image(icons[n].icon))) + content = gtk_image_new_from_pixbuf(pb); + else + content = gtk_label_new(icons[n].icon); + gtk_container_add(GTK_CONTAINER(icons[n].button), content); + gtk_tooltips_set_tip(tips, icons[n].button, icons[n].tip, ""); + g_signal_connect(G_OBJECT(icons[n].button), "clicked", + G_CALLBACK(icons[n].clicked), &icons[n]); + /* pop the icon in a vbox so it doesn't get vertically stretch if there are + * taller things in the control bar */ + vbox = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), icons[n].button, TRUE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0); + } + /* create the adjustments for the volume control */ + volume_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, goesupto, + goesupto / 20, goesupto / 20, + 0)); + balance_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, -1, 1, + 0.2, 0.2, 0)); + /* the volume control */ + v = gtk_hscale_new(volume_adj); + b = gtk_hscale_new(balance_adj); + gtk_scale_set_digits(GTK_SCALE(v), 10); + gtk_scale_set_digits(GTK_SCALE(b), 10); + gtk_widget_set_size_request(v, 192, -1); + gtk_widget_set_size_request(b, 192, -1); + gtk_tooltips_set_tip(tips, v, "Volume", ""); + gtk_tooltips_set_tip(tips, b, "Balance", ""); + gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), b, FALSE, TRUE, 0); + /* space updates rather than hammering the server */ + gtk_range_set_update_policy(GTK_RANGE(v), GTK_UPDATE_DELAYED); + gtk_range_set_update_policy(GTK_RANGE(b), GTK_UPDATE_DELAYED); + /* notice when the adjustments are changed */ + g_signal_connect(G_OBJECT(volume_adj), "value-changed", + G_CALLBACK(volume_adjusted), 0); + g_signal_connect(G_OBJECT(balance_adj), "value-changed", + G_CALLBACK(volume_adjusted), 0); + /* format the volume/balance values ourselves */ + g_signal_connect(G_OBJECT(v), "format-value", + G_CALLBACK(format_volume), 0); + g_signal_connect(G_OBJECT(b), "format-value", + G_CALLBACK(format_balance), 0); + return hbox; +} + +/* Update the control bar after some kind of state change */ +void control_update(void) { + int n; + double l, r; + + D(("control_update")); + for(n = 0; n < NICONS; ++n) + icons[n].update(&icons[n]); + l = volume_l / 100.0; + r = volume_r / 100.0; + ++suppress_set_volume;; + gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto); + gtk_adjustment_set_value(balance_adj, balance(l, r)); + --suppress_set_volume; +} + +static void update_icon(GtkWidget *button, + int visible, int attribute((unused)) usable) { + (visible ? gtk_widget_show : gtk_widget_hide)(button); + /* TODO: show usability */ +} + +static void update_pause(const struct icon *icon) { + int visible = !(last_state & DISORDER_TRACK_PAUSED); + int usable = playing; /* TODO: might be a lie */ + update_icon(icon->button, visible, usable); +} + +static void update_play(const struct icon *icon) { + int visible = !!(last_state & DISORDER_TRACK_PAUSED); + int usable = playing; + update_icon(icon->button, visible, usable); +} + +static void update_scratch(const struct icon *icon) { + int visible = 1; + int usable = playing; + update_icon(icon->button, visible, usable); +} + +static void update_random_enable(const struct icon *icon) { + int visible = !(last_state & DISORDER_RANDOM_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_random_disable(const struct icon *icon) { + int visible = !!(last_state & DISORDER_RANDOM_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_enable(const struct icon *icon) { + int visible = !(last_state & DISORDER_PLAYING_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_disable(const struct icon *icon) { + int visible = !!(last_state & DISORDER_PLAYING_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void clicked_icon(GtkButton attribute((unused)) *button, + gpointer userdata) { + const struct icon *icon = userdata; + + icon->action(client, 0, 0); +} + +static void volume_adjusted(GtkAdjustment attribute((unused)) *a, + gpointer attribute((unused)) user_data) { + double v = gtk_adjustment_get_value(volume_adj) / goesupto; + double b = gtk_adjustment_get_value(balance_adj); + + if(suppress_set_volume) + /* This is the result of an update from the server, not a change from the + * user. Don't feedback! */ + return; + D(("volume_adjusted")); + /* force to 'stereotypical' values */ + v = nearbyint(100 * v) / 100; + b = nearbyint(5 * b) / 5; + /* Set the volume. We don't want a reply, we'll get the actual new volume + * from the log. */ + disorder_eclient_volume(client, 0, + nearbyint(left(v, b) * 100), + nearbyint(right(v, b) * 100), + 0); +} + +/* Called to format the volume value */ +static gchar *format_volume(GtkScale attribute((unused)) *scale, + gdouble value) { + char s[32]; + + snprintf(s, sizeof s, "%.1f", (double)value); + return xstrdup(s); +} + +/* Called to format the balance value. */ +static gchar *format_balance(GtkScale attribute((unused)) *scale, + gdouble value) { + char s[32]; + + if(fabs(value) < 0.1) + return xstrdup("0"); + snprintf(s, sizeof s, "%+.1f", (double)value); + return xstrdup(s); +} + +/* Volume mapping. We consider left, right, volume to be in [0,1] + * and balance to be in [-1,1]. + * + * First, we just have volume = max(left, right). + * + * Balance we consider to linearly represent the amount by which the quieter + * channel differs from the louder. In detail: + * + * if right > left then balance > 0: + * balance = 0 => left = right (as an endpoint, not an instance) + * balance = 1 => left = 0 + * fitting to linear, left = right * (1 - balance) + * so balance = 1 - left / right + * (right > left => right > 0 so no division by 0.) + * + * if left > right then balance < 0: + * balance = 0 => right = left (same caveat as above) + * balance = -1 => right = 0 + * again fitting to linear, right = left * (1 + balance) + * so balance = right / left - 1 + * (left > right => left > 0 so no division by 0.) + * + * if left = right then we just have balance = 0. + * + * Thanks to Clive and Andrew. + */ + +static double max(double x, double y) { + return x > y ? x : y; +} + +static double left(double v, double b) { + if(b > 0) /* volume = right */ + return v * (1 - b); + else /* volume = left */ + return v; +} + +static double right(double v, double b) { + if(b > 0) /* volume = right */ + return v; + else /* volume = left */ + return v * (1 + b); +} + +static double volume(double l, double r) { + return max(l, r); +} + +static double balance(double l, double r) { + if(l > r) + return r / l - 1; + else if(r > l) + return 1 - l / r; + else /* left = right */ + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:IEbGnYlX8cqOFjY1EXlXBA */ diff --git a/disobedience/disobedience.c b/disobedience/disobedience.c new file mode 100644 index 0000000..dc8c27e --- /dev/null +++ b/disobedience/disobedience.c @@ -0,0 +1,379 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +#include <getopt.h> +#include <locale.h> + +/* Apologies for the numerous de-consting casts, but GLib et al do not seem to + * have heard of const. */ + +#include "style.h" /* generated style */ + +/* Functions --------------------------------------------------------------- */ + +static void log_connected(void *v); +static void log_completed(void *v, const char *track); +static void log_failed(void *v, const char *track, const char *status); +static void log_moved(void *v, const char *user); +static void log_playing(void *v, const char *track, const char *user); +static void log_queue(void *v, struct queue_entry *q); +static void log_recent_added(void *v, struct queue_entry *q); +static void log_recent_removed(void *v, const char *id); +static void log_removed(void *v, const char *id, const char *user); +static void log_scratched(void *v, const char *track, const char *user); +static void log_state(void *v, unsigned long state); +static void log_volume(void *v, int l, int r); + +/* Variables --------------------------------------------------------------- */ + +GMainLoop *mainloop; /* event loop */ +GtkWidget *toplevel; /* top level window */ +GtkWidget *report_label; /* label for progress indicator */ +GtkWidget *tabs; /* main tabs */ + +disorder_eclient *client; /* main client */ + +unsigned long last_state; /* last reported state */ +int playing; /* true if playing some track */ +int volume_l, volume_r; /* volume */ +double goesupto = 10; /* volume upper bound */ +int choosealpha; /* break up choose by letter */ + +static const GMemVTable glib_memvtable = { + xmalloc, + xrealloc, + xfree, + 0, /* calloc */ + 0, /* try_malloc */ + 0 /* try_realloc */ +}; + +static const disorder_eclient_log_callbacks gdisorder_log_callbacks = { + log_connected, + log_completed, + log_failed, + log_moved, + log_playing, + log_queue, + log_recent_added, + log_recent_removed, + log_removed, + log_scratched, + log_state, + log_volume +}; + +/* Window creation --------------------------------------------------------- */ + +/* Note that all the client operations kicked off from here will only complete + * after we've entered the event loop. */ + +static gboolean delete_event(GtkWidget attribute((unused)) *widget, + GdkEvent attribute((unused)) *event, + gpointer attribute((unused)) data) { + D(("delete_event")); + exit(0); /* die immediately */ +} + +/* Called when the current tab is switched. */ +static void tab_switched(GtkNotebook attribute((unused)) *notebook, + GtkNotebookPage attribute((unused)) *page, + guint page_num, + gpointer attribute((unused)) user_data) { + menu_update(page_num); +} + +static GtkWidget *report_box(void) { + GtkWidget *vbox = gtk_vbox_new(FALSE, 0); + + report_label = gtk_label_new(""); + gtk_label_set_ellipsize(GTK_LABEL(report_label), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment(GTK_MISC(report_label), 0, 0); + gtk_container_add(GTK_CONTAINER(vbox), gtk_hseparator_new()); + gtk_container_add(GTK_CONTAINER(vbox), report_label); + return vbox; +} + +static GtkWidget *notebook(void) { + tabs = gtk_notebook_new(); + g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(), + gtk_label_new("Queue")); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(), + gtk_label_new("Recent")); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), choose_widget(), + gtk_label_new("Choose")); + return tabs; +} + +static void make_toplevel_window(void) { + GtkWidget *const vbox = gtk_vbox_new(FALSE, 1); + GtkWidget *const rb = report_box(); + + D(("top_window")); + toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL); + /* default size is too small */ + gtk_window_set_default_size(GTK_WINDOW(toplevel), 640, 480); + /* terminate on close */ + g_signal_connect(G_OBJECT(toplevel), "delete_event", + G_CALLBACK(delete_event), NULL); + /* lay out the window */ + gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience"); + gtk_container_add(GTK_CONTAINER(toplevel), vbox); + /* lay out the vbox */ + gtk_box_pack_start(GTK_BOX(vbox), + menubar(toplevel), + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_box_pack_start(GTK_BOX(vbox), + control_widget(), + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_container_add(GTK_CONTAINER(vbox), notebook()); + gtk_box_pack_end(GTK_BOX(vbox), + rb, + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_widget_set_name(toplevel, "disobedience"); +} + +/* State monitoring -------------------------------------------------------- */ + +static void all_update(void) { + playing_update(); + queue_update(); + recent_update(); + control_update(); +} + +static void log_connected(void attribute((unused)) *v) { + struct callbackdata *cbd; + + /* Don't know what we might have missed while disconnected so update + * everything. We get this at startup too and this is how we do the initial + * state fetch. */ + all_update(); + /* Re-get the volume */ + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + disorder_eclient_volume(client, log_volume, -1, -1, cbd); +} + +static void log_completed(void attribute((unused)) *v, + const char attribute((unused)) *track) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_failed(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *status) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_moved(void attribute((unused)) *v, + const char attribute((unused)) *user) { + queue_update(); +} + +static void log_playing(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *user) { + playing = 1; + playing_update(); + control_update(); + /* we get a log_removed() anyway so we don't need to update_queue() from + * here */ +} + +static void log_queue(void attribute((unused)) *v, + struct queue_entry attribute((unused)) *q) { + queue_update(); +} + +static void log_recent_added(void attribute((unused)) *v, + struct queue_entry attribute((unused)) *q) { + recent_update(); +} + +static void log_recent_removed(void attribute((unused)) *v, + const char attribute((unused)) *id) { + /* nothing - log_recent_added() will trigger the relevant update */ +} + +static void log_removed(void attribute((unused)) *v, + const char attribute((unused)) *id, + const char attribute((unused)) *user) { + queue_update(); +} + +static void log_scratched(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *user) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_state(void attribute((unused)) *v, + unsigned long state) { + last_state = state; + control_update(); + /* If the track is paused or resume then the currently playing track is + * refetched so that we can continue to correctly calculate the played so-far + * field */ + playing_update(); +} + +static void log_volume(void attribute((unused)) *v, + int l, int r) { + if(volume_l != l || volume_r != r) { + volume_l = l; + volume_r = r; + control_update(); + } +} + +/* Called once every 10 minutes */ +static gboolean periodic(gpointer attribute((unused)) data) { + D(("periodic")); + /* Expire cached data */ + cache_expire(); + /* Update everything to be sure that the connection to the server hasn't + * mysteriously gone stale on us. */ + all_update(); + return TRUE; /* don't remove me */ +} + +static gboolean heartbeat(gpointer attribute((unused)) data) { + static struct timeval last; + struct timeval now; + double delta; + + xgettimeofday(&now, 0); + if(last.tv_sec) { + delta = (now.tv_sec + now.tv_sec / 1.0E6) + - (last.tv_sec + last.tv_sec / 1.0E6); + if(delta >= 1.0625) + fprintf(stderr, "%f: %fs between 1s heartbeats\n", + now.tv_sec + now.tv_sec / 1.0E6, + delta); + } + last = now; + return TRUE; +} + +/* main -------------------------------------------------------------------- */ + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "config", required_argument, 0, 'c' }, + { "tufnel", no_argument, 0, 't' }, + { "choosealpha", no_argument, 0, 'C' }, + { "debug", no_argument, 0, 'd' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Disobedience - GUI client for DisOrder\n" + "\n" + "Usage:\n" + " disobedience [OPTIONS]\n" + "Options:\n" + " --help, -h Display usage message\n" + " --version, -V Display version number\n" + " --config PATH, -c PATH Set configuration file\n" + " --debug, -d Turn on debugging\n" + "\n" + "Also GTK+ options will work.\n"); + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorder version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +int main(int argc, char **argv) { + int n; + disorder_eclient *logclient; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + /* GLib sucks - not const-correct */ + g_mem_set_vtable((GMemVTable *)&glib_memvtable); + gtk_init(&argc, &argv); + gtk_rc_parse_string(style); + while((n = getopt_long(argc, argv, "hVc:dtH", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'V': version(); + case 'c': configfile = optarg; break; + case 'd': debugging = 1; break; + case 't': goesupto = 11; break; + case 'C': choosealpha = 1; break; /* not well tested any more */ + default: fatal(0, "invalid option"); + } + } + /* create the event loop */ + D(("create main loop")); + mainloop = g_main_loop_new(0, 0); + if(config_read()) fatal(0, "cannot read configuration"); + /* create the clients */ + client = gtkclient(); + logclient = gtkclient(); + disorder_eclient_log(logclient, &gdisorder_log_callbacks, 0); + /* periodic operations (e.g. expiring the cache) */ + g_timeout_add(600000/*milliseconds*/, periodic, 0); + /* The point of this is to try and get a handle on mysterious + * unresponsiveness. It's not very useful in production use. */ + if(0) + g_timeout_add(1000/*milliseconds*/, heartbeat, 0); + make_toplevel_window(); + /* reset styles now everything has its name */ + gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default())); + gtk_widget_show_all(toplevel); + D(("enter main loop")); + g_main_loop_run(mainloop); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:dJmEdDzrCQktkNJWAmdQAQ */ diff --git a/disobedience/disobedience.h b/disobedience/disobedience.h new file mode 100644 index 0000000..a63cf44 --- /dev/null +++ b/disobedience/disobedience.h @@ -0,0 +1,180 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DISOBEDIENCE_H +#define DISOBEDIENCE_H + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <time.h> +#include <string.h> +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <math.h> + +#include "mem.h" +#include "log.h" +#include "eclient.h" +#include "printf.h" +#include "cache.h" +#include "queue.h" +#include "printf.h" +#include "vector.h" +#include "trackname.h" +#include "syscalls.h" +#include "defs.h" +#include "configuration.h" +#include "hash.h" +#include "selection.h" + +#include <glib.h> +#include <gtk/gtk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +/* Types ------------------------------------------------------------------- */ + +struct queuelike; +struct choosenode; + +struct callbackdata { + void (*onerror)(struct callbackdata *cbd, + int code, + const char *msg); /* called on error */ + union { + const char *key; /* gtkqueue.c op_part_lookup */ + struct choosenode *choosenode; /* gtkchoose.c got_files/got_dirs */ + struct queuelike *ql; /* gtkqueue.c queuelike_completed */ + struct prefdata *f; /* properties.c */ + } u; +}; + +struct tabtype { + int (*properties_sensitive)(GtkWidget *tab); + int (*selectall_sensitive)(GtkWidget *tab); + void (*properties_activate)(GtkWidget *tab); + void (*selectall_activate)(GtkWidget *tab); +}; + +/* Variables --------------------------------------------------------------- */ + +extern GMainLoop *mainloop; +extern GtkWidget *toplevel; /* top level window */ +extern GtkWidget *report_label; /* label for progress indicator */ +extern GtkWidget *tabs; /* main tabs */ +extern disorder_eclient *client; /* main client */ + +extern unsigned long last_state; /* last reported state */ +extern int playing; /* true if playing some track */ +extern int volume_l, volume_r; /* current volume */ +extern double goesupto; /* volume upper bound */ +extern int choosealpha; /* break up choose by letter */ + +/* Functions --------------------------------------------------------------- */ + +disorder_eclient *gtkclient(void); +/* Configure C for use in GTK+ programs */ + +void popup_protocol_error(int code, + const char *msg); +/* Report an error */ + +void properties(int ntracks, char **tracks); +/* Pop up a properties window for a list of tracks */ + +GtkWidget *scroll_widget(GtkWidget *child, const char *name); +/* Wrap a widget up for scrolling */ + +GdkPixbuf *find_image(const char *name); +/* Get the pixbuf for an image. Returns a null pointer if it cannot be + * found. */ + +void popup_error(const char *msg); +/* Pop up an error message */ + + +/* Main menu */ + +GtkWidget *menubar(GtkWidget *w); +/* Create the menu bar */ + +void menu_update(int page); +/* Called whenever the main menu might need to change. PAGE is the current + * page if known or -1 otherwise. */ + + +/* Controls */ + +GtkWidget *control_widget(void); +/* Make the controls widget */ + +void control_update(void); +/* Called whenever we think the control widget needs changing */ + + +/* Queue/Recent */ + +GtkWidget *queue_widget(void); +GtkWidget *recent_widget(void); +/* Create widgets for displaying the queue and the recently played list */ + +void queue_update(void); +void recent_update(void); +/* Called whenever we think the queue or recent list might have chanegd */ + +void queue_select_all(struct queuelike *ql); +/* Select all on some queue */ + +void queue_properties(struct queuelike *ql); +/* Pop up properties of selected items in some queue */ + +void playing_update(void); +/* Called whenever we think the currently playing track might have changed */ + +int queued(const char *track); +/* Return nonzero iff TRACK is queued or playing */ + +void namepart_update(const char *track, + const char *context, + const char *part); +/* Called when a namepart might have changed */ + + +/* Choose */ + +GtkWidget *choose_widget(void); +/* Create a widget for choosing tracks */ + +void choose_update(void); +/* Called when we think the choose tree might need updating */ + +#endif /* DISOBEDIENCE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:5DbN8e67AvkhPmNqSQyZFQ */ diff --git a/disobedience/disobedience.rc b/disobedience/disobedience.rc new file mode 100644 index 0000000..ed2710c --- /dev/null +++ b/disobedience/disobedience.rc @@ -0,0 +1,78 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +# Default style for Disobedience +# +# See e.g. http://developer.gnome.org/doc/API/2.0/gtk/gtk-Resource-Files.html +# for syntax documentation. + +style "disobedience-default" +{ + bg[NORMAL] = { 1.0, 1.0, 1.0 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } + bg[PRELIGHT] = { 1.0, 1.0, 1.0 } + fg[PRELIGHT] = { 1.0, 1.0, 1.0 } +} + +style "disobedience-playing" +{ + bg[NORMAL] = { 0.875, 1.0, 0.875 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-even" +{ + bg[NORMAL] = { 1.0, 0.921875, 0.921875 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-odd" +{ + bg[NORMAL] = { 1.0, 1.0, 1.0 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-title" +{ + bg[NORMAL] = { 0.0, 0.0, 0.0 } + fg[NORMAL] = { 1.0, 1.0, 1.0 } + font_name = "Bold" +} + +style "disobedience-drag" +{ + bg[NORMAL] = { 0.4, 0.4, 0.4 } + fg[NORMAL] = { 1.0, 1.0, 1.0 } +} + +# The main tabs +widget "disobedience.*.choose" style : application "disobedience-default" +widget "disobedience.*.queue" style : application "disobedience-default" +widget "disobedience.*.recent" style : application "disobedience-default" + +# Drag target +widget "disobedience.*.queue-drag" style : application "disobedience-drag" + +# Rows in the queue/recent tabs +widget "disobedience.*.row-playing" style : application "disobedience-playing" +widget "disobedience.*.row-even" style : application "disobedience-even" +widget "disobedience.*.row-odd" style : application "disobedience-odd" +widget "disobedience.*.row-title" style : application "disobedience-title" +# arch-tag:kFBxSkTwM9fRlwC/NTqR4A diff --git a/disobedience/menu.c b/disobedience/menu.c new file mode 100644 index 0000000..6eaea06 --- /dev/null +++ b/disobedience/menu.c @@ -0,0 +1,150 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +static GtkWidget *selectall_widget; +static GtkWidget *properties_widget; + +static void about_popup_got_version(void *v, const char *value); + +static void quit_program(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + D(("quit_program")); + exit(0); +} + +/* TODO can we have a single parameterized callback for all these */ +static void select_all(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs))); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + t->selectall_activate(tab); +} + +static void properties_item(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs))); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + t->properties_activate(tab); +} + +void menu_update(int page) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), + page < 0 ? gtk_notebook_current_page(GTK_NOTEBOOK(tabs)) : page); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + assert(t != 0); + gtk_widget_set_sensitive(properties_widget, + t->properties_sensitive(tab)); + gtk_widget_set_sensitive(selectall_widget, + t->selectall_sensitive(tab)); +} + +static void about_popup(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + D(("about_popup")); + + gtk_label_set_text(GTK_LABEL(report_label), "getting server version"); + disorder_eclient_version(client, + about_popup_got_version, + 0); +} + +static void about_popup_got_version(void attribute((unused)) *v, + const char *value) { + GtkWidget *w; + char *server_version_string; + + byte_xasprintf(&server_version_string, "Server version %s", value); + w = gtk_dialog_new_with_buttons("About DisOrder", + GTK_WINDOW(toplevel), + (GTK_DIALOG_MODAL + |GTK_DIALOG_DESTROY_WITH_PARENT), + GTK_STOCK_OK, + GTK_RESPONSE_ACCEPT, + (char *)NULL); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new("DisOrder client " VERSION)); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new(server_version_string)); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new("(c) 2004-2006 Richard Kettlewell")); + gtk_widget_show_all(w); + gtk_dialog_run(GTK_DIALOG(w)); + gtk_widget_destroy(w); +} + +GtkWidget *menubar(GtkWidget *w) { + static const GtkItemFactoryEntry entries[] = { + { (char *)"/File", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/File/Quit", (char *)"<CTRL>Q", quit_program, 0, + (char *)"<StockItem>", GTK_STOCK_QUIT }, + { (char *)"/Edit", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/Edit/Select All", (char *)"<CTRL>A", select_all, 0, + 0, 0 }, + { (char *)"/Edit/Properties", 0, properties_item, 0, + 0, 0 }, + { (char *)"/Help", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/Help/About DisOrder", 0, about_popup, 0, + (char *)"<StockItem>", GTK_STOCK_ABOUT }, + }; + + GtkItemFactory *itemfactory; + GtkAccelGroup *accel = gtk_accel_group_new(); + + D(("add_menubar")); + /* TODO: item factories are deprecated in favour of some XML thing */ + itemfactory = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<GdisorderMain>", + accel); + gtk_item_factory_create_items(itemfactory, + sizeof entries / sizeof *entries, + (GtkItemFactoryEntry *)entries, + 0); + gtk_window_add_accel_group(GTK_WINDOW(w), accel); + selectall_widget = gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>/Edit/Select All"); + properties_widget = gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>/Edit/Properties"); + assert(selectall_widget != 0); + assert(properties_widget != 0); + return gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>"); + /* menu bar had better not expand vertically if the window is too big */ +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:3vGhvsh3YABCyUS65pvmVA */ diff --git a/disobedience/misc.c b/disobedience/misc.c new file mode 100644 index 0000000..26222a9 --- /dev/null +++ b/disobedience/misc.c @@ -0,0 +1,97 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Miscellaneous GTK+ stuff ------------------------------------------------ */ + +/* Functions */ + +GtkWidget *scroll_widget(GtkWidget *child, + const char *widgetname) { + GtkWidget *scroller = gtk_scrolled_window_new(0, 0); + GtkAdjustment *adj; + + D(("scroll_widget")); + /* Why isn't _AUTOMATIC the default? */ + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + if(GTK_IS_LAYOUT(child)) { + /* Child widget has native scroll support */ + gtk_container_add(GTK_CONTAINER(scroller), child); + /* Fix up the step increments if they are 0 (seems like an odd default?) */ + if(GTK_IS_LAYOUT(child)) { + adj = gtk_layout_get_hadjustment(GTK_LAYOUT(child)); + if(!adj->step_increment) adj->step_increment = 16; + adj = gtk_layout_get_vadjustment(GTK_LAYOUT(child)); + if(!adj->step_increment) adj->step_increment = 16; + } + } else { + /* Child widget requires a viewport */ + gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scroller), + child); + } + /* Apply a name to the widget so it can be recolored */ + gtk_widget_set_name(GTK_BIN(scroller)->child, widgetname); + gtk_widget_set_name(scroller, widgetname); + return scroller; +} + +GdkPixbuf *find_image(const char *name) { + static const struct cache_type image_cache_type = { INT_MAX }; + + GdkPixbuf *pb; + char *path; + GError *err = 0; + + if(!(pb = (GdkPixbuf *)cache_get(&image_cache_type, name))) { + byte_xasprintf(&path, "%s/static/%s", pkgdatadir, name); + if(!(pb = gdk_pixbuf_new_from_file(path, &err))) { + error(0, "%s", err->message); + return 0; + } + cache_put(&image_cache_type, name, pb); + } + return pb; +} + +void popup_error(const char *msg) { + GtkWidget *w; + + w = gtk_message_dialog_new(GTK_WINDOW(toplevel), + GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", msg); + gtk_dialog_run(GTK_DIALOG(w)); + gtk_widget_destroy(w); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ + +/* arch-tag:tV2wkVwV3p2A66vyViQJSw */ diff --git a/disobedience/properties.c b/disobedience/properties.c new file mode 100644 index 0000000..ae945be --- /dev/null +++ b/disobedience/properties.c @@ -0,0 +1,438 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Track properties -------------------------------------------------------- */ + +struct prefdata; + +static void kickoff_namepart(struct prefdata *f); +static void completed_namepart(struct prefdata *f); +static const char *get_edited_namepart(struct prefdata *f); +static void set_namepart(struct prefdata *f, const char *value); +static void set_namepart_completed(void *v); + +static void kickoff_string(struct prefdata *f); +static void completed_string(struct prefdata *f); +static const char *get_edited_string(struct prefdata *f); +static void set_string(struct prefdata *f, const char *value); + +static void kickoff_boolean(struct prefdata *f); +static void completed_boolean(struct prefdata *f); +static const char *get_edited_boolean(struct prefdata *f); +static void set_boolean(struct prefdata *f, const char *value); + +static void prefdata_completed(void *v, const char *value); +static void prefdata_onerror(struct callbackdata *cbd, + int code, + const char *msg); +static struct callbackdata *make_callbackdata(struct prefdata *f); +static void prefdata_completed_common(struct prefdata *f, + const char *value); + +static void properties_ok(GtkButton *button, gpointer userdata); +static void properties_apply(GtkButton *button, gpointer userdata); +static void properties_cancel(GtkButton *button, gpointer userdata); + +/* Data for a single preference */ +struct prefdata { + const char *track; + int row; + const struct pref *p; + const char *value; + GtkWidget *widget; +}; + +/* The type of a preference is the collection of callbacks needed to get, + * display and set it */ +struct preftype { + void (*kickoff)(struct prefdata *f); + /* Kick off the request to fetch the pref from the server. */ + + void (*completed)(struct prefdata *f); + /* Called when the value comes back in; creates the widget. */ + + const char *(*get_edited)(struct prefdata *f); + /* Get the edited value from the widget. */ + + void (*set)(struct prefdata *f, const char *value); + /* Set the new value and (if necessary) arrange for our display to update. */ +}; + +/* A namepart pref */ +static const struct preftype preftype_namepart = { + kickoff_namepart, + completed_namepart, + get_edited_namepart, + set_namepart +}; + +/* A string pref */ +static const struct preftype preftype_string = { + kickoff_string, + completed_string, + get_edited_string, + set_string +}; + +/* A boolean pref */ +static const struct preftype preftype_boolean = { + kickoff_boolean, + completed_boolean, + get_edited_boolean, + set_boolean +}; + +/* The known prefs for each track */ +static const struct pref { + const char *label; + const char *part; + const char *default_value; + const struct preftype *type; +} prefs[] = { + { "Artist", "artist", 0, &preftype_namepart }, + { "Album", "album", 0, &preftype_namepart }, + { "Title", "title", 0, &preftype_namepart }, + { "Tags", "tags", "", &preftype_string }, + { "Random", "pick_at_random", "1", &preftype_boolean }, +}; + +#define NPREFS (int)(sizeof prefs / sizeof *prefs) + +/* Buttons that appear at the bottom of the window */ +static const struct button { + const gchar *stock; + void (*clicked)(GtkButton *button, gpointer userdata); +} buttons[] = { + { GTK_STOCK_OK, properties_ok }, + { GTK_STOCK_APPLY, properties_apply }, + { GTK_STOCK_CANCEL, properties_cancel }, +}; + +#define NBUTTONS (int)(sizeof buttons / sizeof *buttons) + +static int prefs_unfilled; /* Prefs remaining to get */ +static int prefs_total; /* Total prefs */ +static struct prefdata *prefdatas; /* Current prefdatas */ +static GtkWidget *properties_window; +static GtkWidget *properties_table; +static GtkWidget *progress_window, *progress_bar; + +void properties(int ntracks, char **tracks) { + int n, m; + struct prefdata *f; + GtkWidget *hbox, *vbox, *button, *label, *entry; + + /* If there is a properties window open then just bring it to the + * front. It might not have the right values in... */ + if(properties_window) { + if(!prefs_unfilled) + gtk_window_present(GTK_WINDOW(properties_window)); + return; + } + assert(properties_table == 0); + if(ntracks > INT_MAX / NPREFS) { + popup_error("Too many tracks selected"); + return; + } + /* Create a new properties window */ + properties_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(properties_window, "destroy", + G_CALLBACK(gtk_widget_destroyed), &properties_window); + /* Most of the action is the table of preferences */ + properties_table = gtk_table_new((NPREFS + 1) * ntracks, 2, FALSE); + g_signal_connect(properties_table, "destroy", + G_CALLBACK(gtk_widget_destroyed), &properties_table); + gtk_window_set_title(GTK_WINDOW(properties_window), "Track Properties"); + /* Create labels for each pref of each track and kick off requests to the + * server to fill in the values */ + prefs_total = NPREFS * ntracks; + prefdatas = xcalloc(prefs_total, sizeof *prefdatas); + for(n = 0; n < ntracks; ++n) { + label = gtk_label_new("Track"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0); + gtk_table_attach(GTK_TABLE(properties_table), + label, + 0, 1, + (NPREFS + 1) * n, (NPREFS + 1) * n + 1, + GTK_FILL, 0, + 1, 1); + entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(entry), tracks[n]); + gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE); + gtk_table_attach(GTK_TABLE(properties_table), + entry, + 1, 2, + (NPREFS + 1) * n, (NPREFS + 1) * n + 1, + GTK_EXPAND|GTK_FILL, 0, + 1, 1); + for(m = 0; m < NPREFS; ++m) { + label = gtk_label_new(prefs[m].label); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0); + gtk_table_attach(GTK_TABLE(properties_table), + label, + 0, 1, + (NPREFS + 1) * n + 1 + m, (NPREFS + 1) * n + 2 + m, + GTK_FILL/*xoptions*/, 0/*yoptions*/, + 1, 1); + f = &prefdatas[NPREFS * n + m]; + f->track = tracks[n]; + f->row = (NPREFS + 1) * n + 1 + m; + f->p = &prefs[m]; + prefs[m].type->kickoff(f); + } + } + prefs_unfilled = prefs_total; + /* Buttons */ + hbox = gtk_hbox_new(FALSE, 1); + for(n = 0; n < NBUTTONS; ++n) { + button = gtk_button_new_from_stock(buttons[n].stock); + g_signal_connect(G_OBJECT(button), "clicked", + G_CALLBACK(buttons[n].clicked), 0); + gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 1); + } + /* Put it all together */ + vbox = gtk_vbox_new(FALSE, 1); + gtk_box_pack_start(GTK_BOX(vbox), + scroll_widget(properties_table, + "properties"), + TRUE, TRUE, 1); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 1); + gtk_container_add(GTK_CONTAINER(properties_window), vbox); + /* The table only really wants to be vertically scrollable */ + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(GTK_WIDGET(properties_table)->parent->parent), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + /* Pop up a progress bar while we're waiting */ + progress_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(progress_window, "destroy", + G_CALLBACK(gtk_widget_destroyed), &progress_window); + gtk_window_set_default_size(GTK_WINDOW(progress_window), 360, -1); + gtk_window_set_title(GTK_WINDOW(progress_window), + "Fetching Track Properties"); + progress_bar = gtk_progress_bar_new(); + gtk_container_add(GTK_CONTAINER(progress_window), progress_bar); + gtk_widget_show_all(progress_window); +} + +/* Everything is filled in now */ +static void prefdata_alldone(void) { + if(progress_window) + gtk_widget_destroy(progress_window); + /* Default size may be too small */ + gtk_window_set_default_size(GTK_WINDOW(properties_window), 480, 512); + /* TODO: relate default size to required size more closely */ + gtk_widget_show_all(properties_window); +} + +/* Namepart preferences ---------------------------------------------------- */ + +static void kickoff_namepart(struct prefdata *f) { + char *s; + + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + disorder_eclient_get(client, prefdata_completed, f->track, s, + make_callbackdata(f)); +} + +static void completed_namepart(struct prefdata *f) { + if(!f->value) + /* No setting, use the computed default value instead */ + f->value = trackname_part(f->track, "display", f->p->part); + f->widget = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(f->widget), f->value); +} + +static const char *get_edited_namepart(struct prefdata *f) { + return gtk_entry_get_text(GTK_ENTRY(f->widget)); +} + +static void set_namepart(struct prefdata *f, const char *value) { + char *s; + struct callbackdata *cbd = xmalloc(sizeof *cbd); + + cbd->u.f = f; + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + if(strcmp(trackname_part(f->track, "display", f->p->part), value)) + /* Different from default, set it */ + disorder_eclient_set(client, set_namepart_completed, f->track, s, value, + cbd); + else + /* Same as default, just unset */ + disorder_eclient_unset(client, set_namepart_completed, f->track, s, cbd); +} + +/* Called when we've set a namepart */ +static void set_namepart_completed(void *v) { + struct callbackdata *cbd = v; + struct prefdata *f = cbd->u.f; + + namepart_update(f->track, "display", f->p->part); +} + +/* String preferences ------------------------------------------------------ */ + +static void kickoff_string(struct prefdata *f) { + disorder_eclient_get(client, prefdata_completed, f->track, f->p->part, + make_callbackdata(f)); +} + +static void completed_string(struct prefdata *f) { + if(!f->value) + /* No setting, use the default value instead */ + f->value = f->p->default_value; + f->widget = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(f->widget), f->value); +} + +static const char *get_edited_string(struct prefdata *f) { + return gtk_entry_get_text(GTK_ENTRY(f->widget)); +} + +static void set_string(struct prefdata *f, const char *value) { + if(strcmp(f->p->default_value, value)) + /* Different from default, set it */ + disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, + value, 0/*v*/); + else + /* Same as default, just unset */ + disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part, + 0/*v*/); +} + +/* Boolean preferences ----------------------------------------------------- */ + +static void kickoff_boolean(struct prefdata *f) { + disorder_eclient_get(client, prefdata_completed, f->track, f->p->part, + make_callbackdata(f)); +} + +static void completed_boolean(struct prefdata *f) { + f->widget = gtk_check_button_new(); + if(!f->value) + /* Not set, use the default */ + f->value = f->p->default_value; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(f->widget), + strcmp(f->value, "0")); +} + +static const char *get_edited_boolean(struct prefdata *f) { + return (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(f->widget)) + ? "1" : "0"); +} + +static void set_boolean(struct prefdata *f, const char *value) { + char *s; + + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + if(strcmp(value, f->p->default_value)) + disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, value, + 0/*v*/); + else + /* If default value then delete the pref */ + disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part, + 0/*v*/); +} + +/* Querying preferences ---------------------------------------------------- */ + +/* Make a suitable callbackdata */ +static struct callbackdata *make_callbackdata(struct prefdata *f) { + struct callbackdata *cbd = xmalloc(sizeof *cbd); + + cbd->onerror = prefdata_onerror; + cbd->u.f = f; + return cbd; +} + +/* No pref was set */ +static void prefdata_onerror(struct callbackdata *cbd, + int attribute((unused)) code, + const char attribute((unused)) *msg) { + prefdata_completed_common(cbd->u.f, 0); +} + +/* Got the value of a pref */ +static void prefdata_completed(void *v, const char *value) { + struct callbackdata *cbd = v; + + prefdata_completed_common(cbd->u.f, value); +} + +static void prefdata_completed_common(struct prefdata *f, + const char *value) { + f->value = value; + f->p->type->completed(f); + assert(f->value != 0); /* Had better set a default */ + gtk_table_attach(GTK_TABLE(properties_table), f->widget, + 1, 2, + f->row, f->row + 1, + GTK_EXPAND|GTK_FILL/*xoptions*/, 0/*yoptions*/, + 1, 1); + --prefs_unfilled; + if(prefs_total && progress_window) + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress_bar), + 1.0 - (double)prefs_unfilled / prefs_total); + if(!prefs_unfilled) + prefdata_alldone(); +} + +/* Button callbacks -------------------------------------------------------- */ + +static void properties_ok(GtkButton *button, + gpointer userdata) { + properties_apply(button, userdata); + properties_cancel(button, userdata); +} + +static void properties_apply(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + int n; + const char *edited; + struct prefdata *f; + + /* For each possible property we see if we've changed it and if so tell the + * server */ + for(n = 0; n < prefs_total; ++n) { + f = &prefdatas[n]; + edited = f->p->type->get_edited(f); + if(strcmp(edited, f->value)) { + /* The value has changed */ + f->p->type->set(f, edited); + f->value = xstrdup(edited); + } + } +} + +static void properties_cancel(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_widget_destroy(properties_window); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:+COG6p7PaNPZjzknPrKdcw */ diff --git a/disobedience/queue.c b/disobedience/queue.c new file mode 100644 index 0000000..2494505 --- /dev/null +++ b/disobedience/queue.c @@ -0,0 +1,1279 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +#define HCELLPADDING 4 +#define VCELLPADDING 2 + +/* A queue layout is structured as follows: + * + * vbox + * titlescroll + * titlelayout + * titlecells[col] eventbox (made by wrap_queue_cell) + * titlecells[col]->child label (from columns[]) + * mainscroll + * mainlayout + * cells[row * N + c] eventbox (made by wrap_queue_cell) + * cells[row * N + c]->child label (from column constructors) + * + * titlescroll never has any scrollbars. Instead whenever mainscroll's + * horizontal adjustment is changed, queue_scrolled adjusts titlescroll to + * match, forcing the title and the queue to pan in sync but allowing the queue + * to scroll independently. + * + * Whenever the queue changes everything below mainlayout is thrown away and + * reconstructed from scratch. Name lookups are cached, so this doesn't imply + * lots of disorder protocol traffic. + * + * The last cell on each row is the padding cell, and this extends ridiculously + * far to the right. (Can we do better?) + * + * When drag and drop is active we create extra eventboxes to act as dropzones. + * These only exist while the drag proceeds, as otherwise they steal events + * from more deserving widgets. (It might work to hide them when not in use + * too but this way around the d+d code is a bit more self-contained.) + */ + +/* Queue management -------------------------------------------------------- */ + +struct queuelike; + +static void add_drag_targets(struct queuelike *ql); +static void remove_drag_targets(struct queuelike *ql); +static void redisplay_queue(struct queuelike *ql); +static GtkWidget *column_when(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_who(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_namepart(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_length(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static int draggable_row(const struct queue_entry *q); + +static const struct tabtype tabtype_queue; /* forward */ + +static const GtkTargetEntry dragtargets[] = { + { (char *)"disobedience-queue", GTK_TARGET_SAME_APP, 0 } +}; +#define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets) + +/* Definition of a column */ +struct column { + const char *name; /* Column name */ + GtkWidget *(*widget)(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); /* Make a label for this column */ + const char *data; /* Data to pass to widget() */ + gfloat xalign; /* Alignment of the label */ +}; + +/* Need this in the middle of the types for NCOLUMNS */ +static const struct column columns[] = { + { "When", column_when, 0, 1 }, + { "Who", column_who, 0, 0 }, + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } +}; +#define NCOLUMNS (int)(sizeof columns / sizeof *columns) + +/* Data passed to menu item activation handlers */ +struct menuiteminfo { + struct queuelike *ql; /* which queue we're dealing with */ + struct queue_entry *q; /* hovered entry or 0 */ +}; + +struct menuitem { + /* Parameters */ + const char *name; /* name */ + + /* Callbacks */ + void (*activate)(GtkMenuItem *menuitem, + gpointer user_data); + /* Called to activate the menu item. The user data is the queue entry that + * the pointer was over when the menu popped up. */ + + int (*sensitive)(struct queuelike *ql, + struct menuitem *m, + struct queue_entry *q); + /* Called to determine whether the menu item is usable. Returns TRUE if it + * should be sensitive and FALSE otherwise. Q points to the queue entry the + * pointer is over. */ + + /* State */ + gulong handlerid; /* signal handler ID */ + GtkWidget *w; /* menu item widget */ +}; + +struct queuelike { + /* Parameters */ + const char *name; /* queue or recent */ + + /* Callbacks */ + void (*notify)(void); + /* Called when an update completes. */ + + struct queue_entry *(*fixup)(struct queue_entry *q); + /* Fix up the queue after update, or 0. Q is the list passed back from the + * server, the return value is assigned to ql->q. */ + + /* Widgets */ + GtkWidget *mainlayout; /* main layout */ + GtkWidget *mainscroll; /* scroller for main layout */ + GtkWidget *titlelayout; /* title layout */ + GtkWidget *titlecells[NCOLUMNS + 1]; /* title cells */ + GtkWidget **cells; /* all the cells */ + GtkWidget *menu; /* popup menu */ + struct menuitem *menuitems; /* menu items */ + GtkWidget *dragmark; /* drag destination marker */ + GtkWidget **dropzones; /* drag targets */ + + /* State */ + struct queue_entry *q; /* head of queue */ + struct queue_entry *last_click; /* last click */ + int nrows; /* number of rows */ + int mainrowheight; /* height of one row */ + hash *selection; /* currently selected items */ + int swallow_release; /* swallow button release from drag */ +}; + +static struct queuelike ql_queue, ql_recent; /* queue and recently played */ +static struct queue_entry *actual_queue; /* actual queue */ +static struct queue_entry *playing_track; /* currenty playing */ +static time_t last_playing = (time_t)-1; /* when last got playing */ +static int namepart_lookups_outstanding; +static int namepart_completions_deferred; /* # of completions not processed */ +static const struct cache_type cachetype_string = { 3600 }; +static const struct cache_type cachetype_integer = { 3600 }; +static GtkWidget *playing_length_label; + +/* Debugging --------------------------------------------------------------- */ + +#if 0 +static void describe_widget(const char *name, GtkWidget *w, int indent) { + int ww, wh, wx, wy; + + if(name) + fprintf(stderr, "%*s[%s]: '%s'\n", indent, "", + name, gtk_widget_get_name(w)); + gdk_window_get_position(w->window, &wx, &wy); + gdk_drawable_get_size(GDK_DRAWABLE(w->window), &ww, &wh); + fprintf(stderr, "%*s window %p: %dx%d at %dx%d\n", + indent, "", w->window, ww, wh, wx, wy); +} + +static void dump_layout(const struct queuelike *ql) { + GtkWidget *w; + char s[20]; + int row, col; + const struct queue_entry *q; + + describe_widget("mainscroll", ql->mainscroll, 0); + describe_widget("mainlayout", ql->mainlayout, 1); + for(q = ql->q, row = 0; q; q = q->next, ++row) + for(col = 0; col < NCOLUMNS + 1; ++col) + if((w = ql->cells[row * (NCOLUMNS + 1) + col])) { + sprintf(s, "%dx%d", row, col); + describe_widget(s, w, 2); + if(GTK_BIN(w)->child) + describe_widget(0, w, 3); + } +} +#endif + +/* Track detail lookup ----------------------------------------------------- */ + +/* A namepart lookup has completed or failed. */ +static void namepart_completed_or_failed(void) { + D(("namepart_completed_or_failed")); + --namepart_lookups_outstanding; + if(!namepart_lookups_outstanding || namepart_completions_deferred > 24) { + redisplay_queue(&ql_queue); + redisplay_queue(&ql_recent); + namepart_completions_deferred = 0; + } +} + +/* A namepart lookup has completed. */ +static void namepart_completed(void *v, const char *value) { + struct callbackdata *cbd = v; + + D(("namepart_completed")); + cache_put(&cachetype_string, cbd->u.key, value); + ++namepart_completions_deferred; + namepart_completed_or_failed(); +} + +/* A length lookup has completed. */ +static void length_completed(void *v, long l) { + struct callbackdata *cbd = v; + long *value; + + D(("namepart_completed")); + value = xmalloc(sizeof *value); + *value = l; + cache_put(&cachetype_integer, cbd->u.key, value); + ++namepart_completions_deferred; + namepart_completed_or_failed(); +} + +/* A length or namepart lookup has failed. */ +static void namepart_protocol_error( + struct callbackdata attribute((unused)) *cbd, + int attribute((unused)) code, + const char *msg) { + D(("namepart_protocol_error")); + gtk_label_set_text(GTK_LABEL(report_label), msg); + namepart_completed_or_failed(); +} + +/* Arrange to fill in a namepart cache entry */ +static void namepart_fill(const char *track, + const char *context, + const char *part, + const char *key) { + struct callbackdata *cbd; + + ++namepart_lookups_outstanding; + cbd = xmalloc(sizeof *cbd); + cbd->onerror = namepart_protocol_error; + cbd->u.key = key; + disorder_eclient_namepart(client, namepart_completed, + track, context, part, cbd); +} + +/* Look up a namepart. If it is in the cache then just return its value. If + * not then look it up and arrange for the queues to be updated when its value + * is available. */ +static const char *namepart(const char *track, + const char *context, + const char *part) { + char *key; + const char *value; + + D(("namepart %s %s %s", track, context, part)); + byte_xasprintf(&key, "namepart context=%s part=%s track=%s", + context, part, track); + value = cache_get(&cachetype_string, key); + if(!value) { + D(("deferring...")); + /* stick a value in the cache so we don't issue another lookup if we + * revisit */ + cache_put(&cachetype_string, key, value = "?"); + namepart_fill(track, context, part, key); + } + return value; +} + +/* Called from properties.c when we know a name part has changed */ +void namepart_update(const char *track, + const char *context, + const char *part) { + char *key; + + byte_xasprintf(&key, "namepart context=%s part=%s track=%s", + context, part, track); + /* Only refetch if it's actually in the cache */ + if(cache_get(&cachetype_string, key)) + namepart_fill(track, context, part, key); +} + +/* Look up a track length. If it is in the cache then just return its value. + * If not then look it up and arrange for the queues to be updated when its + * value is available. */ +static long getlength(const char *track) { + char *key; + const long *value; + struct callbackdata *cbd; + static const long bogus = -1; + + D(("getlength %s", track)); + byte_xasprintf(&key, "length track=%s", track); + value = cache_get(&cachetype_integer, key); + if(!value) { + D(("deferring..."));; + cache_put(&cachetype_integer, key, value = &bogus); + ++namepart_lookups_outstanding; + cbd = xmalloc(sizeof *cbd); + cbd->onerror = namepart_protocol_error; + cbd->u.key = key; + disorder_eclient_length(client, length_completed, track, cbd); + } + return *value; +} + +/* Column constructors ----------------------------------------------------- */ + +/* Format the 'when' column */ +static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + char when[64]; + struct tm tm; + time_t t; + + D(("column_when")); + switch(q->state) { + case playing_isscratch: + case playing_unplayed: + case playing_random: + t = q->expected; + break; + case playing_failed: + case playing_no_player: + case playing_ok: + case playing_scratched: + case playing_started: + case playing_paused: + case playing_quitting: + t = q->played; + break; + default: + t = 0; + break; + } + if(t) + strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm)); + else + when[0] = 0; + return gtk_label_new(when); +} + +/* Format the 'who' column */ +static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_who")); + return gtk_label_new(q->submitter ? q->submitter : ""); +} + +/* Format one of the track name columns */ +static GtkWidget *column_namepart(const struct queuelike + attribute((unused)) *ql, + const struct queue_entry *q, + const char *data) { + D(("column_namepart")); + return gtk_label_new(namepart(q->track, "display", data)); +} + +/* Compute the length field */ +static const char *text_length(const struct queue_entry *q) { + long l; + time_t now; + char *played = 0, *length = 0; + + /* Work out what to say for the length */ + l = getlength(q->track); + if(l > 0) + byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60); + else + byte_xasprintf(&length, "?:??"); + /* For the currently playing track we want to report how much of the track + * has been played */ + if(q == playing_track) { + /* log_state() arranges that we re-get the playing data whenever the + * pause/resume state changes */ + if(last_state & DISORDER_TRACK_PAUSED) + l = playing_track->sofar; + else { + time(&now); + l = playing_track->sofar + (now - last_playing); + } + byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length); + return played; + } else + return length; +} + +/* Format the length column */ +static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_length")); + if(q == playing_track) { + assert(!playing_length_label); + playing_length_label = gtk_label_new(text_length(q)); + /* Zot playing_length_label when it is destroyed */ + g_signal_connect(playing_length_label, "destroy", + G_CALLBACK(gtk_widget_destroyed), &playing_length_label); + return playing_length_label; + } else + return gtk_label_new(text_length(q)); +} + +/* Apply a new queue contents, transferring the selection from the old value */ +static void update_queue(struct queuelike *ql, struct queue_entry *newq) { + struct queue_entry *q; + + D(("update_queue")); + /* Propagate last_click across the change */ + if(ql->last_click) { + for(q = newq; q; q = q->next) { + if(!strcmp(q->id, ql->last_click->id)) + break; + ql->last_click = q; + } + } + /* Tell every queue entry which queue owns it */ + for(q = newq; q; q = q->next) + q->ql = ql; + /* Switch to the new queue */ + ql->q = newq; + /* Clean up any selected items that have fallen off */ + for(q = ql->q; q; q = q->next) + selection_live(ql->selection, q->id); + selection_cleanup(ql->selection); +} + +/* Wrap up a widget for putting into the queue or title */ +static GtkWidget *wrap_queue_cell(GtkWidget *label, + const char *name, + int *wp) { + GtkRequisition req; + GtkWidget *bg; + + D(("wrap_queue_cell")); + /* Padding should be in the label so there are no gaps in the + * background */ + gtk_misc_set_padding(GTK_MISC(label), HCELLPADDING, VCELLPADDING); + /* Event box is just to hold a background color */ + bg = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(bg), label); + if(wp) { + /* Update maximum width */ + gtk_widget_size_request(label, &req); + if(req.width > *wp) *wp = req.width; + } + /* Set widget names */ + gtk_widget_set_name(bg, name); + gtk_widget_set_name(label, name); + return bg; +} + +/* Create the wrapped widget for a cell in the queue display */ +static GtkWidget *get_queue_cell(struct queuelike *ql, + const struct queue_entry *q, + int row, + int col, + const char *name, + int *wp) { + GtkWidget *label; + D(("get_queue_cell %d %d", row, col)); + label = columns[col].widget(ql, q, columns[col].data); + gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); + return wrap_queue_cell(label, name, wp); +} + +/* Add a padding cell to the end of a row */ +static GtkWidget *get_padding_cell(const char *name) { + D(("get_padding_cell")); + return wrap_queue_cell(gtk_label_new(""), name, 0); +} + +/* User button press and menu ---------------------------------------------- */ + +/* Update widget states in order to reflect the selection status */ +static void set_widget_states(struct queuelike *ql) { + struct queue_entry *q; + int row, col; + + for(q = ql->q, row = 0; q; q = q->next, ++row) { + for(col = 0; col < NCOLUMNS + 1; ++col) + gtk_widget_set_state(ql->cells[row * (NCOLUMNS + 1) + col], + selection_selected(ql->selection, q->id) ? + GTK_STATE_SELECTED : GTK_STATE_NORMAL); + } + /* Might need to change sensitivity of 'Properties' in main menu */ + menu_update(-1); +} + +static int queue_before(const struct queue_entry *a, + const struct queue_entry *b) { + while(a && a != b) + a = a->next; + return !!a; +} + +/* A button was pressed and released */ +static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data) { + struct queue_entry *q = user_data, *qq; + struct queuelike *ql = q->ql; + struct menuiteminfo *mii; + int n; + + /* Might be a release left over from a drag */ + if(ql->swallow_release) { + ql->swallow_release = 0; + return FALSE; /* propagate */ + } + + if(event->type == GDK_BUTTON_PRESS + && event->button == 3) { + /* Right button click. + * If the current item is not selected then switch the selection to just + * this item */ + if(q && !selection_selected(ql->selection, q->id)) { + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + ql->last_click = q; + set_widget_states(ql); + } + /* Set the sensitivity of each menu item and (re-)establish the signal + * handlers */ + for(n = 0; ql->menuitems[n].name; ++n) { + if(ql->menuitems[n].handlerid) + g_signal_handler_disconnect(ql->menuitems[n].w, + ql->menuitems[n].handlerid); + gtk_widget_set_sensitive(ql->menuitems[n].w, + ql->menuitems[n].sensitive(ql, + &ql->menuitems[n], + q)); + mii = xmalloc(sizeof *mii); + mii->ql = ql; + mii->q = q; + ql->menuitems[n].handlerid = g_signal_connect + (ql->menuitems[n].w, "activate", + G_CALLBACK(ql->menuitems[n].activate), mii); + } + /* Update the menu according to context */ + gtk_widget_show_all(ql->menu); + gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0, + event->button, event->time); + return TRUE; /* hide the click from other widgets */ + } + if(event->type == GDK_BUTTON_RELEASE + && event->button == 1) { + /* no modifiers: select this, unselect everything else, set last click + * +ctrl: flip selection of this, set last click + * +shift: select from last click to here, don't set last click + * +ctrl+shift: select from last click to here, set last click + */ + switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + case 0: + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + ql->last_click = q; + break; + case GDK_CONTROL_MASK: + selection_flip(ql->selection, q->id); + ql->last_click = q; + break; + case GDK_SHIFT_MASK: + case GDK_SHIFT_MASK|GDK_CONTROL_MASK: + if(ql->last_click) { + if(!(event->state & GDK_CONTROL_MASK)) + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + qq = q; + if(queue_before(ql->last_click, q)) + while(qq != ql->last_click) { + qq = qq->prev; + selection_set(ql->selection, qq->id, 1); + } + else + while(qq != ql->last_click) { + qq = qq->next; + selection_set(ql->selection, qq->id, 1); + } + if(event->state & GDK_CONTROL_MASK) + ql->last_click = q; + } + break; + } + set_widget_states(ql); + gtk_widget_queue_draw(ql->mainlayout); + } + return FALSE; /* propagate */ +} + +/* A button was pressed or released on the mainlayout. For debugging only at + * the moment. */ +static gboolean mainlayout_button(GtkWidget attribute((unused)) *widget, + GdkEventButton attribute((unused)) *event, + gpointer attribute((unused)) user_data) { + return FALSE; /* propagate */ +} + +void queue_select_all(struct queuelike *ql) { + struct queue_entry *qq; + + for(qq = ql->q; qq; qq = qq->next) + selection_set(ql->selection, qq->id, 1); + ql->last_click = 0; + set_widget_states(ql); +} + +void queue_properties(struct queuelike *ql) { + struct vector v; + const struct queue_entry *qq; + + vector_init(&v); + for(qq = ql->q; qq; qq = qq->next) + if(selection_selected(ql->selection, qq->id)) + vector_append(&v, (char *)qq->track); + if(v.nvec) + properties(v.nvec, v.vec); +} + +/* Drag and drop rearrangement --------------------------------------------- */ + +static int draggable_row(const struct queue_entry *q) { + return q->ql == &ql_queue && q != playing_track; +} + +/* Called when a drag begings */ +static void queue_drag_begin(GtkWidget attribute((unused)) *widget, + GdkDragContext attribute((unused)) *dc, + gpointer data) { + struct queue_entry *q = data; + struct queuelike *ql = q->ql; + + /* Make sure the playing track is not selected, since it cannot be dragged */ + if(playing_track) + selection_set(ql->selection, playing_track->id, 0); + /* If the dragged item is not in the selection then change the selection to + * just that */ + if(!selection_selected(ql->selection, q->id)) { + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + set_widget_states(ql); + } + /* Ignore the eventual button release */ + ql->swallow_release = 1; + /* Create dropzones */ + add_drag_targets(ql); +} + +/* Convert an ID back into a queue entry and a screen row number */ +static struct queue_entry *findentry(struct queuelike *ql, + const char *id, + int *rowp) { + int row; + struct queue_entry *q; + + if(id) { + for(q = ql->q, row = 0; q && strcmp(q->id, id); q = q->next, ++row) + ; + } else { + q = 0; + row = playing_track ? 0 : -1; + } + if(rowp) *rowp = row; + return q; +} + +/* Called when data is dropped */ +static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget, + GdkDragContext *drag_context, + gint attribute((unused)) x, + gint attribute((unused)) y, + guint when, + gpointer user_data) { + struct queuelike *ql = &ql_queue; + const char *id = user_data; + struct vector vec; + struct queue_entry *q; + + if(!id || (playing_track && !strcmp(id, playing_track->id))) + id = ""; + vector_init(&vec); + for(q = ql->q; q; q = q->next) + if(q != playing_track && selection_selected(ql->selection, q->id)) + vector_append(&vec, (char *)q->id); + disorder_eclient_moveafter(client, id, vec.nvec, (const char **)vec.vec, + 0/*completed*/, 0/*v*/); + gtk_drag_finish(drag_context, TRUE, TRUE, when); + /* Destroy dropzones */ + remove_drag_targets(ql); + return TRUE; +} + +/* Called when we enter, or move within, a drop zone */ +static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget, + GdkDragContext *drag_context, + gint attribute((unused)) x, + gint attribute((unused)) y, + guint when, + gpointer user_data) { + struct queuelike *ql = &ql_queue; + const char *id = user_data; + int row; + struct queue_entry *q = findentry(ql, id, &row); + + if(!id || q) { + if(!ql->dragmark) { + ql->dragmark = gtk_event_box_new(); + g_signal_connect(ql->dragmark, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->dragmark); + gtk_widget_set_size_request(ql->dragmark, 10240, row ? 4 : 2); + gtk_widget_set_name(ql->dragmark, "queue-drag"); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, + (row + 1) * ql->mainrowheight - !!row); + } else + gtk_layout_move(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, + (row + 1) * ql->mainrowheight - !!row); + gtk_widget_show(ql->dragmark); + gdk_drag_status(drag_context, GDK_ACTION_MOVE, when); + return TRUE; + } else + /* ID has gone AWOL */ + return FALSE; +} + +/* Called when we leave a drop zone */ +static void queue_drag_leave(GtkWidget attribute((unused)) *widget, + GdkDragContext attribute((unused)) *drag_context, + guint attribute((unused)) when, + gpointer attribute((unused)) user_data) { + struct queuelike *ql = &ql_queue; + + if(ql->dragmark) + gtk_widget_hide(ql->dragmark); +} + +/* Add a drag target at position Y. ID is the track to insert the moved tracks + * after, and might be 0 to insert before the start. */ +static void add_drag_target(struct queuelike *ql, int y, int row, + const char *id) { + GtkWidget *eventbox; + + assert(ql->dropzones[row] == 0); + eventbox = gtk_event_box_new(); + /* Make the target zone invisible */ + gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox), FALSE); + /* Make it large enough */ + gtk_widget_set_size_request(eventbox, 10240, + y ? ql->mainrowheight : ql->mainrowheight / 2); + /* Position it */ + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), eventbox, 0, + y ? y - ql->mainrowheight / 2 : 0); + /* Mark it as capable of receiving drops */ + gtk_drag_dest_set(eventbox, + 0, + dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); + g_signal_connect(eventbox, "drag-drop", + G_CALLBACK(queue_drag_drop), (char *)id); + /* Monitor drag motion */ + g_signal_connect(eventbox, "drag-motion", + G_CALLBACK(queue_drag_motion), (char *)id); + g_signal_connect(eventbox, "drag-leave", + G_CALLBACK(queue_drag_leave), (char *)id); + /* The widget needs to be shown to receive drags */ + gtk_widget_show(eventbox); + /* Remember the drag targets */ + ql->dropzones[row] = eventbox; + g_signal_connect(eventbox, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->dropzones[row]); +} + +/* Create dropzones for dragging into */ +static void add_drag_targets(struct queuelike *ql) { + int row, y; + struct queue_entry *q; + + /* Create an array to store the widgets */ + ql->dropzones = xcalloc(ql->nrows, sizeof (GtkWidget *)); + y = 0; + /* Add a drag target before the first row provided it's not the playing + * track */ + if(!playing_track || ql->q != playing_track) + add_drag_target(ql, 0, 0, 0); + /* Put a drag target at the bottom of every row */ + for(q = ql->q, row = 0; q; q = q->next, ++row) { + y += ql->mainrowheight; + add_drag_target(ql, y, row, q->id); + } +} + +/* Remove the dropzones */ +static void remove_drag_targets(struct queuelike *ql) { + int row; + + for(row = 0; row < ql->nrows; ++row) { + if(ql->dropzones[row]) { + gtk_widget_destroy(ql->dropzones[row]); + } + assert(ql->dropzones[row] == 0); + } +} + +/* Layout ------------------------------------------------------------------ */ + +/* Redisplay the queue. */ +static void redisplay_queue(struct queuelike *ql) { + struct queue_entry *q; + int row, col; + GList *c; + const char *name; + GtkRequisition req; + GtkWidget *w; + int maxwidths[NCOLUMNS], x, y, titlerowheight; + int totalwidth = 10240; /* TODO: can we be less blunt */ + + D(("redisplay_queue")); + /* Eliminate all the existing widgets and start from scratch */ + for(c = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout)); + c; + c = c->next) { + /* Destroy both the label and the eventbox */ + if(GTK_BIN(c->data)->child) + gtk_widget_destroy(GTK_BIN(c->data)->child); + gtk_widget_destroy(GTK_WIDGET(c->data)); + } + /* Adjust the row count */ + for(q = ql->q, ql->nrows = 0; q; q = q->next) + ++ql->nrows; + /* We need to create all the widgets before we can position them */ + ql->cells = xcalloc(ql->nrows * (NCOLUMNS + 1), sizeof *ql->cells); + /* Minimum width is given by the column headings */ + for(col = 0; col < NCOLUMNS; ++col) { + /* Reset size so we don't inherit last iteration's maximum size */ + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, -1, -1); + gtk_widget_size_request(GTK_BIN(ql->titlecells[col])->child, &req); + maxwidths[col] = req.width; + } + /* Find the vertical size of the title bar */ + gtk_widget_size_request(GTK_BIN(ql->titlecells[0])->child, &req); + titlerowheight = req.height; + y = 0; + if(ql->nrows) { + /* Construct the widgets */ + for(q = ql->q, row = 0; q; q = q->next, ++row) { + /* Figure out the widget name for this row */ + if(q == playing_track) name = "row-playing"; + else name = row % 2 ? "row-even" : "row-odd"; + /* Make the widget for each column */ + for(col = 0; col <= NCOLUMNS; ++col) { + /* Create and store the widget */ + if(col < NCOLUMNS) + w = get_queue_cell(ql, q, row, col, name, &maxwidths[col]); + else + w = get_padding_cell(name); + ql->cells[row * (NCOLUMNS + 1) + col] = w; + /* Maybe mark it draggable */ + if(draggable_row(q)) { + gtk_drag_source_set(w, GDK_BUTTON1_MASK, + dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); + g_signal_connect(w, "drag-begin", G_CALLBACK(queue_drag_begin), q); + } + /* Catch button presses */ + g_signal_connect(w, "button-release-event", + G_CALLBACK(queuelike_button_released), q); + g_signal_connect(w, "button-press-event", + G_CALLBACK(queuelike_button_released), q); + } + } + /* ...and of each row in the main layout */ + gtk_widget_size_request(GTK_BIN(ql->cells[0])->child, &req); + ql->mainrowheight = req.height; + /* Now we know the maximum width of each column we can set the size of + * everything and position it */ + for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) { + x = 0; + for(col = 0; col < NCOLUMNS; ++col) { + w = ql->cells[row * (NCOLUMNS + 1) + col]; + gtk_widget_set_size_request(GTK_BIN(w)->child, + maxwidths[col], -1); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); + x += maxwidths[col]; + } + w = ql->cells[row * (NCOLUMNS + 1) + col]; + gtk_widget_set_size_request(GTK_BIN(w)->child, + totalwidth - x, -1); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); + y += ql->mainrowheight; + } + } + /* Titles */ + x = 0; + for(col = 0; col < NCOLUMNS; ++col) { + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, + maxwidths[col], -1); + gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); + x += maxwidths[col]; + } + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, + totalwidth - x, -1); + gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); + /* Set the states */ + set_widget_states(ql); + /* Make sure it's all visible */ + gtk_widget_show_all(ql->mainlayout); + gtk_widget_show_all(ql->titlelayout); + /* Layouts might shrink to arrange for the area they shrink out of to be + * redrawn */ + gtk_widget_queue_draw(ql->mainlayout); + gtk_widget_queue_draw(ql->titlelayout); + /* Adjust the size of the layout */ + gtk_layout_set_size(GTK_LAYOUT(ql->mainlayout), x, y); + gtk_layout_set_size(GTK_LAYOUT(ql->titlelayout), x, titlerowheight); + gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight); +} + +/* Called with new queue/recent contents */ +static void queuelike_completed(void *v, struct queue_entry *q) { + struct callbackdata *cbd = v; + struct queuelike *ql = cbd->u.ql; + + D(("queuelike_complete")); + /* Install the new queue */ + update_queue(ql, ql->fixup(q)); + /* Update the display */ + redisplay_queue(ql); + if(ql->notify) + ql->notify(); + /* Update sensitivity of main menu items */ + menu_update(-1); +} + +/* Called with a new currently playing track */ +static void playing_completed(void attribute((unused)) *v, + struct queue_entry *q) { + struct callbackdata cbd; + D(("playing_completed")); + playing_track = q; + /* Record when we got the playing track data so we know how old the 'sofar' + * field is */ + time(&last_playing); + cbd.u.ql = &ql_queue; + queuelike_completed(&cbd, actual_queue); +} + +static void queue_scrolled(GtkAdjustment *adjustment, + gpointer user_data) { + GtkAdjustment *titleadj = user_data; + + D(("queue_scrolled")); + gtk_adjustment_set_value(titleadj, adjustment->value); +} + +/* Create a queuelike thing (queue/recent) */ +static GtkWidget *queuelike(struct queuelike *ql, + struct queue_entry *(*fixup)(struct queue_entry *), + void (*notify)(void), + struct menuitem *menuitems, + const char *name) { + GtkWidget *vbox, *mainscroll, *titlescroll, *label; + GtkAdjustment *mainadj, *titleadj; + int col, n; + + D(("queuelike")); + ql->fixup = fixup; + ql->notify = notify; + ql->menuitems = menuitems; + ql->name = name; + ql->mainrowheight = !0; /* else division by 0 */ + ql->selection = selection_new(); + /* Create the layouts */ + ql->mainlayout = gtk_layout_new(0, 0); + ql->titlelayout = gtk_layout_new(0, 0); + /* Scroll the layouts */ + ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout, name); + titlescroll = scroll_widget(ql->titlelayout, name); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(titlescroll), + GTK_POLICY_NEVER, GTK_POLICY_NEVER); + mainadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(mainscroll)); + titleadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(titlescroll)); + g_signal_connect(mainadj, "changed", G_CALLBACK(queue_scrolled), titleadj); + g_signal_connect(mainadj, "value-changed", G_CALLBACK(queue_scrolled), titleadj); + /* Fill the titles and put them anywhere */ + for(col = 0; col < NCOLUMNS; ++col) { + label = gtk_label_new(columns[col].name); + gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); + ql->titlecells[col] = wrap_queue_cell(label, "row-title", 0); + gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); + } + ql->titlecells[col] = get_padding_cell("row-title"); + gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); + /* Pack the lot together in a vbox */ + vbox = gtk_vbox_new(0, 0); + gtk_box_pack_start(GTK_BOX(vbox), titlescroll, 0, 0, 0); + gtk_box_pack_start(GTK_BOX(vbox), mainscroll, 1, 1, 0); + /* Create the popup menu */ + ql->menu = gtk_menu_new(); + g_signal_connect(ql->menu, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->menu); + for(n = 0; menuitems[n].name; ++n) { + menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name); + gtk_menu_attach(GTK_MENU(ql->menu), menuitems[n].w, 0, 1, n, n + 1); + } + g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_queue); + g_object_set_data(G_OBJECT(vbox), "queue", ql); + /* Catch button presses */ + g_signal_connect(ql->mainlayout, "button-release-event", + G_CALLBACK(mainlayout_button), ql); +#if 0 + g_signal_connect(ql->mainlayout, "button-press-event", + G_CALLBACK(mainlayout_button), ql); +#endif + return vbox; +} + +/* Popup menu items -------------------------------------------------------- */ + +/* Count the number of items selected */ +static int queue_count_selected(const struct queuelike *ql) { + return hash_count(ql->selection); +} + +/* Count the number of items selected */ +static int queue_count_entries(const struct queuelike *ql) { + int nitems = 0; + const struct queue_entry *q; + + for(q = ql->q; q; q = q->next) + ++nitems; + return nitems; +} + +/* Count the number of items selected, excluding the playing track if there is + * one */ +static int count_selected_nonplaying(const struct queuelike *ql) { + int nselected = queue_count_selected(ql); + + if(ql->q == playing_track && selection_selected(ql->selection, ql->q->id)) + --nselected; + return nselected; +} + +static int scratch_sensitive(struct queuelike attribute((unused)) *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* We can scratch if the playing track is selected */ + return playing_track && selection_selected(ql->selection, playing_track->id); +} + +static void scratch_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + if(playing_track) + disorder_eclient_scratch(client, playing_track->id, 0, 0); +} + +static int remove_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry *q) { + /* We can remove if we're hovering over a particular track or any non-playing + * tracks are selected */ + return (q && q != playing_track) || count_selected_nonplaying(ql); +} + +static void remove_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + struct queue_entry *q = mii->q; + struct queuelike *ql = mii->ql; + + if(count_selected_nonplaying(mii->ql)) { + /* Remove selected tracks */ + for(q = ql->q; q; q = q->next) + if(selection_selected(ql->selection, q->id) && q != playing_track) + disorder_eclient_remove(client, q->id, 0, 0); + } else if(q) + /* Remove just the hovered track */ + disorder_eclient_remove(client, q->id, 0, 0); +} + +static int properties_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* "Properties" is sensitive if at least something is selected */ + return hash_count(ql->selection) > 0; +} + +static void properties_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + + queue_properties(mii->ql); +} + +static int selectall_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* Sensitive if there is anything to select */ + return !!ql->q; +} + +static void selectall_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + queue_select_all(mii->ql); +} + +/* The queue --------------------------------------------------------------- */ + +/* Fix up the queue by sticking the currently playing track on the front */ +static struct queue_entry *fixup_queue(struct queue_entry *q) { + D(("fixup_queue")); + actual_queue = q; + if(playing_track) { + if(actual_queue) + actual_queue->prev = playing_track; + playing_track->next = actual_queue; + return playing_track; + } else + return actual_queue; +} + +/* Called regularly to adjust the so-far played label (redrawing the whole + * queue once a second makes disobedience occupy >10% of the CPU on my Athlon + * which is ureasonable expensive) */ +static gboolean adjust_sofar(gpointer attribute((unused)) data) { + if(playing_length_label && playing_track) + gtk_label_set_text(GTK_LABEL(playing_length_label), + text_length(playing_track)); + return TRUE; +} + +/* Popup menu for the queue. Put the properties first so that finger trouble + * is less dangerous. */ +static struct menuitem queue_menu[] = { + { "Properties", properties_activate, properties_sensitive, 0, 0 }, + { "Select all", selectall_activate, selectall_sensitive, 0, 0 }, + { "Scratch", scratch_activate, scratch_sensitive, 0, 0 }, + { "Remove", remove_activate, remove_sensitive, 0, 0 }, + { 0, 0, 0, 0, 0 } +}; + +GtkWidget *queue_widget(void) { + D(("queue_widget")); + /* Arrange periodic update of the so-far played field */ + g_timeout_add(1000/*ms*/, adjust_sofar, 0); + /* We pass choose_update() as our notify function since the choose screen + * marks tracks that are playing/in the queue. */ + return queuelike(&ql_queue, fixup_queue, choose_update, queue_menu, + "queue"); +} + +void queue_update(void) { + struct callbackdata *cbd; + + D(("queue_update")); + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + cbd->u.ql = &ql_queue; + gtk_label_set_text(GTK_LABEL(report_label), "updating queue"); + disorder_eclient_queue(client, queuelike_completed, cbd); +} + +void playing_update(void) { + D(("playing_update")); + gtk_label_set_text(GTK_LABEL(report_label), "updating playing track"); + disorder_eclient_playing(client, playing_completed, 0); +} + +/* Recently played tracks -------------------------------------------------- */ + +static struct queue_entry *fixup_recent(struct queue_entry *q) { + /* 'recent' is in the wrong order. TODO: globally fix this! */ + struct queue_entry *qr = 0, *qn; + + D(("fixup_recent")); + while(q) { + qn = q->next; + /* Swap next/prev pointers */ + q->next = q->prev; + q->prev = qn; + /* Remember last node for new head */ + qr = q; + /* Next node */ + q = qn; + } + return qr; +} + +static struct menuitem recent_menu[] = { + { "Properties", properties_activate, properties_sensitive,0, 0 }, + { "Select all", selectall_activate, selectall_sensitive, 0, 0 }, + { 0, 0, 0, 0, 0 } +}; + +GtkWidget *recent_widget(void) { + D(("recent_widget")); + return queuelike(&ql_recent, fixup_recent, 0, recent_menu, "recent"); +} + +void recent_update(void) { + struct callbackdata *cbd; + + D(("recent_update")); + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + cbd->u.ql = &ql_recent; + gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list"); + disorder_eclient_recent(client, queuelike_completed, cbd); +} + +/* Main menu plumbing ------------------------------------------------------ */ + +static int queue_properties_sensitive(GtkWidget *w) { + return !!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue")); +} + +static int queue_selectall_sensitive(GtkWidget *w) { + return !!queue_count_entries(g_object_get_data(G_OBJECT(w), "queue")); +} + +static void queue_properties_activate(GtkWidget *w) { + queue_properties(g_object_get_data(G_OBJECT(w), "queue")); +} + +static void queue_selectall_activate(GtkWidget *w) { + queue_select_all(g_object_get_data(G_OBJECT(w), "queue")); +} + +static const struct tabtype tabtype_queue = { + queue_properties_sensitive, + queue_selectall_sensitive, + queue_properties_activate, + queue_selectall_activate, +}; + +/* Other entry points ------------------------------------------------------ */ + +int queued(const char *track) { + struct queue_entry *q; + + D(("queued %s", track)); + for(q = ql_queue.q; q; q = q->next) + if(!strcmp(q->track, track)) + return 1; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:kxVCqYNfvMJkYlkf2Pn8pg */ diff --git a/doc/Makefile.am b/doc/Makefile.am new file mode 100644 index 0000000..743ace3 --- /dev/null +++ b/doc/Makefile.am @@ -0,0 +1,47 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +SEDFILES=disorder.1 disorderd.8 disorder_config.5 \ + disorder-dump.8 disorder_protocol.5 disorder-deadlock.8 \ + disorder-rescan.8 disobedience.1 disorderfm.1 + +include ${top_srcdir}/scripts/sedfiles.make + +man_MANS=disorderd.8 disorder.1 disorder.3 disorder_config.5 disorder-dump.8 \ + disorder_protocol.5 tkdisorder.1 disorder-deadlock.8 \ + disorder-rescan.8 disobedience.1 disorderfm.1 + +HTMLMAN=$(foreach man,$(man_MANS),$(man).html) + +$(HTMLMAN) : %.html : % $(top_srcdir)/scripts/htmlman + rm -f $@.new + $(top_srcdir)/scripts/htmlman $< >$@.new + chmod 444 $@.new + mv -f $@.new $@ + +pkgdata_DATA=$(HTMLMAN) + +EXTRA_DIST=disorderd.8.in disorder.1.in disorder_config.5.in \ + disorder.3 disorder-dump.8.in disorder_protocol.5.in \ + tkdisorder.1 disorder-deadlock.8.in disorder-rescan.8.in \ + disobedience.1.in disorderfm.1.in + +CLEANFILES=$(SEDFILES) $(HTMLMAN) +# arch-tag:9e90ca91629f0571b87f5a5f11227b7f diff --git a/doc/checklist.txt b/doc/checklist.txt new file mode 100644 index 0000000..f62e7be --- /dev/null +++ b/doc/checklist.txt @@ -0,0 +1,80 @@ +* Server + +After an hour or so of play use lsof to check that only a reasonable +number of FDs are used by the server; the speaker; the deadlock +checker. + +* Playing + +Check that artist and album work. + +Scratch button should work. + +Queue some tracks, check they can be removed. + +Album link should show track as playing. + +Amount of track played should be correct (also 'disorder playing'). + +* Recent + +Most recent should be at the top. + +Check that artist, album and prefs links work. + +* Choose + +Queue some tracks. They should be marked as queued. + +Pick an album. Try 'play all'. Check order. Remove all. + +Navigate around. Go into albums, back out with the navigation links. + +Go up outside the collection. Should work, produce directories you +can go back into. + +* Search + +Try a large search, e.g. 'love'. + +Look for 'Various'. It should be in the right order and say +'Various', i.e. an alias artist name should not have leaked into it. + +* Manage + +Check pause and play controls. Pause should _not crash_ for tracks +that cannot be paused. + +Set the volume up and down. + +Set the volume to exact values (different for L and R), check that the +proper speaker is affected. + +Add some tracks, rearrange them, remove them again. + +* Help + +Are all the man page links there? + +Are recent UI changes documented? + +* About + +Does the search league look plausible? + +Are there are any good candidates for additional stopwords? + +Is the copyright date right? Also check credits.html. + +* Preferences + +Modify prefs for a track from 'recent'. + +Modify prefs for a single track from 'choose'. + +Modify prefs for a whole album from 'choose'. + +Local Variables: +mode:outline +End: +# arch-tag:wuvzKmIxK6XShozQ9Bsuww diff --git a/doc/disobedience.1.in b/doc/disobedience.1.in new file mode 100644 index 0000000..75fc867 --- /dev/null +++ b/doc/disobedience.1.in @@ -0,0 +1,223 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disobedience 1 +.SH NAME +disobedience \- GUI client for DisOrder jukebox +.SH SYNOPSIS +.B disobedience +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disobedience +is a graphical client for DisOrder. It is a work in progress and many features +are not implemented yet. However everything in this man page is either +implemented or marked as missing. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SS "GTK+ Options" +Additional options are supported by the GTK+ library. Refer to GTK+ +documentation for further information. Under X11 they include: +.TP +.B --display \fIDISPLAY\fR +The X display to use. +.TP +.B --screen \fISCREEN\fR +The screen number to use. +.\" If know enough to use it you know enough to find it +.\" .TP +.\" .B --sync +.\" Make all X requests synchronously. +.SH "WINDOWS AND ICONS" +.SS "File Menu" +This only has one option, "Quit", which terminates the program. +.SS "Edit Menu" +This has the following options: +.TP +.B "Select All" +Select all tracks in whichever of the Queue or Recent tabs are showing. +.TP +.B Properties +Edit the details of the selected tracks. +.SS "Help Menu" +This has only one option, "About DisOrder", which pops up a box giving the +name, author and version number of the software. +.SS "Controls" +.TP +.B "Pause button" +The pause button can be used to pause and resume tracks. +.TP +.B "Scratch button" +The scratch button, a red cross, can be used to interrupt the currently playing +track. +.TP +.B "Random play button" +The random play button can be used to enable and disable random play. It does +not take effect until the currently playing track finishes. +.TP +.B "Play button" +The play button controls whether tracks will be played at all. As above it +does not take effect until the currently playing track finishes. +.TP +.B "Volume slider" +The volume slider indicates the current volume level and can be used to adjust +it. 0 is silent and 10 is maximum volume. +.TP +.B "Balance slider" +The balance slider indicates the current balance and can be used to adjust it. +-1 means only the left speaker, 0 means both speakers at equal volume and +1 +means the only the right speaker. +.SS "Queue Tab" +This displays the currently playing track and the queue. The currently playing +track is at the top and has a green background. Queued tracks appear below it +and have alternating red and white backgrounds. +.PP +The left button can be use to select and deselect tracks. On its own it just +selects the pointed track and deselects everything else. With CTRL it flips +the state of the pointed track without affecting anything else. With SHIFT it +selects every track from the last click to the current position and deselects +everything else. With both CTRL and SHIFT it selects everything from the last +click to the current position without deselecting anything. +.PP +The right button pops up a menu. This has the following options: +.TP +.B Properties +Edit the details of the selected tracks. See +.B "Properties Window" +below. +.TP +.B "Select All" +Select all tracks. +.TP +.B Scratch +Interrupt the currently playing track. (Note that this appears even if you +right click over a queued track rather than the currently playing track.) +.TP +.B Remove +Remove the selected tracks from the queue. +.SS "Recent Tab" +This displays recently played tracks, the most recent at the top. +.PP +The left button functions as above. The right button pops up a menu with the +following options: +.TP +.B Properties +Edit the details of the selected tracks. See +.B "Properties Window" +below. +.TP +.B "Select All" +Select all tracks. +.SS "Choose Tab" +This displays all the tracks known to the server in a tree structure. +.PP +Directories are represented with an arrow to their left. This can be clicked +to reveal or hide the contents of the directory. The top level "directories" +break up tracks by their first letter. +.PP +Playable files are represented by their name. If they are playing or in the +queue then a notes icon appears next to them. +.PP +Left clicking on a file will select it. As with the queue tab you can use +SHIFT and CTRL to select multiple files. +.PP +The text box at the top is a search form. If you enter search terms here then +the display will be limited to tracks containing all those words. You can also +limit the results to tracks with particular tags, by including \fBtag:\fITAG\fR +for each tag. +.PP +To start a new search just edit the contents of the search box. The cancel +button to its right clears the current search. +.PP +Right clicking will pop up a menu with the following options: +.TP +.B Play +Play selected tracks. +.TP +.B Properties +Edit properties of selected tracks. +.PP +A middle click on a file will add it to the queue. +.SS "Properties Window" +This window contains details of one or more tracks and allows them to be +edited. +.PP +The Artist, Album and Title fields determine how the tracks appear in +the queue and recently played tabs. +.PP +The Tags field determine which tags apply to the track. Tags are separated by +commas and can contain any printing characters except comma. +.PP +The Random checkbox determines whether the track will be picked at random. +Random play is enabled for every track by default, but it can be turned off +here. +.PP +Press "OK" to confirm all changes and close the window, "Apply" to confirm +changes but keep the window open and "Cancel" to close the window and discard +all changes. +.SH "KEYBOARD SHORTCUTS" +.TP +.B CTRL+A +Select all tracks (queue/recent) +.TP +.B CTRL+Q +Quit. +.SH "GTK+ RESOURCES" +You can override these resources in order to customize the appearance of +Disobedience. TODO example that actually works. +.SS "Widget Names" +.TP +.B disobedience.*.choose +This is the panel containing the track choice tree. +.TP +.B disobedience.*.queue +This is the panel displaying the queue. +.TP +.B disobedience.*.choose +This is the panel listing recently played tracks. +.TP +.B disobedience.*.row-playing +This is the row listing the currently playing track. +.TP +.B disobedience.*.row-odd +This an odd-numbered row in the queue or recently played track list. +.TP +.B disobedience.*.row-even +This an even-numbered row in the queue or recently played track list. +.SH "SEE ALSO" +.BR disorder_config (5) +.PP +.B http://www.gtk.org/api/2.6/gtk/gtk-x11.html +.br +- Using GTK+ on the X Window System +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:PmkAKyNwF7gDXfzSQ9GnYg diff --git a/doc/disorder-deadlock.8.in b/doc/disorder-deadlock.8.in new file mode 100644 index 0000000..a876bb0 --- /dev/null +++ b/doc/disorder-deadlock.8.in @@ -0,0 +1,47 @@ +.\" +.\" Copyright (C) 2005 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-deadlock 8 +.SH NAME +disorder-deadlock \- DisOrder deadlock manager +.SH SYNOPSIS +.B disorder-deadlock +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disorder-deadlock +is DisOrder's deadlock manager. It is automatically started by the +server and does not need to be invoked manually. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:miB8m/aYAr2KdqJ/DLNqig diff --git a/doc/disorder-dump.8.in b/doc/disorder-dump.8.in new file mode 100644 index 0000000..601f0cc --- /dev/null +++ b/doc/disorder-dump.8.in @@ -0,0 +1,116 @@ +.\" +.\" Copyright (C) 2004, 2005 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-dump 8 +.SH NAME +disorder-dump \- DisOrder dump/undump tool +.SH SYNOPSIS +.B disorder-dump +.RI [ OPTIONS ] +.BR --dump | --undump +.I PATH +.br +.B disorder-dump +.RI [ OPTIONS ] +.BR --recompute-aliases +.SH DESCRIPTION +.B disorder-dump +is used to dump and restore preferences data. +.SH OPTIONS +.TP +.B --dump +Write preferences data to \fIPATH\fR. This can safely be used whether +or not the server is running. +.TP +.B --undump +Read preferences data from \fIPATH\fR, replacing (unrecoverably) the +current settings. This should normally only be done while the server +is not running. +.IP +If the server is running then it may hang while the undump completes. +.TP +.B --recover +Perform database recovery at startup. The server should not be +running if this option is used. +.TP +.B --recompute-aliases +Recompute aliases without dumping or undumping the databases. Under +normal circumstances this is never necessary. +.TP +.B --remove-pathless +Remove tracks with no associated path when undumping or when +recomputing aliases. In normal use such tracks are all aliases. +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR /etc/disorder/config . +.TP +.B --debug\fR +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH NOTES +This program might be used for a number of purposes: +.TP 2 +.B . +Taking a backup of the non-regeneratable parts of DisOrder's databases. +.TP +.B . +Indoctrinating one DisOrder server with the preference values of +another. +.TP +.B . +Upgrading DisOrder across data format changes in the underlying +database library. +.PP +The output file is versioned, so versions produced from a future +version of DisOrder may be rejected by \fB--undump\fR. It has an end +marker so truncated inputs will also be rejected. +.PP +The input or output file must be a regular file, as it may be rewound +and re-read or re-written multiple times. +.PP +The dump or undump operation is carried out inside a single +transaction, so it should seem atomic from the point of view of +anything else accessing the databases. +.PP +The server performs normal database recovery on startup. However if +the database needs normal recovery before an undump can succeed and +you don't want to start the server for some reason then the +.B --recover +operation is available for this purpose. No other process should be +accessing the database at the time. +.PP +DisOrder does not currently support catastrophic recovery. +.PP +This program requires write access to DisOrder's databases. Ideally +therefore it should be run as the same user as the server or as root. +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorderd\fR(8) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:acbd58712d46b2a4559b49a099126acf diff --git a/doc/disorder-rescan.8.in b/doc/disorder-rescan.8.in new file mode 100644 index 0000000..e66894e --- /dev/null +++ b/doc/disorder-rescan.8.in @@ -0,0 +1,48 @@ +.\" +.\" Copyright (C) 2005 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-rescan 8 +.SH NAME +disorder-rescan \- DisOrder rescanner +.SH SYNOPSIS +.B disorder-rescan +.RI [ OPTIONS ] +.RI [ PATH ...] +.SH DESCRIPTION +.B disorder-rescan +is DisOrder's rescan rescanner. It is invoked by DisOrder when +necessary and does not need to be invoked manually. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:w32jqhcFaBEGCakBFZI0GQ diff --git a/doc/disorder.1.in b/doc/disorder.1.in new file mode 100644 index 0000000..369f2ca --- /dev/null +++ b/doc/disorder.1.in @@ -0,0 +1,346 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder 1 +.SH NAME +disorder \- DisOrder jukebox client +.SH SYNOPSIS +.B disorder +.RI [ OPTIONS ] +.RB [ -- ] +.RI [ COMMANDS ...] +.br +.B disorder +.B --length +.RI [ OPTIONS ] +.RB [ -- ] +.IR PATH ... +.SH DESCRIPTION +Without the \fB--length\fR option, +.B disorder +is used to query the \fBdisorderd\fR(8) daemon from the command line. +It may be used to request tracks, scratch tracks, query the current +state, etc, and by an administrator to shutdown or reconfigure the +daemon. +.PP +If no commands are specified then \fBdisorder\fR connects to the +daemon and then immediately disconnects. This can be used to test +whether the daemon is running. Otherwise, it executes the commands +specified. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --length\fR, \fB-L +Calculate the length in seconds of the files specified using the tracklength +plugin. +.TP +.B --version\fR, \fB-V +Display version number. +.TP +.B --help-commands\fR, \fB-H +List all known commands. +.SH COMMANDS +.TP +.B dirs \fIDIRECTORY\fR [\fB~\fIREGEXP\fR] +List all the directories in \fIDIRECTORY\fR. +.IP +An optional regexp may be specified, marked with an initial \fB~\fR. Only +directories with a basename matching the regexp will be returned. +.TP +.B disable +Disables playing after the current track finishes. +.TP +.B enable +(Re-)enable playing. +.TP +.B files \fIDIRECTORY\fR [\fB~\fIREGEXP\fR] +List all the files in \fIDIRECTORY\fR. +.IP +An optional regexp may be specified, marked with an initial \fB~\fR. Only +files with a basename matching the regexp will be returned. +.TP +.B get \fITRACK\fR \fIKEY\fR +Display the preference \fIKEY\fR for \fITRACK\fR. +.TP +.B get-global \fIKEY\fR +Get a global preference. +.TP +.B get-volume +Displays the current volume settings. +.TP +.B length \fITRACK\fR +Reports the length of \fITRACK\fR in seconds. +.TP +.B log +Writes event log messages to standard output, until the server is terminated. +See \fBdisorder_protocol\fR (5) for details of the output syntax. +.TP +.B move \fITRACK\fR \fIDELTA\fR +Move +.I TRACK +by +.I DELTA +within the queue. Positive values move towards the head of the queue, negative +values towards the tail. +.IP +Note that if you specify a negative value then the +.B -- +option separate (before all commands) becomes mandatory, as otherwise the +negative value is misinterpreted an an option. +.TP +.B part \fITRACK\fR \fICONTEXT\fI \fIPART\fR +Get a track name part. +.IP +\fICONTEXT\fR should be either \fBsort\fR or \fBdisplay\fR. \fBpart\fR is the +part of the name desired, typically \fBartist\fR, \fBalbum\fR or \fBtitle\fR. +.TP +.B pause +Pause the current track. (Note that not all players support pausing.) +.TP +.B play \fITRACKS\fR... +Add \fITRACKS\fR to the end of the queue. +.TP +.B playing +Report the currently playing track. +.TP +.B prefs \fITRACK\fR +Display all the preferences for \fITRACK\fR. +.TP +.B queue +List the current queue. The first entry in the list is the next track to play. +.TP +.B random-disable +Disable random play. +.TP +.B random-enable +Enable random play. +.TP +.B recent +List recently played tracks. The first entry is the oldest track, the last +entry is the most recently played one. +.TP +.B remove \fITRACK\fR +Remove a track from the queue. +.TP +.B resolve \fITRACK\fR +Resolve aliases for \fITRACK\fR and print out the real track name. +.TP +.B resume +Resume the current track after a pause. +.TP +.B scratch +Scratch the currently playing track. +.TP +.B scratch-id \fIID\fR +Scratch the currently playing track, provided it has the given ID. +.TP +.B search \fITERMS\fR +Search for tracks containing all of the listed terms. The terms are +separated by spaces and form a single argument, so must be quoted, +for example: +.IP +.B "disorder search 'bowie china'" +.IP +You can limit the search to tracks with a particular tag, too, using the +\fBtag:\fR modifier. For example: +.IP +.B "disorder search 'love tag:depressing' +.TP +.B set \fITRACK\fR \fIKEY\fR \fIVALUE\fR +Set the preference \fIKEY\fR for \fITRACK\fR to \fIVALUE\fR. +.TP +.B set-global \fIKEY\fR \fIVALUE\fR +Set a global preference. +.TP +.B set-volume \fBLEFT\fR \fBRIGHT\fR +Sets the volume. +.TP +.B stats +List server statistics. +.TP +.B tags +List known tags. +.TP +.B unset \fITRACK\fR \fIKEY\fR +Unset the preference \fIKEY\fR for \fITRACK\fR. +.TP +.B unset-global \fIKEY\fR +Unset the global preference \fIKEY\fR. +.TP +.B version +Report the daemon's version number. +.PP +For +.B move +and +.BR remove , +tracks may be specified by name or by ID. If you use the name and a track +appears twice in the queue it is undefined which is affected. +.SS "Privileged Commands" +These commands are only available to privileged users. +.TP +.B become \fIUSER\fR +Become another user. +.TP +.B reconfigure +Make the daemon reload its configuration file. +.TP +.B rescan +Rescan the filesystem for new tracks. There is an automatic daily rescan but +if you've just added some tracks and want them to show up immediately, use this +command. +.TP +.B shutdown +Shut down the daemon. +.SH PREFERENCES +Currently the following preferences are supported. Some are expected +to be set by users, others updated automatically by plugins. +.TP +.B pick_at_random +If this preference is present and set to "0" then the track will not +be picked for random play. Otherwise it may be. +.TP +.B played +A decimal integer giving the number times the track was played. This +includes tracks that are scratched or were picked at random. +.TP +.B played_time +The last time the track was played, as a \fBtime_t\fR converted to a +decimal integer. +.TP +.B scratched +The number of times the track has been scratched. +.TP +.B requested +A decimal integer giving the number of times the track was requested. +(Tracks that are removed before being played are not counted.) +.TP +.B tags +Tags that apply to this track, separated by commas. Tags can contain any +printing character except comma. Leading and trailing spaces are not +significant but internal spaces are. +.IP +Using the +.B required-tags +and +.B prohibited-tags +global preferences, it is possible to limit the tracks that will be selected at +random. +.TP +.B trackname_\fICONTEXT\fB_\fIPART\fR +These preferences can be used to override the filename parsing rules +to find a track name part. For backwards compatibility, +\fBtrackname_\fIPART\fR will be used if the full version +is not present. +.TP +.B unscratched +The number of times the track has been played to completion without +being scratched. +.SH "Superuser Commands" +These commands will (generally) only work for root, who must be a privileged +user. +.TP +.B authorize \fIUSER\fR +Chooses a password for \fIUSER\fR and adds it to \fIconfig.private\fR. Also +creates an appropriate \fIconfig.USER\fR, be owned by the user. +.IP +If at least one \fBauthorize\fR command succeeds then the server is +automatically told to re-read its configuration. +.SH NOTES +.B disorder +is locale-aware. If you do not set the locale correctly then it may +not handle non-ASCII data properly. +.PP +The client determines which user to attempt to authenticate as by +examining the current UID. +.PP +This program is not intended to run in a setuid environment. +.PP +The regexp syntax used by the \fBfiles\fR and \fBdirs\fR commands use the +syntax described in \fBpcrepattern\fR(3). Matching is case-independent. It is +strongly recommended that you quote regexps, since they often contain +characters treated specially by the shell. For example: +.PP +.B "disorder dirs /Music ~'^(?!the [^t])t'" +.SH TROUBLESHOOTING +If you cannot play a track, or it does not appear in the database even after a +rescan, check the following things: +.TP +.B . +Are there any error messages in the system log? The server logs to +\fBLOG_DAEMON\fR, which typically ends up in +.I /var/log/daemon.log +or +.IR /var/log/messages , +though this depends on local configuration. +.TP +.B . +Is the track in a known format? Have a look at +.I pkgconfdir/config +for the formats recognized by the local installation. The filename matching is +case-sensitive. +.TP +.B . +Do permissions on the track allow the server to read it? +.TP +.B . +Do the permissions on the containing directories allow the server to read and +execute them? +.PP +The user the server runs as is determined by the \fBuser\fR directive in the +configuration file. The README recommends using \fBjukebox\fR for this purpose +but it could be different locally. +.SH ENVIRONMENT +.TP +.B LOGNAME +The default username. +.TP +.B HOME +The user's home directory. +.TP +.B LC_ALL\fR, \fBLANG\fR, etc +Current locale. See \fBlocale\fR(7). +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.TP +.I ~/.disorder/passwd +Per-user password file +.TP +.I pkgstatedir/socket +Communication socket for \fBdisorder\fR(1). +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5), \fBsyslog\fR(3), \fBtime\fR(2), +\fBpcrepattern\fR(3), \fBdisobedience\fR(1) +.PP +"\fBpydoc disorder\fR" for the Python API documentation. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:f4ecee92157cdac8ee0f71c3fc14044d diff --git a/doc/disorder.3 b/doc/disorder.3 new file mode 100644 index 0000000..62508f5 --- /dev/null +++ b/doc/disorder.3 @@ -0,0 +1,430 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder 3 +.SH NAME +disorder \- plugin interface to DisOrder jukebox +.SH SYNOPSIS +.B "#include <disorder.h>" +.SH DESCRIPTION +This header file defines the plugin interface to DisOrder. +.PP +The first half of this man page describes the functions DisOrder +provides to plugins; the second half describes the functions that +plugins must provide. +.SH "MEMORY ALLOCATION" +DisOrder uses a garbage collector internally. Therefore it is recommended that +plugins use the provided memory allocation interface, rather than calling +\fBmalloc\fR(3) etc directly. +.PP +.nf +\fBvoid *disorder_malloc(size_t); +void *disorder_realloc(void *, size_t); +.fi +.IP +These functions behave much like \fBmalloc\fR(3) and \fBrealloc\fR(3) +except that they never fail; they always zero out the memory +allocated; and you do not need to free the result. +.IP +They may still return a null pointer if asked for a 0-sized +allocation. +.PP +.nf +\fBvoid *disorder_malloc_noptr(size_t); +void *disorder_realloc_noptr(void *, size_t); +.fi +.IP +These functions are like \fBmalloc\fR(3) and \fBrealloc\fR(3) +except that they never fail and you must not put any pointer +values in the allocated memory. +.IP +They may still return a null pointer if asked for a 0-sized +allocation. They do not guarantee to zero out the memory allocated. +.PP +.nf +\fBchar *disorder_strdup(const char *); +char *disorder_strndup(const char *, size_t); +.fi +.IP +These functions are like \fBstrdup\fR(3) and \fBstrndup\fR(3) except +that they never fail and you do not need to free the result. +.PP +.nf +\fBint disorder_asprintf(char **rp, const char *fmt, ...); +int disorder_snprintf(char buffer[], size_t bufsize, + const char *fmt, ...); +.fi +.IP +These function are like \fBsnprintf\fR(3) and \fBasprintf\fR(3). +.B disorder_asprintf +never fails on memory allocation and +you do not need to free the results. +.IP +Floating point conversions and wide character support are not +currently implemented. +.PP +"Never fail" in the above means that the process is terminated on error. +.SH LOGGING +Standard error doesn't reliably go anywhere in current versions of DisOrder, +and whether syslog is to be used varies depending on how the program is +invoked. Therefore plugins should use these functions to log any errors or +informational messages. +.PP +.nf +\fBvoid disorder_error(int errno_value, const char *fmt, ...); +.fi +.IP +Log an error message. If \fBerrno_value\fR is not 0 then the relevant +string is included in the error message. +.PP +.nf +\fBvoid disorder_fatal(int errno_value, const char *fmt, ...); +.fi +.IP +Log an error message and then terminate the process. If +\fBerrno_value\fR is not 0 then the relevant string is included in the +error message. +.IP +.B disorder_fatal +is the right way to terminate the process if a fatal error arises. +You shouldn't usually try to use \fBexit\fR(3) or \fB_exit\fR(2). +.PP +.nf +\fBvoid disorder_info(const char *fmt, ...); +.fi +.IP +Log a message. +.IP +.SH "TRACK DATABASE" +The functions in this section provide a way of accessing the track database. +In server plugins these access the database directly; in client plugins the +requests are transmitted to the server over a socket. +.PP +All strings in this section are encoded using UTF-8. +.PP +.nf +\fBint disorder_track_exists(const char *track); +.fi +.IP +This function returns non-0 if \fBtrack\fR exists and 0 if it does +not. +.PP +.nf +\fBconst char *disorder_track_get_data(const char *track, + const char *key); +.fi +.IP +This function looks up the value of \fBkey\fR for \fBtrack\fR and +returns a pointer to a copy of it. Do not bother to free the pointer. +If the track or key are not found a null pointer is returned. +.PP +.nf +\fBint disorder_track_set_data(const char *track, + const char *key, + const char *value); +.fi +.IP +This function sets the value of \fBkey\fR for \fBtrack\fR to +\fBvalue\fR. On success, 0 is returned; on error, -1 is returned. +.IP +If \fBvalue\fR is a null pointer then the preference is deleted. +.IP +Values starting with an underscore are stored in the tracks database, +and are lost if the track is deleted; they should only ever have +values that can be regenerated on demand. Other values are stored in +the prefs database and never get automatically deleted. +.PP +.nf +\fBconst char *disorder_track_random(void) +.fi +.IP +Returns a pointer to a copy of the name of a randomly chosen track. +Each non-alias track has an equal probability of being chosen. +Aliases are never returned. +Only available in server plugins. +.SH "PLUGIN FUNCTIONS" +This section describes the functions that you must implement to write various +plugins. All of the plugins have at least one standard implementation +available in the DisOrder source. +.PP +Some functions are listed as only available in server plugins. +Currently this means that they are not even defined outside the +server. +.PP +All strings in this section are encoded using UTF-8. +.SS tracklength.so +This is a server plugin. +.PP +.nf +\fBlong disorder_tracklength(const char *track, + const char *path); +.fi +.IP +Called to calculate the length of a track. \fBtrack\fR is the track +name (UTF-8) and \fBpath\fR is the path name if there was one, or a +null pointer otherwise. \fBpath\fR will be the same byte string return from +the scanner plugin, and so presumably encoded according to the +filesystem encoding. +.IP +If the return value is positive it should be the track length in +seconds (round up if it is not an integral number of seconds long). +.IP +If the return value is zero then the track length is unknown. +.IP +If the return value is negative then an error occurred determining the +track length. +.PP +Tracklength plugins are invoked from a subprocess of the server, so +they can block without disturbing the server's operation. +.SS notify.so +This is a server plugin. +.PP +.nf +\fBvoid disorder_notify_play(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR is about to be played. \fBsubmitter\fR identifies the +submitter or is a null pointer if the track was picked for random play. +.PP +.nf +\fBvoid disorder_notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +.fi +.IP +Called when \fBtrack\fR is scratched by \fBscratcher\fR. \fBsubmitter\fR +identifies the submitter or is a null pointer if the track was picked for +random play. \fBseconds\fR is the number of seconds since the track started +playing. +.PP +.nf +\fBvoid disorder_notify_not_scratched(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR completes without being scratched (an error might have +occurred though). \fBsubmitter\fR identifies the submitter or is a null +pointer if the track was picked for random play. +.PP +.nf +\fBvoid disorder_notify_queue(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR is added to the queue by \fBsubmitter\fR +(which is never a null pointer). Not called for scratches. +.PP +.nf +\fBvoid disorder_notify_queue_remove(const char *track, + const char *remover); +.fi +.IP +Called when \fBtrack\fR is removed from queue by \fBremover\fR (which +is never a null pointer). +.PP +.nf +\fBvoid disorder_notify_queue_move(const char *track, + const char *remover); +.fi +.IP +Called when \fBtrack\fR is moved in the queue by \fBmover\fR +(which is never a null pointer). +.PP +.nf +\fBvoid disorder_notify_pause(const char *track, + const char *who); +.fi +.IP +Called when \fBtrack\fR is paused by \fBwho\fR +(which might be a null pointer). +.PP +.nf +\fBvoid disorder_notify_resume(const char *track, + const char *who); +.fi +.IP +Called when \fBtrack\fR is resumed by \fBwho\fR +(which might be a null pointer). +.SS "Scanner Plugins" +Scanner plugins are server plugins and may have any name; they are +chosen via the configuration file. +.PP +.nf +\fBvoid disorder_scan(const char *root); +.fi +.IP +Write a list of files below \fBroot\fR to standard output. Each +filename should be in the encoding defined for this root in the +configuration file and should be terminated by character 0. +.IP +It is up to the plugin implementor whether they prefer to use stdio or +write to file descriptor 1 directly. +.IP +All the filenames had better start with \fBroot\fR as this is used to +match them back up to the right collection to call +\fBdisorder_check\fR on. +.PP +.nf +\fBint disorder_check(const char *root, const char *path); +.fi +.IP +Check whether file \fBpath\fR under \fBroot\fR still exists. Should +return 1 if it exists, 0 if it does not and -1 on error. This is run +in the main server process. +.PP +Both scan and recheck are executed inside a subprocess, so it will not +break the server if they block for an extended period (though of +course, they should not gratuitously take longer than necessary to do +their jobs). +.SS "Player plugins" +Player plugins are server plugins and may have any name; they are +chosen via the configuration file. +.PP +.nf +extern const unsigned long disorder_player_type; +.fi +.IP +This defines the player type and capabilities. It should consist of a +single type value ORed with any number of capability values. The +following are known type values: +.RS +.TP +.B DISORDER_PLAYER_STANDALONE +A standalone player that writes directly to some suitable audio +device. +.TP +.B DISORDER_PLAYER_RAW +A player that writes raw samples to \fB$DISORDER_RAW_FD\fR, for +instance by using the \fBdisorder\fR libao driver. +.RE +.IP +Known capabilities are: +.RS +.TP +.B DISORDER_PLAYER_PREFORK +Supports the prefork and cleanup calls. +.TP +.B DISORDER_PLAYER_PAUSES +Supports the pause and resume calls. +.RE +.PP +.nf +\fBvoid *disorder_play_prefork(const char *track); +.fi +.IP +Called before a track is played, if \fB_PREFORK\fR is set. +\fBtrack\fR is the name of the track in UTF-8. This function must +never block, as it runs inside the main loop of the server. +.IP +The return value will be passed to the functions below as \fBdata\fR. +On error, a null pointer should be returned. +.PP +.nf +\fBvoid disorder_play_cleanup(void *data); +.fi +.IP +Called after a track has been completed, if \fB_PREFORK\fR is set, for +instance to release the memory used by \fBdata\fR. This function must +never block, as it runs inside the main loop of the server. +.PP +.nf +\fBvoid disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void *data); +.fi +.IP +Play a track. +.IP +\fBpath\fR is the path name as originally encoded in the filesystem. +This is the value you should ultimately pass to \fBopen\fR(2). +.IP +\fBtrack\fR is the path name converted to UTF-8. This value (possibly +converted to some other encoding) should be used in any logs, etc. +.IP +If there is no meaningful path, or if the track is a scratch (where no +filename encoding information is available), \fBpath\fR will be equal +to \fBtrack\fR. +.IP +The parameters are any additional arguments +supplied to the \fBplayer\fR configuration file command. +.IP +This function is always called inside a fork, and it should not return +until playing has finished. +.IP +DisOrder sends the subprocess a signal if the track is to be scratched +(and when \fBdisorderd\fR is shut down). By default this signal is +\fBSIGKILL\fR but it can be reconfigured. +.PP +.nf +\fBint disorder_play_pause(long *playedp, + void *data); +.fi +.IP +Pauses the current track, for players that support pausing. This +function must never block, as it runs inside the main loop of the +server. +.IP +On success, should return 0 and set \fB*playedp\fR to the number of +seconds played so far of this track, or to -1 if this cannot be +determined. +.IP +On error, should return -1. +.PP +.nf +\fBvoid disorder_play_resume(void *data); +.fi +.IP +Resume playing the current track after a pause. This function must +never block, as it runs inside the main loop of the server. +.SH NOTES +There is no special DisOrder library to link against; the symbols are +exported by the executables themselves. +(You should NOT try to link against \fB-ldisorder\fR.) +Plugins must be separately +linked against any other libraries they require, even if the DisOrder +executables are already linked against them. +.PP +The easiest approach is probably to develop the plugin inside the +DisOrder tree; then you can just use DisOrder's build system. This +might also make it easier to submit patches if you write something of +general utility. +.PP +Failing that you can use Libtool, if you make sure to pass the +\fB-module\fR option. For current versions of DisOrder you only need +the shared object itself, not the \fB.la\fR file. +.PP +If you know the right runes for your toolchain you could also build +the modules more directly. +.PP +It is possible, up to a point, to implement several plugin interfaces +from within a single shared object. If you ever use any of the +functions that are listed as only being available in server plugins, +though, then you can only use the resulting shared object as a server +plugin. +.SH "SEE ALSO" +.BR disorderd (8), +.BR disorder (1), +.BR disorder_config (5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:ff45ed6fb0bc4fff61a4179138e26f01 diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in new file mode 100644 index 0000000..d9bd260 --- /dev/null +++ b/doc/disorder_config.5.in @@ -0,0 +1,1022 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder_config 5 +.SH NAME +pkgconfdir/config - DisOrder jukebox configuration +.SH DESCRIPTION +The purpose of DisOrder is to organize and play digital audio files, under the +control of multiple users. \fIpkgconfdir/config\fR is the primary +configuration file but this man page currently documents all of its various +configuration files. +.SS Tracks +DisOrder can be configured with multiple collections of tracks, indexing them +by their filename, and picking players on the basis of filename patterns (for +instance, "*.mp3"). +.PP +Although the model is of filenames, it is not inherent that there are +corresponding real files - merely that they can be interpreted by the chosen +player. See \fBdisorder\fR(3) for more details about this. +.PP +Each track can have a set of preferences associated with it. These are simple +key-value pairs; they can be used for anything you like, but a number of keys +have specific meanings. See \fBdisorder\fR(1) for more details about these. +.SS "Track Names" +Track names are derived from filenames under the control of regular +expressions, rather than attempting to interpret format-specific embedded name +information. They can be overridden by setting preferences. +.PP +Names for display are distinguished from names for sorting, so with the right +underlying filenames an album can be displayed in its original order even if +the displayed track titles are not lexically sorted. +.SS "Server State" +A collection of global preferences define various bits of server state: whether +random play is enabled, what tags to check for when picking at random, etc. +.SS "Users And Access Control" +DisOrder distinguishes between multiple users. This is for access control and +reporting, not to provide different views of the world: i.e. preferences and so +on are global. +.PP +It's possible to restrict a small number of operations to a specific subset of +users. However, it is assumed that every user is supposed to be able to do +most operations - since the users are all sharing the same audio environment +they are expected to cooperate with each other. +.PP +Access control is entirely used-based. If you configure DisOrder to listen for +TCP/IP connections then it will accept a connection from anywhere provided the +right password is available. Passwords are never transmitted over TCP/IP +connections in clear, but everything else is. The expected model is that +host-based access control is imposed at the network layer. +.SS "Web Interface" +The web interface is controlled by a collection of template files, one for each +kind of page, and a collection of option files. These are split up and +separate from the main configuration file to make it more convenient to +override specific bits. +.PP +The web interface connects to the DisOrder server like any other user, though +it is given a special privilege to "become" any other user. (Thus, any process +with the same UID as the web interface is very powerful as far as DisOrder +goes.) +.PP +Access control to the web interface is (currently) separate from DisOrder's own +access control (HTTP authentication is required) but uses the same user +namespace. +.SH "CONFIGURATION FILE" +.SS "General Syntax" +Lines are split into fields separated by whitespace (space, tab, line +feed, carriage return, form feed). Comments are started by the number +sign ("#"). +.PP +Fields may be unquoted (in which case they may not contain spaces and +may not start with a quotation mark or apostrophe) or quoted by either +quotation marks or apostrophes. Inside quoted fields every character +stands for itself, except that a backslash can only appear as part of +one of the following escape sequences: +.TP +.B \e\e +Backslash +.TP +.B \e" +Quotation mark +.\" " +.TP +.B \e' +Apostrophe +.TP +.B \en +Line feed +.PP +No other escape sequences are allowed. +.PP +Within any line the first field is a configuration command and any +further fields are parameters. Lines with no fields are ignored. +.PP +After editing the config file use \fBdisorder reconfigure\fR to make +it re-read it. If there is anything wrong with it the daemon will +record a log message and ignore the new config file. (You should fix +it before next terminating and restarting the daemon, as it cannot +start up without a valid config file.) +.SS "Global Configuration" +.TP +.B home \fIDIRECTORY\fR +The home directory for state files. Defaults to +.IR pkgstatedir . +.TP +.B plugin \fIPATH\fR +Adds a directory to the plugin path. (This is also used by the web +interface.) +.IP +Plugins are opened the first time they are required and never after, +so after changing a plugin you must restart the server before it is +guaranteed to take effect. +.SS "Server Configuration" +.TP +.B alias \fIPATTERN\fR +Defines the pattern use construct virtual filenames from \fBtrackname_\fR +preferences. +.IP +Most characters stand for themselves, the exception being \fB{\fR which is used +to insert a track name part in the form \fB{\fIname\fB}\fR or +\fB{/\fIname\fB}\fR. +.IP +The difference is that the first form just inserts the name part while the +second prefixes it with a \fB/\fR if it is nonempty. +.IP +The pattern should not attempt to include the collection root, which is +automatically included, but should include the proper extension. +.IP +The default is \fB{/artist}{/album}{/title}{ext}\fR. +.TP +.B channel \fICHANNEL\fR +The mixer channel that the volume control should use. Valid names depend on +your operating system and hardware, but some standard ones that might be useful +are: +.RS +.TP 8 +.B pcm +Output level for the audio device. This is probably what you want. +.TP +.B speaker +Output level for the PC speaker, if that is connected to the sound card. +.TP +.B pcm2 +Output level for alternative codec device. +.TP +.B vol +Master output level. The OSS documentation recommends against using this, as +it affects all output devices. +.RE +.IP +You can also specify channels by number, if you know the right value. +.TP +.B collection \fIMODULE\fR \fIENCODING\fR \fIROOT\fR +Define a collection of tracks. +.IP +\fIMODULE\fR defines which plugin module should be used for this +collection. Use the supplied \fBfs\fR module for tracks that exists +as ordinary files in the filesystem. +.IP +\fIENCODING\fR defines the encoding of filenames in this collection. +For \fBfs\fR this would be the encoding you use for filenames. +Examples might be \fBiso-8859-1\fR or \fButf-8\fR. +.IP +\fIROOT\fR is the root in the filesystem of the filenames and is +passed to the plugin module. +.TP +.B device \fINAME\fR +ALSA device to play raw-format audio. Default is \fBdefault\fR, i.e. to use +the whatever the ALSA configured default is. +.TP +.B gap \fISECONDS\fR +Specifies the number of seconds to leave between tracks. The default +is 2. +.TP +.B history \fIINTEGER\fR +Specifies the number of recently played tracks to remember (including +failed tracks and scratches). +.TP +.B listen \fR[\fIHOST\fR] \fISERVICE\fR +Listen for connections on the address specified by \fIHOST\fR and port +specified by \fISERVICE\fR. If \fIHOST\fR is omitted then listens on all +local addresses. +.IP +Normally the server only listens on a UNIX domain socket. +.TP +.B lock yes\fR|\fBno +Determines whether the server locks against concurrent operation. Default is +\fByes\fR. +.TP +.B mixer \fIPATH\fR +The path to the mixer device, if you want access to the volume control, +e.g. \fB/dev/mixer\fR. +.TP +.B namepart \fIPART\fR \fIREGEXP\fR \fISUBST\fR [\fICONTEXT\fR [\fIREFLAGS\fR]] +Determines how to extract trackname part \fIPART\fR from a +track name (with the collection root part removed). +Used in \fB@recent@\fR, \fB@playing@\fR and \fB@search@\fR. +.IP +Track names can be different in different contexts. For instance the sort +string might include an initial track number, but this would be stripped for +the display string. \fICONTEXT\fR should be a glob pattern matching the +contexts in which this directive will be used. +.IP +Valid contexts are \fBsort\fR and \fBdisplay\fR. +.IP +All the \fBnamepart\fR directives are considered in order. The +first directive for the right part, that matches the desired context, +and with a \fIREGEXP\fR that +matches the track is used, and the value chosen is constructed from +\fISUBST\fR according to the substitution rules below. +.IP +Note that searches use the raw track name and \fBtrackname_\fR preferences but +not (currently) the results of \fBnamepart\fR, so generating words via this option +that aren't in the original track name will lead to confusing results. +.IP +If you supply no \fBnamepart\fR directives at all then a default set will be +supplied automatically. But if you supply even one then you must supply all of +them. See the example config file for the defaults. +.TP +.B nice_rescan \fIPRIORITY\fR +Set the recan subprocess priority. The default is 10. +.IP +(Note that higher values mean the process gets less CPU time; UNIX priority +values are the backwards.) +.TP +.B nice_server \fIPRIORITY\fR +Set the server priority. This is applied to the server at startup time (and +not when you reload configuration). The server does not use much CPU itself +but this value is inherited by programs it executes. If you have limited CPU +then it might help to set this to a small negative value. The default is 0. +.TP +.B nice_speaker \fIPRIORITY\fR +Set the speaker process priority. This is applied to the speaker process at +startup time (and not when you reload the configuration). The speaker process +is not massively CPU intensive by today's standards but depends on reasonably +timely scheduling. If you have limited CPU then it might help to set this to a +small negative value. The default is 0. +.TP +.B player \fIPATTERN\fR \fIMODULE\fR [\fIOPTIONS.. [\fB--\fR]] \fIARGS\fR... +Specifies the player for files matching the glob \fIPATTERN\fR. \fIMODULE\fR +specifies which plugin module to use. +.IP +The following options are supported: +.RS +.TP +.B --wait-for-device\fR[\fB=\fIDEVICE\fR] +Waits (for up to a couple of seconds) for the default, or specified, libao +device to become openable. +.TP +.B -- +Defines the end of the list of options. Needed if the first argument to the +plugin starts with a "-". +.RE +.IP +The following are the standard modules: +.RS +.TP +.B exec \fICOMMAND\fR \fIARGS\fR... +The command is executed via \fBexecvp\fR(3), not via the shell. +The \fBPATH\fR environment variable is searched for the executable if it is not +an absolute path. +The command is expected to know how to open its own sound device. +.TP +.B execraw \fICOMMAND\fR \fIARGS\fR... +Identical to the \fBexec\fR except that the player is expected to use the +DisOrder raw player protocol (see notes below). +.TP +.B shell \fR[\fISHELL\fR] \fICOMMAND\fR +The command is executed using the shell. If \fISHELL\fR is specified then that +is used, otherwise \fBsh\fR will be used. In either case the \fBPATH\fR +environment variable is searched for the shell executable if it is not an +absolute path. The track name is stored in the environment variable +\fBTRACK\fR. +.IP +Be careful of the interaction between the configuration file quoting rules and +the shell quoting rules. +.RE +.IP +If multiple player commands match a track then the first match is used. +.TP +.B prefsync \fISECONDS\fR +The interval at which the preferences log file will be synchronised. Defaults +to 3600, i.e. one hour. +.TP +.B signal \fINAME\fR +Defines the signal to be sent to track player process groups when tracks are +scratched. The default is \fBSIGKILL\fR. +.IP +Signals are specified by their full C name, i.e. \fBSIGINT\fR and not \fBINT\fR +or \fBInterrupted\fR or whatever. +.TP +.B restrict \fR[\fBscratch\fR] [\fBremove\fR] [\fBmove\fR] +Determine which operations are restricted to the submitter of a +track. By default, no operations are restricted, i.e. anyone can +scratch or remove anything. +.IP +If \fBrestrict scratch\fR or \fBrestrict remove\fR are set then only the user +that submitted a track can scratch or remove it, respectively. +.IP +If \fBrestrict move\fR is set then only trusted users can move tracks around in +the queue. +.IP +If \fBrestrict\fR is used more than once then only the final use has any +effect. +.TP +.B scratch \fIPATH\fR +Specifies a scratch. When a track is scratched, a scratch track is +played at random. +Scratches are played using the same logic as other tracks. +.IP +At least for the time being, path names of scratches must be encoded using +UTF-8 (which means that ASCII will do). +.TP +.B stopword \fIWORD\fR ... +Specifies one or more stopwords that should not take part in searches +over track names. +.SS "Client Configuration" +.TP +.B connect \fR[\fIHOST\fR] \fISERVICE\fR +Connect to the address specified by \fIHOST\fR and port specified by +\fISERVICE\fR. If \fIHOST\fR is omitted then connects to the local host. +Normally the UNIX domain socket is used instead. +.SS "Web Interface Configuration" +.TP +.B refresh \fISECONDS\fR +Specifies the maximum refresh period in seconds. Default 15. +.TP +.B templates \fIPATH\fR ... +Specifies the directory containing templates used by the web +interface. If a template appears in more than one template directory +then the one in the earliest directory specified is chosen. +.IP +See below for further details. +.TP +.B transform \fITYPE\fR \fIREGEXP\fR \fISUBST\fR [\fICONTEXT\fR [\fIREFLAGS\fR]] +Determines how names are sorted and displayed in track choice displays. +.IP +\fITYPE\fR is the type of transformation; usually \fBtrack\fR or +\fBdir\fR but you can define your own. +.IP +\fICONTEXT\fR is a glob pattern matching the context. Standard contexts are +\fBsort\fR (which determines how directory names are sorted) and \fBdisplay\fR +(which determines how they are displayed). Again, you can define your +own. +.IP +All the \fBtransform\fR directives are considered in order. If +the \fITYPE\fR, \fIREGEXP\fR and the \fICONTEXT\fR match +then a new track name is constructed from +\fISUBST\fR according to the substitution rules below. If several +match then each is executed in order. +.IP +If you supply no \fBtransform\fR directives at all then a default set will be +supplied automatically. But if you supply even one then you must supply all of +them. See the example config file for the defaults. +.TP +.B url \fIURL\fR +Specifies the URL of the web interface. This URL will be used in +generated web pages. +.IP +This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not +\fB/cgi-bin/jukebox\fR. +.SS "Authentication Configuration" +.TP +.B allow \fIUSERNAME\fR \fIPASSWORD\fR +Specify a username/password pair. +.TP +.B password \fIPASSWORD\fR +Specify password. +.TP +.B trust \fIUSERNAME\fR +Allow \fIUSERNAME\fR to perform privileged operations such as shutting +down or reconfiguring the daemon, or becoming another user. +.TP +.B user \fIUSER\fR +Specifies the user to run as. Only makes sense if invoked as root (or +the target user). +.TP +.B username \fIUSERNAME\fR +Specify username. The default is taken from the environment variable +\fBLOGNAME\fR. +.PP +Configuration files are read in the following order: +.TP +.I pkgconfdir/config +.TP +.I pkgconfdir/config.private +Should be readable only by the jukebox group, and contain \fBallow\fR +commands for authorised users. +.TP +.I pkgconfdir/config.\fRUSER +Per-user system-controlled client configuration. Optional but if it +exists must be readable only by the relevant user. Would normally +contain a \fBpassword\fR directive. +.TP +.I ~\fRUSER\fI/.disorder/passwd +Per-user client configuration. Optional but if it exists must be +readable only by the relevant user. Would normally contain a +\fBpassword\fR directive. +.SH "GLOBAL PREFERENCES" +These are the values set with \fBset-global\fR. +.TP +.B required-tags +If this is set an nonempty then randomly played tracks will always have at +least one of the listed tags. +.IP +Tags can contain any printing character except comma. Leading and trailing +spaces are not significant but internal spaces are. Tags in a list are +separated by commas. +.TP +.B prohibited-tags +If this is set an nonempty then randomly played tracks will never have any of +the listed tags. +.TP +.B playing +If unset or \fByes\fR then play is enabled. Otherwise it is disabled. Use +\fBdisable\fR rather than setting it directly. +.TP +.B random-play +If unset or \fByes\fR then random play is enabled. Otherwise it is disabled. +Use \fBdisable\fR rather than setting it directly. +.SH "LIBAO DRIVER" +.SS "Raw Protocol Players" +Raw protocol players are expected to use the \fBdisorder\fR libao driver. +Programs that use libao generally have command line options to select the +driver and pass options to it. +.SS "Driver Options" +The known driver options are: +.TP +.B fd +The file descriptor to write to. If this is not specified then the driver +looks like the environment variable \fBDISORDER_RAW_FD\fR. If that is not set +then the default is 1 (i.e. standard output). +.TP +.B fragile +If this is set to a nonzero value then the driver will call \fB_exit\fR(2) if a +write to the output file descriptor fails. This is a workaround for buggy +players such as \fBogg123\fR that ignore write errors. +.SH "WEB TEMPLATES" +When \fBdisorder.cgi\fR wants to generate a page for an action it searches the +directories specified with \fBtemplates\fR for a matching file. It is +suggested that you leave the distributed templates unchanged and put +any customisations in an earlier entry in the template path. +.PP +The supplied templates are: +.TP +.B about.html +Display information about DisOrder. +.TP +.B choose.html +Navigates through the track database to choose a track to play. The +\fBdir\fR argument gives the directory to look in; if it is missing +then the root directory is used. +.TP +.B choosealpha.html +Provides a front end to \fBchoose.html\fR which allows subsets of the top level +directories to be selected by initial letter. +.TP +.B playing.html +The "front page", which usually shows the currently playing tracks and +the queue. +Gets an HTTP \fBRefresh\fR header. +.IP +If the \fBmgmt\fR CGI argument is set to \fBtrue\fR then we include extra +buttons for moving tracks up and down in the queue. There is some logic in +\fBdisorder.cgi\fR to ensure that \fBmgmt=true\fR is preserved across refreshes +and redirects back into itself, but URLs embedded in web pages must include it +explicitly. +.TP +.B prefs.html +Views preferences. If the \fBfile\fR, \fBname\fR and \fBvalue\fR arguments are +all set then that preference is modified; if \fBfile\fR and \fBname\fR are set +but not \fBvalue\fR then the preference is deleted. +.TP +.B recent.html +Lists recently played tracks. +.TP +.B search.html +Presents search results. +.TP +.B volume.html +Primitive volume control. +.PP +Additionally, other standard files are included by these: +.TP +.B credits.html +Included at the end of the main content \fB<DIV>\fR element. +.TP +.B sidebar.html +Included at the start of the \fB<BODY>\fR element. +.TP +.B stdhead.html +Included in the \fB<HEAD>\fR element. +.TP +.B stylesheet.html +Contains the default DisOrder stylesheet. You can override this by editing the +CSS or by replacing it all with a \fB<LINK>\fR to an external stylesheet. +.PP +Templates are ASCII files containing HTML documents, with an expansion +syntax to enable data supplied by the implementation to be inserted. +.PP +If you want to use characters outside the ASCII range, use either the +appropriate HTML entity, e.g. \fBé\fR, or an SGML numeric +character reference, e.g. \fBý\fR. Use \fB@\fR to insert a +literal \fB@\fR without falling foul of the expansion syntax. +.SS "Expansion Syntax" +Expansions are surrounded by at ("@") symbols take the form of a keyword +followed by zero or more arguments. Arguments may either be quoted by curly +brackets ("{" and "}") or separated by colons (":"). Both kinds may be mixed +in a single expansion, though doing so seems likely to cause confusion. +The descriptions below contain suggested forms for each +expansion. +.PP +Leading and trailing whitespace in unquoted arguments is ignored, as is +whitespace (including newlines) following a close bracket ("}"). +.PP +Arguments are recursively expanded before being interpreted, except for +\fITEMPLATE\fR arguments. These are expanded (possibly more than once) to +produce the final expansion. +(More than once means the same argument being expanded more than once +for different tracks or whatever, not the result of the first +expansion itself being re-expanded.) +.PP +Strings constructed by expansions (i.e. not literally copied from the template +text) are SGML-quoted: any character which does not stand for itself in #PCDATA +or a quoted attribute value is replaced by the appropriate numeric character +reference. +.PP +The exception to this is that such strings are \fInot\fR quoted when they are +generated in the expansion of a parameter. +.PP +In the descriptions below, the current track means the one set by +\fB@playing@\fR, \fB@recent@\fR or \fB@queue@\fR, not the one that is playing. +If none of these expansions are in force then there is no current track. +\fIBOOL\fR should always be either \fBtrue\fR or \fBfalse\fR. +.SS "Expansions" +The following expansion keywords are defined: +.TP +.B @#{\fICOMMENT\fB}@ +Ignored. +.TP +.B @action@ +The current action. This reports +.B manage +if the action is really +.B playing +but +.B mgmt=true +was set. +.TP +.B @and{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@ +If there are no arguments, or all the arguments are \fBtrue\fB, then expands to +\fBtrue\fR, otherwise to \fBfalse\fR. +.TP +.B @arg:\fINAME\fB@ +Expands to the value of CGI script argument \fINAME\fR. +.TP +.B @basename@ +The basename of the current directory component, in \fB@navigate@\fR. +.TP +.B @basename{\fIPATH\fB}@ +The base name part of \fIPATH\fR. +.TP +.B @choose{\fIWHAT\fB}{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly for each file or directory under +\fB@arg:directory@\fR. +\fIWHAT\fR should be either \fBfile\fR or \fBdirectory\fR. +Use \fB@file@\fR to get the display name or filename of the file or +directory. +Usually used in \fBchoose.html\fR. +.TP +.B @dirname@ +The directory of the current directory component, in \fB@navigate@\fR. +.TP +.B @dirname{\fIPATH\fB}@ +The directory part of \fIPATH\fR. +.TP +.B @enabled@ +Expands to \fBtrue\fR if play is currently enabled, otherwise to \fBfalse\fR. +.TP +.B @eq{\fIA\fB}{\fIB\fB} +Expands to \fBtrue\fR if \fIA\fR and \fIB\fR are identical, otherwise to +\fBfalse\fR. +.TP +.B @file@ +Expands to the filename of the current file or directory, inside the template +argument to \fBchoose\fR. +.TP +.B @files{\fITEMPLATE\fB} +Expands \fITEMPLATE\fB once for each file indicated by the \fBdirectory\fR CGI +arg if it is present, or otherwise for the list of files counted by \fBfiles\fR +with names \fB0_file\fR, \fB1_file\fR etc. +.TP +.B @fullname@ +The full path of the current directory component, in \fB@navigate@\fR. +.TP +.B @id@ +The ID of the current track. +.TP +.B @if{\fIBOOL\fB}{\fITRUEPART\fB}{\fIFALSEPART\fB}@ +If \fIBOOL\fR expands to \fBtrue\fR then expands to \fITRUEPART\fR, otherwise +to \fIFALSEPART\fR (which may be omitted). +.TP +.B @include:\fIPATH\fR@ +Include the named file as if it were a template file. If \fIPATH\fR +starts with a \fB/\fR then it is used as-is; otherwise, ".html" is +appended and the template path is searched. +.TP +.B @index@ +Expands to the index of the current file in \fB@queue@\fR, \fB@recent@\fR or +\fB@files@\fR. +.TP +.B @isdirectories@ +Expands to \fBtrue\fR if there are any directories in \fB@arg:directory@\fR, +otherwise to \fBfalse\fR. +.TP +.B @isfiles@ +Expands to \fBtrue\fR if there are any files in \fB@arg:directory@\fR, +otherwise to \fBfalse\fR. +.TP +.B @isfirst@ +Expands to \fBtrue\fR if this is the first repetition of a \fITEMPLATE\fR +argument in a loop (\fB@queue\fR or similar), otherwise to \fBfalse\fR. +.TP +.B @islast@ +Expands to \fBtrue\fR if this is the last repetition of a \fITEMPLATE\fR in a +loop, otherwise to \fBfalse\fR. +.TP +.B @isplaying@ +Expands to \fBtrue\fR if a track is playing, otherwise to \fBfalse\fR. +.TP +.B @isqueue@ +Expands to \fBtrue\fR if there are any tracks in the queue, otherwise to +\fBfalse\fR. +.TP +.B @isrecent@ +Expands to \fBtrue\fR if the recently played list has any tracks in it, +otherwise to \fBfalse\fR. +.TP +.B @label:\fINAME\fR\fB@ +Expands to the value of label \fINAME\fR. See the shipped \fIoptions.labels\fR +file for full documentation of the labels used by the standard templates. +.TP +.B @length@ +Expands to the length of the current track. +.TP +.B @navigate{\fIDIRECTORY\fB}{\fITEMPLATE\fB} +Expands \fITEMPLATE\fR for each component of \fIDIRECTORY\fR in turn. +Use \fB@dirname\fR and \fB@basename@\fR to get the components of the path to +each component. +Usually used in \fBchoose.html\fR. +.TP +.B @ne{\fIA\fB}{\fIB\fB} +Expands to \fBtrue\fR if \fIA\fR and \fIB\fR differ, otherwise to \fBfalse\fR. +.TP +.B @nfiles@ +Expands to the number of files from \fB@files\fR (above). +.TP +.B @nonce@ +Expands to a string including the time and process ID, intended to be +unique across invocations. +.TP +.B @not{\fIBOOL\fB}@ +Expands to \fBfalse\fR if \fIBOOL\fR is \fBtrue\fR, otherwise to +\fBfalse\fR. +.TP +.B @or{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@ +If at least one argument is \fBtrue\fB, then expands to \fBtrue\fR, otherwise +to \fBfalse\fR. +.TP +.B @parity@ +Expands to \fBeven\fR or \fBodd\fR depending on whether the current track is at +an even or odd position in \fB@queue@\fR, \fB@recent@\fR or \fB@files@\fR. +.TP +.B @part{\fICONTEXT\fB}{\fIPART\fB}@ +Expands to track name part \fIPART\fR using context \fICONTEXT\fR for the +current track. The context may be omitted (and normally would be) and defaults +to \fBdisplay\fR. +.TP +.B @part{\fICONTEXT\fB}{\fIPART\fB}{\fITRACK\fB}@ +Expands to track name part \fIPART\fR using context \fICONTEXT\fR for +\fITRACK\fR. In this usage the context may not be omitted. +.TP +.B @paused@ +Expands to \fBtrue\fR if the current track is paused, else \fBfalse\fR. +.TP +.B @playing{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR using the playing track as the current track. +.TP +.B @pref{\fITRACK\fB}{\fIKEY\fB}@ +Expand to the track preference, or the empty string if it is not set. +.TP +.B @prefname@ +Expands to the name of the current preference, in the template +argument of \fB@prefs@\fR. +.TP +.B @prefs{\fIFILE\fB}{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly, for each preference of track +\fIFILE\fR. +Use \fB@prefname@\fR and \fB@prefvalue@\fR to get the name and value. +.TP +.B @prefvalue@ +Expands to the value of the current preference, in the template +argument of \fB@prefs@\fR. +.TP +.B @queue{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly using the each track on the queue in turn as +the current track. The track at the head of the queue comes first. +.TP +.B @random-enabled@ +Expands to \fBtrue\fR if random play is currently enabled, otherwise to +\fBfalse\fR. +.TP +.B @recent{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly using the each recently played track in turn +as the current track. The most recently played track comes first. +.TP +.B @resolve{\fITRACK\fB}@ +Resolve aliases for \fITRACK\fR and expands to the result. +.TP +.B @search{\fIPART\fB}\fR[\fB{\fICONTEXT\fB}\fR]\fB{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR once for each group of search results that have +a common value of track part \fIPART\fR. +The groups are sorted by the value of the part. +.IP +.B @part@ +and +.B @file@ +within the template will apply to one of the tracks in the group. +.IP +If \fICONTEXT\fR is specified it should be either \fBsort\fR or \fBdisplay\fR, +and determines the context for \fIPART\fR. The default is \fBsort\fR. Usually +you want \fBdisplay\fR for everything except the title and \fBsort\fR for the +title. If you use \fBsort\fR for artist and album then you are likely to get +strange effects. +.TP +.B @server-version@ +Expands to the server's version string. +.TP +.B @shell{\fICOMMAND\fB}@ +Expands to the output of \fICOMMAND\fR executed via the shell. \fBsh\fR is +searched for using \fBPATH\fR. If the command fails then this is logged but +otherwise ignored. +.TP +.B @state@ +In \fB@queue@\fR and \fB@recent@\fR, expands to the state of the current +track. Otherwise the empty string. Known states are: +.RS +.TP 12 +.B failed +The player terminated with nonzero status, but not because the track was +scratched. +.TP +.B isscratch +A scratch, in the queue. +.TP +.B no_player +No player could be found. +.TP +.B ok +Played successfully. +.TP +.B random +A randomly chosen track, in the queue. +.TP +.B scratched +This track was scratched. +.TP +.B unplayed +An explicitly queued track, in the queue. +.RE +.IP +Some additional states only apply to playing tracks, so will never be seen in +the queue or recently-played list: +.RS +.TP 12 +.B paused +The track has been paused. +.TP +.B quitting +Interrupted because the server is shutting down. +.TP +.B started +This track is currently playing. +.RE +.TP +.B @stats@ +Expands to the server statistics. +.TP +.B @thisurl@ +Expands to the URL of the current page. Typically used in +.B back +arguments. If there is a +.B nonce +argument then it is changed to a fresh value. +.TP +.B @track@ +The current track. +.TP +.B @trackstate{\fIPATH\fB}@ +Expands to the current track state: \fBplaying\fR if the track is actually +playing now, \fBqueued\fR if it is queued or the empty string otherwise. +.TP +.B @transform{\fIPATH\fB}{\fITYPE\fB}{\fICONTEXT\fB}@ +Transform a path according to \fBtransform\fR (see above). +\fIPATH\fR should be a raw filename (of a track or directory). +\fITYPE\fR should be the transform type (e.g. \fItrack\fR or \fIdir\fR). +\fICONTEXT\fR should be the context, and can be omitted (the default +is \fBdisplay\fR). +.TP +.B @url@ +Expands to the canonical URL as defined in \fIpkgconfdir/config\fR. +.TP +.B @urlquote{\fISTRING\fB}@ +URL-quote \fISTRING\fR. +.TP +.B @version@ +Expands to \fBdisorder.cgi\fR's version string. +.TP +.B @volume:\fISPEAKER\fB@ +The volume on the left or right speaker. \fISPEAKER\fR must be \fBleft\fB or +\fBright\fR. +.TP +.B @when@ +When the current track was played (or when it is expected to be played, if it +has not been played yet) +.TP +.B @who@ +Who submitted the current track. +.SH "WEB OPTIONS" +This is a file called \fIoptions\fR, searched for in the same manner +as templates. It includes numerous options for the control of the web +interface. The general syntax is the same as the main configuration +file, except that it should be encoded using UTF-8 (though this might +change to the current locale's character encoding; stick to ASCII to +be safe). +.PP +The shipped \fIoptions\fR file includes four standard options files. +In order, they are: +.TP +.I options.labels +The default labels file. You wouldn't normally edit this directly - instead +supply your own commands in \fIoptions.user\fR. Have a look at the shipped +version of the file for documentation of labels used by the standard templates. +.TP +.I options.user +A user options file. Here you should put any overrides for the default +labels and any extra labels required by your modified templates. +.PP +Valid directives are: +.TP +.B columns \fINAME\fR \fIHEADING\fR... +Defines the columns used in \fB@playing@\fR and \fB@recent@\fB. \fINAME\fR +must be either \fBplaying\fR, \fBrecent\fR or \fBsearch\fR. +\fIHEADING\fR... is a list of +heading names. If a column is defined more than once then the last definitions +is used. +.IP +The heading names \fBbutton\fR, \fBlength\fR, \fBwhen\fR and \fBwho\fR +are built in. +.TP +.B include \fIPATH\fR +Includes another file. If \fIPATH\fR starts with a \fB/\fR then it is +taken as is, otherwise it is searched for in the template path. +.TP +.B label \fINAME\fR \fIVALUE\fR +Define a label. If a label is defined more than once then the last definition +is used. +.SS Labels +Some labels are defined inside \fBdisorder.cgi\fR and others by the +default templates. You can define your own labels and use them inside +a template. +.PP +When an undefined label is expanded, if it has a dot in its name then +the part after the final dot is used as its value. Otherwise the +whole name is used as the value. +.PP +Labels are no longer documented here, see the shipped \fIoptions.labels\fR file +instead. +.SH "REGEXP SUBSTITUTION RULES" +Regexps are PCRE regexps, as defined in \fBpcrepattern\fR(3). The +only option used is \fBPCRE_UTF8\fR. Remember that the configuration +file syntax means you have to escape backslashes and quotes inside +quoted strings. +.PP +In a \fISUBST\fR string the following sequences are interpreted +specially: +.TP +.B $1 \fR... \fB$9 +These expand to the first to ninth bracketed subexpression. +.TP +.B $& +This expands to the matched part of the subject string. +.TP +.B $$ +This expands to a single \fB$\fR symbol. +.PP +All other pairs starting with \fB$\fR are undefined (and might be used +for something else in the future, so don't rely on the current +behaviour.) +.PP +If \fBi\fR is present in \fIREFLAGS\fR then the match is case-independent. If +\fBg\fR is present then all matches are replaced, otherwise only the first +match is replaced. +.SH "ACTIONS" +What the web interface actually does is terminated by the \fBaction\fR CGI +argument. The values listed below are supported. +.PP +Except as specified, all actions redirect back to the \fBplaying.html\fR +template unless the \fBback\fR argument is present, in which case the URL it +gives is used instead. +.PP +Redirection to \fBplaying.html\fR preserves \fBmgmt=true\fR if it is present. +.TP 8 +.B "move" +Move track \fBid\fR by offset \fBdelta\fR. +.TP +.B "play" +Play track \fBfile\fR, or if that is missing then play all the tracks in +\fBdirectory\fR. +.TP +.B "playing" +Don't change any state, but instead compute a suitable refresh time and include +that in an HTTP header. Expands the \fBplaying.html\fR template rather than +redirecting. +.IP +This is the default if \fBaction\fR is missing. +.TP +.B "random-disable" +Disables random play. +.TP +.B "random-enable" +Enables random play. +.TP +.B "disable" +Disables play completely. +.TP +.B "enable" +Enables play. +.TP +.B "pause" +Pauses the current track. +.TP +.B "remove" +Remove track \fBid\fR. +.TP +.B "resume" +Resumes play after a pause. +.TP +.B "scratch" +Scratch the playing track. If \fBid\fR is present it must match the playing +track. +.TP +.B "volume" +Change the volume by \fBdelta\fR, or if that is missing then set it to the +values of \fBleft\fR and \fBright\fR. Expands to the \fBvolume.html\fR template +rather than redirecting. +.TP +.B "prefs" +Adjust preferences from the \fBprefs.html\fR template (which it then expands +rather than redirecting). +.IP +If +.B parts +is set then the cooked interface is assumed. The value of +.B parts +is used to determine which trackname preferences are set. By default the +.B display +context is adjusted but this can be overridden with the +.B context +argument. Also the +.B random +argument is checked; if it is set then random play is enabled for that track, +otherwise it is disabled. +.IP +Otherwise if the +.B name +and +.B value +arguments are set then they are used to set a single preference. +.IP +Otherwise if just the +.B name +argument is set then that preference is deleted. +.IP +It is recommended that links to the \fBprefs\fR action use \fB@resolve@\fR to +enure that the real track name is always used. Otherwise if the preferences +page is used to adjust a trackname_ preference, the alias may change, leading +to the URL going stale. +.TP +.B "error" +This action is generated automatically when an error occurs connecting to the +server. The \fBerror\fR label is set to an indication of what the error is. +.SH "TRACK NAME PARTS" +The traditional track name parts are \fBartist\fR, \fBalbum\fR and \fBtitle\fR, +with the obvious intended meaning. These are controlled by configuration and +by \fBtrackname_\fR preferences. +.PP +In addition there are two built-in parts, \fBpath\fR which is the whole path +name and \fBext\fR which is the filename extension, including the initial dot +(or the empty string if there is not extension). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorderd\fR(8), \fBdisorder-dump\fR(8), +\fBpcrepattern\fR(3) +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:43b51c6f7ce647119d5409797c55908e diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in new file mode 100644 index 0000000..0344ac8 --- /dev/null +++ b/doc/disorder_protocol.5.in @@ -0,0 +1,444 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder_protocol 5 +.SH NAME +disorder_protocol \- DisOrder communication protocol +.SH DESCRIPTION +The DisOrder client and server communicate via the protocol described +in this man page. +.PP +The protocol is liable to change without notice. You are recommended to check +the implementation before believing this document. +.SH "GENERAL SYNTAX" +Everything is encoded using UTF-8. +.PP +Commands and responses consist of a line followed (depending on the +command or response) by a message. +.PP +The line syntax is the same as described in \fBdisorder_config\fR(5) except +that comments are prohibited. +.PP +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. +.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. See below for more +details about this field. +.PP +All commands require the connection to have been already authenticated unless +stated otherwise. +.PP +Neither commands nor responses have a body unless stated otherwise. +.TP +.B allfiles \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the files and directories in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching files and directories are returned. +.TP +.B become \fIUSER\fR +Instructs the server to treat the connection as if \fIUSER\fR had +authenticated it. Only trusted users may issue this command. +.TP +.B dirs \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the directories in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching directories are returned. +.TP +.B disable \fR[\fBnow\fR] +Disables further playing. If the optional \fBnow\fR argument is present then +the current track is stopped. +.TP +.B enable +Re-enables further playing, and is the opposite of \fBdisable\fR. +.TP +.B enabled +Reports whether playing is enabled. The second field of the response line will +be \fByes\fR or \fBno\fR. +.TP +.B exists \fITRACK\fR +Reports whether the named track exists. The second field of the response line +will be \fByes\fR or \fBno\fR. +.TP +.B files \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the files in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching files are returned. +.TP +.B get \fITRACK\fR \fIPREF\fR +Gets a preference value. On success the second field of the response line will +have the value. +.TP +.B get-global \fIKEY\fR +Get a global preference. +.TP +.B length \fITRACK\fR +Gets the length of the track in seconds. On success the second field of the +response line will have the value. +.TP +.B log +Sends event log messages in a response body. The command will only terminate (and +close the response body with a final dot) when a further command is readable on +the control connection. +.IP +See \fBEVENT LOG\fR below for more details. +.TP +.B move \fITRACK\fR \fIDELTA\fR +Move a track in the queue. The track may be identified by ID (preferred) or +name (which might cause confusion if it's there twice). \fIDELTA\fR should be +an negative or positive integer and indicates how many steps towards the head +of the queue the track should be moved. +.TP +.B moveafter \fITARGET\fR \fIID\fR ... +Move all the tracks in the \fIID\fR list after ID \fITARGET\fR. If +\fITARGET\fR is the empty string then the listed tracks are put at the head of +the queue. If \fITARGET\fR is listed in the ID list then the tracks are moved +to just after the first non-listed track before it, or to the head if there is +no such track. +.TP +.B part \fITRACK\fR \fICONTEXT\fI \fIPART\fR +Get a track name part. Returns an empty string if a name part cannot be +constructed. +.IP +.I CONTEXT +is one of +.B sort +or +.B display +and +.I PART +is usually one of +.BR artist , +.B album +or +.BR title . +.TP +.B pause +Pause the current track. +.TP +.B play \fITRACK\fR +Add a track to the queue. +.TP +.B playing +Reports what track is playing. +.IP +If the response is \fB252\fR then the rest of the response line consists of +track information (see below). +.IP +If the response is \fB259\fR then nothing is playing. +.TP +.B prefs \fBTRACK\fR +Sends 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 +name of the pref and the second the value. +.TP +.B queue +Sends back the current queue in a response body, one track to a line, the track +at the head of the queue (i.e. next to be be played) first. See below for the +track information syntax. +.TP +.B random-disable +Disable random play (but don't stop the current track). +.TP +.B random-enable +Enable random play. +.TP +.B random-enabled +Reports whether random play is enabled. The second field of the response line +will be \fByes\fR or \fBno\fR. +.TP +.B recent +Sends back the current recently-played list in a response body, one track to a +line, the track most recently played last. See below for the track +information syntax. +.TP +.B reconfigure +Request that DisOrder reconfigure itself. Only trusted users may issue this +command. +.TP +.B remove \fIID\fR +Remove the track identified by \fIID\fR. If \fBrestrict remove\fR is enabled +in the server's configuration then only the user that submitted the track may +remove it. +.TP +.B rescan +Rescan all roots for new or obsolete tracks. +.TP +.B resolve \fITRACK\fR +Resolve a track name, i.e. if this is an alias then return the real track name. +.TP +.B resume +Resume the current track after a \fBpause\fR command. +.TP +.B scratch \fR[\fIID\fR] +Remove the track identified by \fIID\fR, or the currently playing track if no +\fIID\fR is specified. If \fBrestrict scratch\fR is enabled in the server's +configuration then only the user that submitted the track may scratch it. +.TP +.B search \fITERMS\fR +Search for tracks matching the search terms. The results are put in a response +body, one to a line. +.IP +The search string is split in the usual way, with quoting supported, into a +list of terms. Only tracks matching all terms are included in the results. +.IP +Any terms of the form \fBtag:\fITAG\fR limits the search to tracks with that +tag. +.IP +All other terms are interpreted as individual words which must be present in +the track name. +.IP +Spaces in terms don't currently make sense, but may one day be interpreted to +allow searching for phrases. +.TP +.B \fBset\fR \fITRACK\fR \fIPREF\fR \fIVALUE\fR +Set a preference. +.TP +.B set-global \fIKEY\fR \fIVALUE\fR +Set a global preference. +.TP +.B stats +Send server statistics in plain text in a response body. +.TP +.B \fBtags\fR +Send the list of currently known tags in a response body. +.TP +.B \fBunset\fR \fITRACK\fR \fIPREF\fR +Unset a preference. +.TP +.B \fBunset-global\fR \fIKEY\fR +Unset a global preference. +.TP +.B user \fIUSER\fR \fIRESPONSE\fR +Authenticate as \fIUSER\fR. +.IP +When a connection is made the server sends a \fB221\fR response before any +command is received. As its first field this contains a challenge string +encoded in hex. +.IP +The \fIRESPONSE\fR consists of the SHA-1 hash of the user's password +concatenated with the challenge, encoded in hex. +.TP +.B version +Send back a response with the server version as the second field. +.TP +.B volume \fR[\fILEFT\fR [\fIRIGHT\fR]] +Get or set the volume. +.IP +With zero parameters just gets the volume and reports the left and right sides +as the 2nd and 3rd fields of the response. +.IP +With one parameter sets both sides to the same value. With two parameters sets +each side independently. +.SH RESPONSES +Responses are three-digit codes. The first digit distinguishes errors from +succesful responses: +.TP +.B 2 +Operation succeeded. +.TP +.B 5 +Operation failed. +.PP +The second digit breaks down the origin of the response: +.TP +.B 0 +Generic responses not specific to the handling of the command. Mostly this is +parse errors. +.TP +.B 3 +Authentication responses. +.TP +.B 5 +Responses specific to the handling of the command. +.PP +The third digit provides extra information about the response: +.TP +.B 0 +Text part is just commentary. +.TP +.B 1 +Text part is a constant result e.g. \fBversion\fR. +.TP +.B 2 +Text part is a potentially variable result. +.TP +.B 3 +Text part is just commentary; a dot-stuffed body follows. +.TP +.B 4 +Text part is just commentary; an indefinite dot-stuffed body follows. (Used +for \fBlog\fR.) +.TP +.B 4 +Text part is just commentary; an indefinite dot-stuffed body follows. (Used +for \fBlog\fR.) +.TP +.B 9 +The text part is just commentary (but would normally be a response for this +command) e.g. \fBplaying\fR. +.SH AUTHENTICATION +The server starts by issuing a challenge line, with response code 231. This +contains a random challenge encoded in hex. +.PP +The client should send back a \fBuser\fR command with username and a +hex-encoded response. The response is the SHA-1 hash of the user's password +and the challenge. +.SH "TRACK INFORMATION" +Track information is encoded in a line (i.e. using the usual line syntax) as +pairs of fields. The first is a name, the second a value. The names have the +following meanings: +.TP 12 +.B expected +The time the track is expected to be played at. +.TP +.B id +A string uniquely identifying this queue entry. +.TP +.B played +The time the track was played at. +.TP +.B scratched +The user that scratched the track. +.TP +.B state +The current track state. Valid states are: +.RS +.TP 12 +.B failed +The player failed (exited with nonzero status but wasn't scratched). +.TP +.B isscratch +The track is actually a scratch. +.TP +.B no_player +No player could be found for the track. +.TP +.B ok +The track was played without any problems. +.TP +.B scratched +The track was scratched. +.TP +.B started +The track is currently playing. +.TP +.B unplayed +In the queue, hasn't been played yet. +.TP +.B quitting +The track was terminated because the server is shutting down. +.RE +.TP +.B submitter +The user that submitted the track. +.TP +.B track +The filename of the track. +.TP +.B when +The time the track was added to the queue. +.TP +.B wstat +The wait status of the player in decimal. +.SH NOTES +Times are decimal integers using the server's \fBtime_t\fR. +.PP +For file listings, the regexp applies to the basename of the returned file, not +the whole filename, and letter case is ignored. \fBpcrepattern\fR(3) describes +the regexp syntax. +.PP +Filenames are in UTF-8 even if the collection they come from uses some other +encoding - if you want to access the real file (in such cases as the filenames +actually correspond to a real file) you'll have to convert to whatever the +right encoding is. +.SH "EVENT LOG" +The event log consists of lines starting with a hexadecimal timestamp and a +keyword followed by (optionally) parameters. The parameters are quoted in the +usual DisOrder way. Currently the following keywords are used: +.TP +.B completed \fITRACK\fR +Completed playing \fITRACK\fR +.TP +.B failed \fITRACK\fR \fIERROR\fR +Completed playing \fITRACK\fR with an error status +.TP +.B moved \fIUSER\fR +User \fIUSER\fR moved some track(s). Further details aren't included any +more. +.TP +.B playing \fITRACK\fR [\fIUSER\fR] +Started playing \fITRACK\fR. +.TP +.B queue \fIQUEUE-ENTRY\fR... +Added \fITRACK\fR to the queue. +.TP +.B recent_added \fIQUEUE-ENTRY\fR... +Added \fIID\fR to the recently played list. +.TP +.B recent_removed \fIID\fR +Removed \fIID\fR from the recently played list. +.TP +.B removed \fIID\fR [\fIUSER\fR] +Queue entry \fIID\fR was removed. This is used both for explicit removal (when +\fIUSER\fR is present) and when playing a track (when it is absent). +.TP +.B scratched \fITRACK\fR \fIUSER\fR +\fITRACK\fR was scratched by \fIUSER\fR. +.TP +.B state \fIKEYWORD\fR +Some state change occurred. The current set of keywords is: +.RS +.TP +.B disable_play +Playing was disabled. +.TP +.B disable_random +Random play was disabled. +.TP +.B enable_play +Playing was enabled. +.TP +.B enable_random +Random play was enabled. +.TP +.B pause +The current track was paused. +.TP +.B resume +The current track was resumed. +.RE +.TP +.B volume \fILEFT\fR \fIRIGHT\fR +The volume changed. +.PP +.IR QUEUE-ENTRY ... +is as defined in +.B "TRACK INFORMATION" +above. +.SH "SEE ALSO" +\fBdisorder\fR(1), +\fBtime\fR(2), +\fBdisorder\fR(3), +\fBpcrepattern\fR(3) +\fBdisorder_config\fR(5), +\fBdisorderd\fR(8), +\fButf8\fR(7) +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:7b6e9931e426d2b810422b20aef38601 diff --git a/doc/disorderd.8.in b/doc/disorderd.8.in new file mode 100644 index 0000000..4e68055 --- /dev/null +++ b/doc/disorderd.8.in @@ -0,0 +1,164 @@ +.\" +.\" Copyright (C) 2004, 2005 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorderd 8 +.SH NAME +disorderd \- DisOrder jukebox daemon +.SH SYNOPSIS +.B disorderd +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disorderd +is a daemon which plays audio files and services requests from users +concerning what is to be played. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +See +.BR disorder_config (5) +for further information. +.TP +.B --pidfile \fIPATH\fR, \fB-P \fIPATH +Write a pidfile. +.TP +.B --foreground, \fB-f +Run in the foreground. (By default, +.B disorderd +detaches from its terminal and runs in the background.) +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH NOTES +.SS "Environmental Dependencies" +It is important that +.B disorder-deadlock +and +.B disorder-rescan +are available on the PATH. The example "init" script attempts to +ensure this by appending sbindir to the path, but if you have +installed programs in unusual locations then this might not work. +.SS "How To Configure Authentication" +The administrator should create \fIpkgconfdir/config.private\fR, make sure it +is not world-readable, and populate it with \fBallow\fR commands +listing usernames and passwords. Use +e.g. \fBpwgen\fR(1) to generate random passwords. Passwords should +then be distributed to users. +.PP +Each user should create the file \fI~/.disorder/passwd\fR +and make sure it is not world-readable. Having done so +they should add a \fBpassword\fR command to it giving their password (and +optionally a \fBusername\fR command if their DisOrder username is not the +same as their login name). +.SS Locales +.B disorderd +is locale-aware. If you do not set the locale correctly then it may +not handle non-ASCII data properly. +.PP +Filenames and the configuration file are assumed to be encoded using the +current locale. The +communication with the client happens in UTF-8 (since the client and the server +don't know what locale each other might be using - in the future they might not +even be on the same host.) +.SS Backups +DisOrder uses Berkeley DB but currently discards log files that are no longer +in use. This means that DB's catastrophic recovery cannot be used (normal +recovery can be used, and indeed the server does this automatically on +startup). +.PP +It is suggested that instead you just back up the output of +.BR disorder-dump (8), +which saves only the parts of the database that cannot be regenerated +automatically, and thus has relatively modest storage requirements. +.SH SIGNALS +.TP 8 +.B SIGHUP +Re-read the configuration file. +.TP +.B SIGTERM +Terminate the daemon gracefully. +.TP +.B SIGINT +Terminate the daemon gracefully. +.PP +It may be more convenient to perform these operations from the client +\fBdisorder\fR(1). +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.TP +.I pkgconfdir/config.private +Private configuration (usernames and passwords). +.TP +.I ~/.disorder/passwd +Per-user password file. +.TP +.I pkgstatedir/queue +Saved copy of queue. Do not edit while the daemon is running. +.TP +.I pkgstatedir/recent +Saved copy of recently played track list. +Do not edit while the daemon is running. +.TP +.I pkgstatedir/prefs.db +Preferences database. +.TP +.I pkgstatedir/search.db +Search database. +.TP +.I pkgstatedir/tracks.db +Tracks database. +.TP +.I pkgstatedir/DB_CONFIG +Berkeley DB configuration file. This may be used to override database +settings without recompiling DisOrder. See the Berkeley DB +documention for further details. +.TP +.I pkgstatedir/log.* +Database log files. +.TP +.I pkgstatedir/socket +Communication socket for \fBdisorder\fR(1). +.TP +.I pkgstatedir/lock +Lockfile. This prevents multiple instances of DisOrder running +simultaneously. +.TP +.I sbindir/disorder-deadlock +Deadlock manager. +.TP +.I sbindir/disorder-rescan +Rescanner. +.SH ENVIRONMENT +.TP +.B LC_ALL\fR, \fBLANG\fR, etc +Current locale. See \fBlocale\fR(7). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorder-dump\fR(8) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:90dfddb7692b5c7f621c81bd7852ebde diff --git a/doc/disorderfm.1.in b/doc/disorderfm.1.in new file mode 100644 index 0000000..81b48f3 --- /dev/null +++ b/doc/disorderfm.1.in @@ -0,0 +1,100 @@ +.\" +.\" Copyright (C) 2006 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorderfm 1 +.SH NAME +disorderfm \- DisOrder file management utility +.SH SYNOPSIS +.B disorderfm +.RI [ OPTIONS ] +.I SOURCE +.I DESTINATION +.SH DESCRIPTION +.B disorderfm +recursively links or copies files from +.I SOURCE +to +.IR DESTINATION , +transforming filenames along the way. +.SH OPTIONS +.SS "Filename Format" +.TP +.B --from\fI ENCODING\fR, \fB-f\fI ENCODING +Specifies the filename encoding used below +.IR SOURCE . +.TP +.B --to\fI ENCODING\fR, \fB-t\fI ENCODING +Specifies the filename encoding used below +.IR DESTINATION . +.PP +If neither of \fB--from\fR or \fB--to\fR are specified then no encoding +translation is performed. If only one is specified then the other is set to +the current locale. +.TP +.B --windows-friendly\fR, \fB-w +Specifies that filenames below +.I DESTINATION +must be Windows-friendly. This is achieved by replacing special characters +with '_', prefixing device names with '_' and stripping trailing dots and +spaces. +.SS "File Selection" +.TP +.B --include\fI PATTERN\fR, \fB-i\fI PATTERN +Include filenames matching the glob pattern \fIPATTERN\fR. +.TP +.B --exclude\fI PATTERN\fR, \fB-e\fI PATTERN +Exclude filenames matching the glob pattern \fIPATTERN\fR. +.PP +These options may be used more than once. They will be checked in order and +the first that matches any given filename will determine whether that file is +included or excluded. +.PP +If none of the options match and \fB--include\fR was used at all then the file +is excluded. If none of the options match and \fB--include\fR was never used +then the file is included. +.SS "File Copying" +.TP +.B --link\fR, \fB-l +Files are hard-linked to their destination location. This is the default +action. +.TP +.B --symlink\fR, \fB-s +Symlinks are made in the destination location pointing back into the source +directory. +.TP +.B --copy\fR, \fB-c +Files are copied into their destination location. +.TP +.B --no-action\fR, \fB-n +The destination location is not modified in any way. Instead a report is +written to standard output saying what would be done. +.SS "Other" +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:HqblI8GXJamh2oeBBgb1Ug diff --git a/doc/tkdisorder.1 b/doc/tkdisorder.1 new file mode 100644 index 0000000..d5fb365 --- /dev/null +++ b/doc/tkdisorder.1 @@ -0,0 +1,60 @@ +.\" +.\" Copyright (C) 2005 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 +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH tkdisorder 1 +.SH NAME +tkdisorder \- DisOrder jukebox client +.SH SYNOPSIS +.B tkdisorder +.RI [ OPTIONS ] +.SH DESCRIPTION +.B tkdisorder +is a simple graphical client for DisOrder. It is not really finished. +.PP +The main window is divided into two. The top half contains the name +of the current track and a progress bar indicating how far through +playing it is. It also contains three buttons: +.TP +.B Quit +Terminates tkdisorder. +.TP +.B Scratch +Terminates the current track. +.TP +.B Recent +Pops up a window listing recently played tracks, most recent at the +top. +.PP +The bottom half of the window lists the current queue, with the next +track to be played at the top. +.SH OPTIONS +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5) +.PP +"\fBpydoc disorder\fR" for the Python API documentation. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:yguAsuJM8a5kzQ+UqEnSKg diff --git a/driver/Makefile.am b/driver/Makefile.am new file mode 100644 index 0000000..a43470d --- /dev/null +++ b/driver/Makefile.am @@ -0,0 +1,29 @@ +# +# This file is part of DisOrder +# Copyright (C) 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + + +aolib_LTLIBRARIES=libdisorder.la +aolibdir=${libdir}/ao/plugins-2 +AM_CPPFLAGS=-I${top_srcdir}/lib + +libdisorder_la_SOURCES=disorder.c +libdisorder_la_LDFLAGS=-module + +# arch-tag:tYZU3CmDyRkdyrcHgJMxcA diff --git a/driver/disorder.c b/driver/disorder.c new file mode 100644 index 0000000..8385ae8 --- /dev/null +++ b/driver/disorder.c @@ -0,0 +1,170 @@ + +/* + * This file is part of DisOrder. + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <poll.h> +#include <ao/ao.h> +#include <ao/plugin.h> + +/* extra declarations to help out lazy <ao/plugin.h> */ +int ao_plugin_test(void); +ao_info *ao_plugin_driver_info(void); +char *ao_plugin_file_extension(void); + +/* private data structure for this driver */ +struct internal { + int fd; /* output file descriptor */ + int exit_on_error; /* exit on write error */ +}; + +/* like write() but never returns EINTR/EAGAIN or short */ +static int do_write(int fd, const void *ptr, size_t n) { + size_t written = 0; + int ret; + struct pollfd ufd; + + memset(&ufd, 0, sizeof ufd); + ufd.fd = fd; + ufd.events = POLLOUT; + while(written < n) { + ret = write(fd, (const char *)ptr + written, n - written); + if(ret < 0) { + switch(errno) { + case EINTR: break; + case EAGAIN: + /* Someone sneakily gave us a nonblocking file descriptor, wait until + * we can write again */ + ret = poll(&ufd, 1, -1); + if(ret < 0 && errno != EINTR) return -1; + break; + default: + return -1; + } + } else + written += ret; + } + return written; +} + +/* return 1 if this driver can be opened */ +int ao_plugin_test(void) { + return 1; +} + +/* return info about this driver */ +ao_info *ao_plugin_driver_info(void) { + static const char *options[] = { "fd" }; + static const ao_info info = { + AO_TYPE_LIVE, /* type */ + (char *)"DisOrder format driver", /* name */ + (char *)"disorder", /* short_name */ + (char *)"http://www.greenend.org.uk/rjk/disorder/", /* comment */ + (char *)"Richard Kettlewell", /* author */ + AO_FMT_NATIVE, /* preferred_byte_format */ + 0, /* priority */ + (char **)options, /* options */ + 1, /* option_count */ + }; + return (ao_info *)&info; +} + +/* initialize the private data structure */ +int ao_plugin_device_init(ao_device *device) { + struct internal *i = malloc(sizeof (struct internal)); + const char *e; + + if(!i) return 0; + memset(i, 0, sizeof *i); + if((e = getenv("DISORDER_RAW_FD"))) + i->fd = atoi(e); + else + i->fd = 1; + device->internal = i; + return 1; +} + +/* set an option */ +int ao_plugin_set_option(ao_device *device, + const char *key, + const char *value) { + struct internal *i = device->internal; + + if(!strcmp(key, "fd")) + i->fd = atoi(value); + else if(!strcmp(key, "fragile")) + i->exit_on_error = atoi(value); + /* unknown options are required to be ignored */ + return 1; +} + +/* open the device */ +int ao_plugin_open(ao_device *device, ao_sample_format *format) { + struct internal *i = device->internal; + + /* we would like native-order samples */ + device->driver_byte_format = AO_FMT_NATIVE; + if(do_write(i->fd, format, sizeof *format) < 0) { + if(i->exit_on_error) exit(-1); + return 0; + } + return 1; +} + +/* play some samples */ +int ao_plugin_play(ao_device *device, const char *output_samples, + uint_32 num_bytes) { + struct internal *i = device->internal; + + if(do_write(i->fd, output_samples, num_bytes) < 0) { + if(i->exit_on_error) _exit(-1); + return 0; + } + return 1; +} + +/* close the device */ +int ao_plugin_close(ao_device attribute((unused)) *device) { + return 1; +} + +/* delete private data structures */ +void ao_plugin_device_clear(ao_device *device) { + free(device->internal); + device->internal = 0; +} + +/* report preferred filename extension */ +char *ao_plugin_file_extension(void) { + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:ru/Jqo0hseWMoSt/ba4Xlw */ diff --git a/examples/Makefile.am b/examples/Makefile.am new file mode 100644 index 0000000..74984f4 --- /dev/null +++ b/examples/Makefile.am @@ -0,0 +1,31 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +noinst_SCRIPTS=disorder.init +noinst_DATA=config.sample + +SEDFILES=disorder.init config.sample + +include ${top_srcdir}/scripts/sedfiles.make + +EXTRA_DIST=disorder.init.in config.sample.in disorder-log + +CLEANFILES=$(SEDFILES) +# arch-tag:c335ba7bb6f5a6077a563a1b79951e31 diff --git a/examples/config.sample.in b/examples/config.sample.in new file mode 100644 index 0000000..eb10fda --- /dev/null +++ b/examples/config.sample.in @@ -0,0 +1,59 @@ +# player programs +player *.mp3 execraw mpg321 -q -o disorder +player *.ogg execraw ogg123 -q -d disorder -o fragile:1 +player *.wav shell --wait-for-device play + +# use the fs module to list files under /export/mp3. The encoding +# is ISO-8859-1. +collection fs ISO-8859-1 /export/mp3 + +# don't leave a gap between tracks +gap 0 + +# scratch tracks +scratch pkgdatadir/slap.ogg +scratch pkgdatadir/scratch.mp3 + +# trust the web user and root +trust www-data root + +# run as user jukebox +user jukebox + +# volume control +mixer /dev/mixer +channel pcm + +# URL of the web interface +url http://jukebox.anjou.terraraq.org.uk/ + +# stopwords (i.e. ignored words) for the track search facility +stopword 01 02 03 04 05 06 07 08 09 10 +stopword 1 2 3 4 5 6 7 8 9 +stopword 11 12 13 14 15 16 17 18 19 20 +stopword 21 22 23 24 25 26 27 28 29 30 +stopword the a an and to too in on of we i am as im for is + +# namepart and transform are now filled in by default if you do not supply +# them. However if you supply any namepart directives then you will not +# get any defaults at all, so you must supply the full set. Similarly, +# if you supply any transform directives then you must supply the full set. + +# Parsing of track names for the currently playing track, the recently +# played list and the queue. +#namepart title "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#namepart title "/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort +#namepart album "/([^/]+)/[^/]+$" "$1" * +#namepart artist "/([^/]+)/[^/]+/[^/]+$" "$1" * +# used in alias construction +#namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +# Transformations of directory and filenames for the track choice screen +#transform track "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#transform track "^.*/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort + +#transform dir "^.*/([^/]+)$" "$1" * +#transform dir "^(the) ([^/]*)" "$2, $1" sort i +#transform dir "[[:punct:]]" "" sort g + +# arch-tag:57c841bad362239972e38de20d15e6c0 diff --git a/examples/disorder-log b/examples/disorder-log new file mode 100755 index 0000000..5a966a8 --- /dev/null +++ b/examples/disorder-log @@ -0,0 +1,70 @@ +#! /usr/bin/env python +# +# This file is part of DisOrder +# Copyright (C) 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +# Example use of disorder.monitor class + +import disorder + +class mymonitor(disorder.monitor): + def completed(self, track): + print "completed %s" % track + return True + + def failed(self, track, error): + print "failed %s (%s)" % (track, error) + return True + + def moved(self, id, offset, user): + print "%s moved by %s (%s)" % (id, offset, user) + return True + + def playing(self, track, user): + print "%s playing" % track + return True + + def queue(self, q): + print "queued %s" % str(q) + return True + + def recent_added(self, q): + print "recent_added %s" % str(q) + return True + + def recent_removed(self, id): + print "recent_removed %s" % id + return True + + def removed(self, id, user): + print "removed %s" % id + return True + + def scratched(self, track, user): + print "%s scratched %s" % (track, user) + return True + + def invalid(self, line): + print "invalid line: %s" % line + return True + +m = mymonitor() +m.run() + +# arch-tag:198M6M5uzq+f8AP1q+P1NA diff --git a/examples/disorder.init.in b/examples/disorder.init.in new file mode 100644 index 0000000..5348597 --- /dev/null +++ b/examples/disorder.init.in @@ -0,0 +1,68 @@ +#! /bin/sh +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +DAEMON=sbindir/disorderd +CLIENT=bindir/disorder + +PATH="$PATH:sbindir" + +start() { + if ${CLIENT} >/dev/null 2>&1; then + : already running + else + printf "Starting disorderd... " + ${DAEMON} + echo done + fi +} + +stop() { + if ${CLIENT} >/dev/null 2>&1; then + printf "Stopping disorderd... " + ${CLIENT} shutdown + echo done + else + : not running + fi +} + +reload() { + printf "Reconfiguring disorderd... " + ${CLIENT} reconfigure + echo done +} + +restart() { + stop + sleep 2 + start +} + +case "$1" in +start | stop | reload | restart ) "$1" ;; +force-reload ) reload ;; +* ) + echo "usage: $0 start|stop|restart|reload" 1>&2 + exit 1 +esac +# arch-tag:b75a857d0e9f723d2ae30687758ee1eb diff --git a/images/Makefile.am b/images/Makefile.am new file mode 100644 index 0000000..1af87d2 --- /dev/null +++ b/images/Makefile.am @@ -0,0 +1,30 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +static_DATA=cross.png down.png downdown.png edit.png nocross.png \ +nodown.png nodowndown.png noup.png noupup.png tick.png up.png upup.png \ +notes.png play.png pause.png random.png randomcross.png notescross.png + +staticdir=${pkgdatadir}/static + +EXTRA_DIST=$(static_DATA) + +CLEANFILES=$(SEDFILES) +# arch-tag:izAg/jA9nncHHWR5+ZuD+Q diff --git a/images/cross.png b/images/cross.png new file mode 100644 index 0000000..2e7a461 Binary files /dev/null and b/images/cross.png differ diff --git a/images/down.png b/images/down.png new file mode 100644 index 0000000..cb51f2d Binary files /dev/null and b/images/down.png differ diff --git a/images/downdown.png b/images/downdown.png new file mode 100644 index 0000000..73ddade Binary files /dev/null and b/images/downdown.png differ diff --git a/images/edit.png b/images/edit.png new file mode 100644 index 0000000..5a348f7 Binary files /dev/null and b/images/edit.png differ diff --git a/images/nocross.png b/images/nocross.png new file mode 100644 index 0000000..18aa9a8 Binary files /dev/null and b/images/nocross.png differ diff --git a/images/nodown.png b/images/nodown.png new file mode 100644 index 0000000..98f1367 Binary files /dev/null and b/images/nodown.png differ diff --git a/images/nodowndown.png b/images/nodowndown.png new file mode 100644 index 0000000..1986297 Binary files /dev/null and b/images/nodowndown.png differ diff --git a/images/notes.png b/images/notes.png new file mode 100644 index 0000000..07e3d04 Binary files /dev/null and b/images/notes.png differ diff --git a/images/notescross.png b/images/notescross.png new file mode 100644 index 0000000..ed107e4 Binary files /dev/null and b/images/notescross.png differ diff --git a/images/noup.png b/images/noup.png new file mode 100644 index 0000000..8e6398b Binary files /dev/null and b/images/noup.png differ diff --git a/images/noupup.png b/images/noupup.png new file mode 100644 index 0000000..29efac2 Binary files /dev/null and b/images/noupup.png differ diff --git a/images/pause.png b/images/pause.png new file mode 100644 index 0000000..f7f0094 Binary files /dev/null and b/images/pause.png differ diff --git a/images/play.png b/images/play.png new file mode 100644 index 0000000..bf16543 Binary files /dev/null and b/images/play.png differ diff --git a/images/random.png b/images/random.png new file mode 100644 index 0000000..ed2f038 Binary files /dev/null and b/images/random.png differ diff --git a/images/randomcross.png b/images/randomcross.png new file mode 100644 index 0000000..e3d515b Binary files /dev/null and b/images/randomcross.png differ diff --git a/images/tick.png b/images/tick.png new file mode 100644 index 0000000..c9378be Binary files /dev/null and b/images/tick.png differ diff --git a/images/up.png b/images/up.png new file mode 100644 index 0000000..52cbf04 Binary files /dev/null and b/images/up.png differ diff --git a/images/upup.png b/images/upup.png new file mode 100644 index 0000000..50611bb Binary files /dev/null and b/images/upup.png differ diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..91809f7 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,93 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +lib_LTLIBRARIES=libdisorder.la +include_HEADERS=disorder.h +noinst_PROGRAMS=test + +# This library is there to share code between the DisOrder applications, +# not to provide an interface to anyone else. As such there are no +# guarantees regarding its ABI. +libdisorder_la_SOURCES=charset.c charset.h \ + addr.c addr.h \ + authhash.c authhash.h \ + basen.c basen.h \ + cache.c cache.h \ + client.c client.h \ + client-common.c client-common.h \ + configuration.c configuration.h \ + defs.c defs.h \ + eclient.c eclient.h \ + event.c event.h \ + eventlog.c eventlog.h \ + filepart.c filepart.h \ + hash.c hash.h \ + hex.c hex.h \ + inputline.c inputline.h \ + kvp.c kvp.h \ + log.c log.h log-impl.h \ + logfd.c logfd.h \ + mem.c mem.h mem-impl.h \ + mime.h mime.c \ + mixer.c mixer.h \ + plugin.c plugin.h \ + printf.c printf.h \ + asprintf.c fprintf.c snprintf.c \ + queue.c queue.h \ + regsub.c regsub.h \ + selection.c selection.h \ + signame.c signame.h \ + sink.c sink.h \ + speaker.c speaker.h \ + split.c split.h \ + syscalls.c syscalls.h \ + types.h \ + table.c table.h \ + trackname.c trackname.h \ + user.h user.c \ + utf8.h utf8.c \ + vacopy.h \ + vector.c vector.h \ + words.c words.h casefold.h unicodegc.h \ + wstat.c wstat.h \ + disorder.h +libdisorder_la_LIBADD=$(LIBGCRYPT) $(LIBGC) $(LIBICONV) $(LIBNSL) \ + $(LIBSOCKET) $(LIBDL) $(LIBPCRE) +libdisorder_la_LDFLAGS=-release ${VERSION} + +definitions.h: Makefile + rm -f $@.new + echo "#define PKGLIBDIR \"${pkglibdir}\"" > $@.new + echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new + echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new + echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new + mv $@.new $@ +defs.o: definitions.h +defs.lo: definitions.h + +test_SOURCES=test.c +test_LDADD=libdisorder.la $(LIBPCRE) +test_DEPENDENCIES=libdisorder.la + +check: test + ./test + +CLEANFILES=definitions.h +# arch-tag:a8730cb4f8b4517b6e37b1f717956d7c diff --git a/lib/addr.c b/lib/addr.c new file mode 100644 index 0000000..07224c6 --- /dev/null +++ b/lib/addr.c @@ -0,0 +1,101 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netdb.h> + +#include "log.h" +#include "printf.h" +#include "configuration.h" +#include "addr.h" + +struct addrinfo *get_address(const struct stringlist *a, + const struct addrinfo *pref, + char **namep) { + struct addrinfo *res; + char *name; + int rc; + + if(a->n == 1) { + byte_xasprintf(&name, "host * service %s", a->s[0]); + if((rc = getaddrinfo(0, a->s[0], pref, &res))) { + error(0, "getaddrinfo %s: %s", a->s[0], gai_strerror(rc)); + return 0; + } + } else { + byte_xasprintf(&name, "host %s service %s", a->s[0], a->s[1]); + if((rc = getaddrinfo(a->s[0], a->s[1], pref, &res))) { + error(0, "getaddrinfo %s %s: %s", a->s[0], a->s[1], gai_strerror(rc)); + return 0; + } + } + if(!res || res->ai_socktype != SOCK_STREAM) { + error(0, "getaddrinfo didn't give us a stream socket"); + if(res) + freeaddrinfo(res); + return 0; + } + if(namep) + *namep = name; + return res; +} + +int addrinfocmp(const struct addrinfo *a, + const struct addrinfo *b) { + const struct sockaddr_in *ina, *inb; + const struct sockaddr_in6 *in6a, *in6b; + + if(a->ai_family != b->ai_family) return a->ai_family - b->ai_family; + if(a->ai_socktype != b->ai_socktype) return a->ai_socktype - b->ai_socktype; + if(a->ai_protocol != b->ai_protocol) return a->ai_protocol - b->ai_protocol; + switch(a->ai_protocol) { + case PF_INET: + ina = (const struct sockaddr_in *)a->ai_addr; + inb = (const struct sockaddr_in *)b->ai_addr; + if(ina->sin_port != inb->sin_port) return ina->sin_port - inb->sin_port; + return ina->sin_addr.s_addr - inb->sin_addr.s_addr; + break; + case PF_INET6: + in6a = (const struct sockaddr_in6 *)a->ai_addr; + in6b = (const struct sockaddr_in6 *)b->ai_addr; + if(in6a->sin6_port != in6b->sin6_port) + return in6a->sin6_port - in6b->sin6_port; + return memcmp(&in6a->sin6_addr, &in6b->sin6_addr, + sizeof (struct in6_addr)); + default: + error(0, "unsupported protocol family %d", a->ai_protocol); + return memcmp(a->ai_addr, b->ai_addr, a->ai_addrlen); /* kludge */ + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e143b5b1b677e108d957da2f6d09bccd */ diff --git a/lib/addr.h b/lib/addr.h new file mode 100644 index 0000000..c20a4c8 --- /dev/null +++ b/lib/addr.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef ADDR_H +#define ADDR_H + +struct addrinfo *get_address(const struct stringlist *a, + const struct addrinfo *pref, + char **namep); + +int addrinfocmp(const struct addrinfo *a, + const struct addrinfo *b); + +#endif /* ADDR_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3502f44d5fa2fd1825e8169df59fa864 */ diff --git a/lib/asprintf.c b/lib/asprintf.c new file mode 100644 index 0000000..3abe0db --- /dev/null +++ b/lib/asprintf.c @@ -0,0 +1,90 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> +#include <errno.h> + +#include "printf.h" +#include "sink.h" +#include "mem.h" +#include "vector.h" +#include "log.h" + +int byte_vasprintf(char **ptrp, + const char *fmt, + va_list ap) { + struct dynstr d; + int n; + + dynstr_init(&d); + if((n = byte_vsinkprintf(sink_dynstr(&d), fmt, ap)) >= 0) { + dynstr_terminate(&d); + *ptrp = d.vec; + } + return n; +} + +int byte_asprintf(char **ptrp, + const char *fmt, + ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vasprintf(ptrp, fmt, ap); + va_end(ap); + return n; +} + +int byte_xasprintf(char **ptrp, + const char *fmt, + ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_xvasprintf(ptrp, fmt, ap); + va_end(ap); + return n; +} + +int byte_xvasprintf(char **ptrp, + const char *fmt, + va_list ap) { + int n; + + if((n = byte_vasprintf(ptrp, fmt, ap)) < 0) + fatal(errno, "error calling byte_vasprintf"); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:39488c5fd6a6d176e613e7e747e55628 */ diff --git a/lib/authhash.c b/lib/authhash.c new file mode 100644 index 0000000..360d7ef --- /dev/null +++ b/lib/authhash.c @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <gcrypt.h> + +#include "hex.h" +#include "log.h" +#include "authhash.h" + +#ifndef AUTHHASH +# define AUTHHASH GCRY_MD_SHA1 +#endif + +const char *authhash(const void *challenge, size_t nchallenge, + const char *password) { + gcrypt_hash_handle h; + const char *res; + +#if HAVE_GCRY_ERROR_T + { + gcry_error_t e; + + if((e = gcry_md_open(&h, AUTHHASH, 0))) { + error(0, "gcry_md_open: %s", gcry_strerror(e)); + return 0; + } + } +#else + h = gcry_md_open(AUTHHASH, 0); +#endif + gcry_md_write(h, password, strlen(password)); + gcry_md_write(h, challenge, nchallenge); + res = hex(gcry_md_read(h, AUTHHASH), gcry_md_get_algo_dlen(AUTHHASH)); + gcry_md_close(h); + return res; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:fbc5c3876475fbeca3f7813dff16352d */ diff --git a/lib/authhash.h b/lib/authhash.h new file mode 100644 index 0000000..4ef57ae --- /dev/null +++ b/lib/authhash.h @@ -0,0 +1,35 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef AUTHHASH_H +#define AUTHHASH_H + +const char *authhash(const void *challenge, size_t nchallenge, + const char *user); + +#endif /* AUTHHASH_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:225ea2aea8c434a7a92fae4018e54c60 */ diff --git a/lib/basen.c b/lib/basen.c new file mode 100644 index 0000000..157919a --- /dev/null +++ b/lib/basen.c @@ -0,0 +1,81 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "basen.h" + +/* test whether v is 0 */ +static int zero(const unsigned long *v, int nwords) { + int n; + + for(n = 0; n < nwords && !v[n]; ++n) + ; + return n == nwords; +} + +/* divide v by m returning the remainder */ +static unsigned divide(unsigned long *v, int nwords, unsigned long m) { + unsigned long r = 0, a, b; + int n; + + /* we do the divide 16 bits at a time */ + for(n = 0; n < nwords; ++n) { + a = v[n] >> 16; + b = v[n] & 0xFFFF; + a += r << 16; + r = a % m; + a /= m; + b += r << 16; + r = b % m; + b /= m; + v[n] = (a << 16) + b; + } + return r; +} + +int basen(unsigned long *v, + int nwords, + char buffer[], + size_t bufsize, + unsigned base) { + size_t i = bufsize; + static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + do { + if(i <= 1) return -1; /* overflow */ + buffer[--i] = chars[divide(v, nwords, base)]; + } while(!zero(v, nwords)); + memmove(buffer, buffer + i, bufsize - i); + buffer[bufsize - i] = 0; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:iGDXjhkM2cdyv0RSlftgGQ */ diff --git a/lib/basen.h b/lib/basen.h new file mode 100644 index 0000000..aedd5cf --- /dev/null +++ b/lib/basen.h @@ -0,0 +1,42 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef BASEN_H +#define BASEN_H + +int basen(unsigned long *v, + int nwords, + char buffer[], + size_t bufsize, + unsigned base); +/* convert the big-endian value at @v@ composed of @nwords@ 32-bit words + * into a base-@base@ string and store in @buffer@. + * Returns 0 on success or -1 on overflow. + */ + +#endif /* BASEN_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:mJ+XdWbrDM6Ft/J6YXXb4A */ diff --git a/lib/cache.c b/lib/cache.c new file mode 100644 index 0000000..df8fa68 --- /dev/null +++ b/lib/cache.c @@ -0,0 +1,108 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <time.h> + +#include "hash.h" +#include "mem.h" +#include "log.h" +#include "cache.h" + +static hash *h; + +struct cache_entry { + const struct cache_type *type; + const void *value; + time_t birth; +}; + +static int expired(const struct cache_entry *c, time_t now) { + return now - c->birth > c->type->lifetime; +} + +void cache_put(const struct cache_type *type, + const char *key, const void *value) { + struct cache_entry *c; + + if(!h) + h = hash_new(sizeof (struct cache_entry)); + c = xmalloc(sizeof *c); + c->type = type; + c->value = value; + time(&c->birth); + hash_add(h, key, c, HASH_INSERT_OR_REPLACE); +} + +const void *cache_get(const struct cache_type *type, const char *key) { + const struct cache_entry *c; + + if(h + && (c = hash_find(h, key)) + && c->type == type + && !expired(c, time(0))) + return c->value; + else + return 0; +} + +static int expiry_callback(const char *key, void *value, void *u) { + const struct cache_entry *c = value; + const time_t *now = u; + + if(expired(c, *now)) + hash_remove(h, key); + return 0; +} + +void cache_expire(void) { + time_t now; + + if(h) { + time(&now); + hash_foreach(h, expiry_callback, &now); + } +} + +static int clean_callback(const char *key, void *value, void *u) { + const struct cache_entry *c = value; + const struct cache_type *type = u; + + if(!type || c->type == type) + hash_remove(h, key); + return 0; +} + +void cache_clean(const struct cache_type *type) { + if(h) + hash_foreach(h, clean_callback, (void *)type); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:uoFZfd12rkQj/ppG5g3BtQ */ diff --git a/lib/cache.h b/lib/cache.h new file mode 100644 index 0000000..f1aeccd --- /dev/null +++ b/lib/cache.h @@ -0,0 +1,54 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CACHE_H +#define CACHE_H + +/* Defines a cache mapping keys to typed data items */ + +struct cache_type { + int lifetime; /* Lifetime of a cache entry */ +}; + +void cache_put(const struct cache_type *type, + const char *key, const void *value); +/* Inserts KEY into the cache with value VALUE. If KEY is already + * present it is overwritten. */ + +const void *cache_get(const struct cache_type *type, const char *key); +/* Get a value from the cache. */ + +void cache_expire(void); +/* Expire values from the cache */ + +void cache_clean(const struct cache_type *type); +/* Clean all elements of a particular type, or all elements if TYPE=0 */ + +#endif /* CACHE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:8TQYM58jrfK4bi9YaWTutQ */ diff --git a/lib/casefold.h b/lib/casefold.h new file mode 100644 index 0000000..5b00c2b --- /dev/null +++ b/lib/casefold.h @@ -0,0 +1,915 @@ +struct cm { + uint32_t ch; + const char *tr; +} cm0[] = { + { 192, "\xC3\xA0" }, + { 256, "\xC4\x81" }, + { 512, "\xC8\x81" }, + { 1024, "\xD1\x90" }, + { 1152, "\xD2\x81" }, + { 1280, "\xD4\x81" }, + { 1344, "\xD5\xB0" }, + { 7680, "\xE1\xB8\x81" }, + { 7744, "\xE1\xB9\x81" }, + { 7808, "\xE1\xBA\x81" }, + { 7872, "\xE1\xBB\x81" }, + { 8064, "\xE1\xBC\x80\xCE\xB9" }, + { 9408, "\xE2\x93\x9A" }, + { 64256, "ff" }, + { 66560, "\xF0\x90\x90\xA8" }, +}, cm1[] = { + { 65, "a" }, + { 193, "\xC3\xA1" }, + { 321, "\xC5\x82" }, + { 385, "\xC9\x93" }, + { 1025, "\xD1\x91" }, + { 1217, "\xD3\x82" }, + { 1345, "\xD5\xB1" }, + { 8065, "\xE1\xBC\x81\xCE\xB9" }, + { 9409, "\xE2\x93\x9B" }, + { 64257, "fi" }, + { 66561, "\xF0\x90\x90\xA9" }, +}, cm2[] = { + { 66, "b" }, + { 194, "\xC3\xA2" }, + { 258, "\xC4\x83" }, + { 386, "\xC6\x83" }, + { 514, "\xC8\x83" }, + { 962, "\xCF\x83" }, + { 1026, "\xD1\x92" }, + { 1282, "\xD4\x83" }, + { 1346, "\xD5\xB2" }, + { 7682, "\xE1\xB8\x83" }, + { 7746, "\xE1\xB9\x83" }, + { 7810, "\xE1\xBA\x83" }, + { 7874, "\xE1\xBB\x83" }, + { 8066, "\xE1\xBC\x82\xCE\xB9" }, + { 8130, "\xE1\xBD\xB4\xCE\xB9" }, + { 9410, "\xE2\x93\x9C" }, + { 64258, "fl" }, + { 66562, "\xF0\x90\x90\xAA" }, +}, cm3[] = { + { 67, "c" }, + { 195, "\xC3\xA3" }, + { 323, "\xC5\x84" }, + { 1027, "\xD1\x93" }, + { 1219, "\xD3\x84" }, + { 1347, "\xD5\xB3" }, + { 8067, "\xE1\xBC\x83\xCE\xB9" }, + { 8131, "\xCE\xB7\xCE\xB9" }, + { 9411, "\xE2\x93\x9D" }, + { 64259, "ffi" }, + { 66563, "\xF0\x90\x90\xAB" }, +}, cm4[] = { + { 68, "d" }, + { 196, "\xC3\xA4" }, + { 260, "\xC4\x85" }, + { 388, "\xC6\x85" }, + { 452, "\xC7\x86" }, + { 516, "\xC8\x85" }, + { 1028, "\xD1\x94" }, + { 1284, "\xD4\x85" }, + { 1348, "\xD5\xB4" }, + { 7684, "\xE1\xB8\x85" }, + { 7748, "\xE1\xB9\x85" }, + { 7812, "\xE1\xBA\x85" }, + { 7876, "\xE1\xBB\x85" }, + { 8068, "\xE1\xBC\x84\xCE\xB9" }, + { 8132, "\xCE\xAE\xCE\xB9" }, + { 9412, "\xE2\x93\x9E" }, + { 64260, "ffl" }, + { 66564, "\xF0\x90\x90\xAC" }, +}, cm5[] = { + { 69, "e" }, + { 197, "\xC3\xA5" }, + { 325, "\xC5\x86" }, + { 453, "\xC7\x86" }, + { 837, "\xCE\xB9" }, + { 1029, "\xD1\x95" }, + { 1221, "\xD3\x86" }, + { 1349, "\xD5\xB5" }, + { 8069, "\xE1\xBC\x85\xCE\xB9" }, + { 9413, "\xE2\x93\x9F" }, + { 64261, "st" }, + { 66565, "\xF0\x90\x90\xAD" }, +}, cm6[] = { + { 70, "f" }, + { 198, "\xC3\xA6" }, + { 262, "\xC4\x87" }, + { 390, "\xC9\x94" }, + { 518, "\xC8\x87" }, + { 902, "\xCE\xAC" }, + { 1030, "\xD1\x96" }, + { 1286, "\xD4\x87" }, + { 1350, "\xD5\xB6" }, + { 7686, "\xE1\xB8\x87" }, + { 7750, "\xE1\xB9\x87" }, + { 7814, "\xE1\xBA\x87" }, + { 7878, "\xE1\xBB\x87" }, + { 8070, "\xE1\xBC\x86\xCE\xB9" }, + { 8134, "\xCE\xB7\xCD\x82" }, + { 9414, "\xE2\x93\xA0" }, + { 64262, "st" }, + { 66566, "\xF0\x90\x90\xAE" }, +}, cm7[] = { + { 71, "g" }, + { 199, "\xC3\xA7" }, + { 327, "\xC5\x88" }, + { 391, "\xC6\x88" }, + { 455, "\xC7\x89" }, + { 1031, "\xD1\x97" }, + { 1223, "\xD3\x88" }, + { 1351, "\xD5\xB7" }, + { 1415, "\xD5\xA5\xD6\x82" }, + { 8071, "\xE1\xBC\x87\xCE\xB9" }, + { 8135, "\xCE\xB7\xCD\x82\xCE\xB9" }, + { 9415, "\xE2\x93\xA1" }, + { 66567, "\xF0\x90\x90\xAF" }, +}, cm8[] = { + { 72, "h" }, + { 200, "\xC3\xA8" }, + { 264, "\xC4\x89" }, + { 456, "\xC7\x89" }, + { 520, "\xC8\x89" }, + { 904, "\xCE\xAD" }, + { 1032, "\xD1\x98" }, + { 1288, "\xD4\x89" }, + { 1352, "\xD5\xB8" }, + { 7688, "\xE1\xB8\x89" }, + { 7752, "\xE1\xB9\x89" }, + { 7816, "\xE1\xBA\x89" }, + { 7880, "\xE1\xBB\x89" }, + { 7944, "\xE1\xBC\x80" }, + { 8008, "\xE1\xBD\x80" }, + { 8072, "\xE1\xBC\x80\xCE\xB9" }, + { 8136, "\xE1\xBD\xB2" }, + { 9416, "\xE2\x93\xA2" }, + { 66568, "\xF0\x90\x90\xB0" }, +}, cm9[] = { + { 73, "i" }, + { 201, "\xC3\xA9" }, + { 329, "\xCA\xBCn" }, + { 393, "\xC9\x96" }, + { 905, "\xCE\xAE" }, + { 1033, "\xD1\x99" }, + { 1225, "\xD3\x8A" }, + { 1353, "\xD5\xB9" }, + { 7945, "\xE1\xBC\x81" }, + { 8009, "\xE1\xBD\x81" }, + { 8073, "\xE1\xBC\x81\xCE\xB9" }, + { 8137, "\xE1\xBD\xB3" }, + { 9417, "\xE2\x93\xA3" }, + { 66569, "\xF0\x90\x90\xB1" }, +}, cm10[] = { + { 74, "j" }, + { 202, "\xC3\xAA" }, + { 266, "\xC4\x8B" }, + { 330, "\xC5\x8B" }, + { 394, "\xC9\x97" }, + { 458, "\xC7\x8C" }, + { 522, "\xC8\x8B" }, + { 906, "\xCE\xAF" }, + { 1034, "\xD1\x9A" }, + { 1162, "\xD2\x8B" }, + { 1290, "\xD4\x8B" }, + { 1354, "\xD5\xBA" }, + { 7690, "\xE1\xB8\x8B" }, + { 7754, "\xE1\xB9\x8B" }, + { 7818, "\xE1\xBA\x8B" }, + { 7882, "\xE1\xBB\x8B" }, + { 7946, "\xE1\xBC\x82" }, + { 8010, "\xE1\xBD\x82" }, + { 8074, "\xE1\xBC\x82\xCE\xB9" }, + { 8138, "\xE1\xBD\xB4" }, + { 9418, "\xE2\x93\xA4" }, + { 66570, "\xF0\x90\x90\xB2" }, +}, cm11[] = { + { 75, "k" }, + { 203, "\xC3\xAB" }, + { 395, "\xC6\x8C" }, + { 459, "\xC7\x8C" }, + { 1035, "\xD1\x9B" }, + { 1227, "\xD3\x8C" }, + { 1355, "\xD5\xBB" }, + { 7947, "\xE1\xBC\x83" }, + { 8011, "\xE1\xBD\x83" }, + { 8075, "\xE1\xBC\x83\xCE\xB9" }, + { 8139, "\xE1\xBD\xB5" }, + { 9419, "\xE2\x93\xA5" }, + { 66571, "\xF0\x90\x90\xB3" }, +}, cm12[] = { + { 76, "l" }, + { 204, "\xC3\xAC" }, + { 268, "\xC4\x8D" }, + { 332, "\xC5\x8D" }, + { 524, "\xC8\x8D" }, + { 908, "\xCF\x8C" }, + { 1036, "\xD1\x9C" }, + { 1164, "\xD2\x8D" }, + { 1292, "\xD4\x8D" }, + { 1356, "\xD5\xBC" }, + { 7692, "\xE1\xB8\x8D" }, + { 7756, "\xE1\xB9\x8D" }, + { 7820, "\xE1\xBA\x8D" }, + { 7884, "\xE1\xBB\x8D" }, + { 7948, "\xE1\xBC\x84" }, + { 8012, "\xE1\xBD\x84" }, + { 8076, "\xE1\xBC\x84\xCE\xB9" }, + { 8140, "\xCE\xB7\xCE\xB9" }, + { 9420, "\xE2\x93\xA6" }, + { 66572, "\xF0\x90\x90\xB4" }, +}, cm13[] = { + { 77, "m" }, + { 205, "\xC3\xAD" }, + { 461, "\xC7\x8E" }, + { 1037, "\xD1\x9D" }, + { 1229, "\xD3\x8E" }, + { 1357, "\xD5\xBD" }, + { 7949, "\xE1\xBC\x85" }, + { 8013, "\xE1\xBD\x85" }, + { 8077, "\xE1\xBC\x85\xCE\xB9" }, + { 9421, "\xE2\x93\xA7" }, + { 66573, "\xF0\x90\x90\xB5" }, +}, cm14[] = { + { 78, "n" }, + { 206, "\xC3\xAE" }, + { 270, "\xC4\x8F" }, + { 334, "\xC5\x8F" }, + { 398, "\xC7\x9D" }, + { 526, "\xC8\x8F" }, + { 910, "\xCF\x8D" }, + { 1038, "\xD1\x9E" }, + { 1166, "\xD2\x8F" }, + { 1294, "\xD4\x8F" }, + { 1358, "\xD5\xBE" }, + { 7694, "\xE1\xB8\x8F" }, + { 7758, "\xE1\xB9\x8F" }, + { 7822, "\xE1\xBA\x8F" }, + { 7886, "\xE1\xBB\x8F" }, + { 7950, "\xE1\xBC\x86" }, + { 8078, "\xE1\xBC\x86\xCE\xB9" }, + { 9422, "\xE2\x93\xA8" }, + { 66574, "\xF0\x90\x90\xB6" }, +}, cm15[] = { + { 79, "o" }, + { 207, "\xC3\xAF" }, + { 399, "\xC9\x99" }, + { 463, "\xC7\x90" }, + { 911, "\xCF\x8E" }, + { 1039, "\xD1\x9F" }, + { 1359, "\xD5\xBF" }, + { 7951, "\xE1\xBC\x87" }, + { 8079, "\xE1\xBC\x87\xCE\xB9" }, + { 9423, "\xE2\x93\xA9" }, + { 66575, "\xF0\x90\x90\xB7" }, +}, cm16[] = { + { 80, "p" }, + { 208, "\xC3\xB0" }, + { 272, "\xC4\x91" }, + { 336, "\xC5\x91" }, + { 400, "\xC9\x9B" }, + { 528, "\xC8\x91" }, + { 912, "\xCE\xB9\xCC\x88\xCC\x81" }, + { 976, "\xCE\xB2" }, + { 1040, "\xD0\xB0" }, + { 1168, "\xD2\x91" }, + { 1232, "\xD3\x91" }, + { 1360, "\xD6\x80" }, + { 7696, "\xE1\xB8\x91" }, + { 7760, "\xE1\xB9\x91" }, + { 7824, "\xE1\xBA\x91" }, + { 7888, "\xE1\xBB\x91" }, + { 8016, "\xCF\x85\xCC\x93" }, + { 8080, "\xE1\xBC\xA0\xCE\xB9" }, + { 66576, "\xF0\x90\x90\xB8" }, +}, cm17[] = { + { 81, "q" }, + { 209, "\xC3\xB1" }, + { 401, "\xC6\x92" }, + { 465, "\xC7\x92" }, + { 913, "\xCE\xB1" }, + { 977, "\xCE\xB8" }, + { 1041, "\xD0\xB1" }, + { 1361, "\xD6\x81" }, + { 8081, "\xE1\xBC\xA1\xCE\xB9" }, + { 66577, "\xF0\x90\x90\xB9" }, +}, cm18[] = { + { 82, "r" }, + { 210, "\xC3\xB2" }, + { 274, "\xC4\x93" }, + { 338, "\xC5\x93" }, + { 530, "\xC8\x93" }, + { 914, "\xCE\xB2" }, + { 1042, "\xD0\xB2" }, + { 1170, "\xD2\x93" }, + { 1234, "\xD3\x93" }, + { 1362, "\xD6\x82" }, + { 7698, "\xE1\xB8\x93" }, + { 7762, "\xE1\xB9\x93" }, + { 7826, "\xE1\xBA\x93" }, + { 7890, "\xE1\xBB\x93" }, + { 8018, "\xCF\x85\xCC\x93\xCC\x80" }, + { 8082, "\xE1\xBC\xA2\xCE\xB9" }, + { 8146, "\xCE\xB9\xCC\x88\xCC\x80" }, + { 66578, "\xF0\x90\x90\xBA" }, +}, cm19[] = { + { 83, "s" }, + { 211, "\xC3\xB3" }, + { 403, "\xC9\xA0" }, + { 467, "\xC7\x94" }, + { 915, "\xCE\xB3" }, + { 1043, "\xD0\xB3" }, + { 1363, "\xD6\x83" }, + { 8083, "\xE1\xBC\xA3\xCE\xB9" }, + { 8147, "\xCE\xB9\xCC\x88\xCC\x81" }, + { 64275, "\xD5\xB4\xD5\xB6" }, + { 66579, "\xF0\x90\x90\xBB" }, +}, cm20[] = { + { 84, "t" }, + { 212, "\xC3\xB4" }, + { 276, "\xC4\x95" }, + { 340, "\xC5\x95" }, + { 404, "\xC9\xA3" }, + { 532, "\xC8\x95" }, + { 916, "\xCE\xB4" }, + { 1044, "\xD0\xB4" }, + { 1172, "\xD2\x95" }, + { 1236, "\xD3\x95" }, + { 1364, "\xD6\x84" }, + { 7700, "\xE1\xB8\x95" }, + { 7764, "\xE1\xB9\x95" }, + { 7828, "\xE1\xBA\x95" }, + { 7892, "\xE1\xBB\x95" }, + { 8020, "\xCF\x85\xCC\x93\xCC\x81" }, + { 8084, "\xE1\xBC\xA4\xCE\xB9" }, + { 64276, "\xD5\xB4\xD5\xA5" }, + { 66580, "\xF0\x90\x90\xBC" }, +}, cm21[] = { + { 85, "u" }, + { 213, "\xC3\xB5" }, + { 469, "\xC7\x96" }, + { 917, "\xCE\xB5" }, + { 981, "\xCF\x86" }, + { 1045, "\xD0\xB5" }, + { 1365, "\xD6\x85" }, + { 8085, "\xE1\xBC\xA5\xCE\xB9" }, + { 64277, "\xD5\xB4\xD5\xAB" }, + { 66581, "\xF0\x90\x90\xBD" }, +}, cm22[] = { + { 86, "v" }, + { 214, "\xC3\xB6" }, + { 278, "\xC4\x97" }, + { 342, "\xC5\x97" }, + { 406, "\xC9\xA9" }, + { 534, "\xC8\x97" }, + { 918, "\xCE\xB6" }, + { 982, "\xCF\x80" }, + { 1046, "\xD0\xB6" }, + { 1174, "\xD2\x97" }, + { 1238, "\xD3\x97" }, + { 1366, "\xD6\x86" }, + { 7702, "\xE1\xB8\x97" }, + { 7766, "\xE1\xB9\x97" }, + { 7830, "h\xCC\xB1" }, + { 7894, "\xE1\xBB\x97" }, + { 8022, "\xCF\x85\xCC\x93\xCD\x82" }, + { 8086, "\xE1\xBC\xA6\xCE\xB9" }, + { 8150, "\xCE\xB9\xCD\x82" }, + { 64278, "\xD5\xBE\xD5\xB6" }, + { 66582, "\xF0\x90\x90\xBE" }, +}, cm23[] = { + { 87, "w" }, + { 407, "\xC9\xA8" }, + { 471, "\xC7\x98" }, + { 919, "\xCE\xB7" }, + { 1047, "\xD0\xB7" }, + { 7831, "t\xCC\x88" }, + { 8087, "\xE1\xBC\xA7\xCE\xB9" }, + { 8151, "\xCE\xB9\xCC\x88\xCD\x82" }, + { 64279, "\xD5\xB4\xD5\xAD" }, + { 66583, "\xF0\x90\x90\xBF" }, +}, cm24[] = { + { 88, "x" }, + { 216, "\xC3\xB8" }, + { 280, "\xC4\x99" }, + { 344, "\xC5\x99" }, + { 408, "\xC6\x99" }, + { 536, "\xC8\x99" }, + { 920, "\xCE\xB8" }, + { 984, "\xCF\x99" }, + { 1048, "\xD0\xB8" }, + { 1176, "\xD2\x99" }, + { 1240, "\xD3\x99" }, + { 7704, "\xE1\xB8\x99" }, + { 7768, "\xE1\xB9\x99" }, + { 7832, "w\xCC\x8A" }, + { 7896, "\xE1\xBB\x99" }, + { 7960, "\xE1\xBC\x90" }, + { 8088, "\xE1\xBC\xA0\xCE\xB9" }, + { 8152, "\xE1\xBF\x90" }, + { 66584, "\xF0\x90\x91\x80" }, +}, cm25[] = { + { 89, "y" }, + { 217, "\xC3\xB9" }, + { 473, "\xC7\x9A" }, + { 921, "\xCE\xB9" }, + { 1049, "\xD0\xB9" }, + { 7833, "y\xCC\x8A" }, + { 7961, "\xE1\xBC\x91" }, + { 8025, "\xE1\xBD\x91" }, + { 8089, "\xE1\xBC\xA1\xCE\xB9" }, + { 8153, "\xE1\xBF\x91" }, + { 66585, "\xF0\x90\x91\x81" }, +}, cm26[] = { + { 90, "z" }, + { 218, "\xC3\xBA" }, + { 282, "\xC4\x9B" }, + { 346, "\xC5\x9B" }, + { 538, "\xC8\x9B" }, + { 922, "\xCE\xBA" }, + { 986, "\xCF\x9B" }, + { 1050, "\xD0\xBA" }, + { 1178, "\xD2\x9B" }, + { 1242, "\xD3\x9B" }, + { 7706, "\xE1\xB8\x9B" }, + { 7770, "\xE1\xB9\x9B" }, + { 7834, "a\xCA\xBE" }, + { 7898, "\xE1\xBB\x9B" }, + { 7962, "\xE1\xBC\x92" }, + { 8090, "\xE1\xBC\xA2\xCE\xB9" }, + { 8154, "\xE1\xBD\xB6" }, + { 66586, "\xF0\x90\x91\x82" }, +}, cm27[] = { + { 219, "\xC3\xBB" }, + { 475, "\xC7\x9C" }, + { 923, "\xCE\xBB" }, + { 1051, "\xD0\xBB" }, + { 7835, "\xE1\xB9\xA1" }, + { 7963, "\xE1\xBC\x93" }, + { 8027, "\xE1\xBD\x93" }, + { 8091, "\xE1\xBC\xA3\xCE\xB9" }, + { 8155, "\xE1\xBD\xB7" }, + { 66587, "\xF0\x90\x91\x83" }, +}, cm28[] = { + { 220, "\xC3\xBC" }, + { 284, "\xC4\x9D" }, + { 348, "\xC5\x9D" }, + { 412, "\xC9\xAF" }, + { 540, "\xC8\x9D" }, + { 924, "\xCE\xBC" }, + { 988, "\xCF\x9D" }, + { 1052, "\xD0\xBC" }, + { 1180, "\xD2\x9D" }, + { 1244, "\xD3\x9D" }, + { 7708, "\xE1\xB8\x9D" }, + { 7772, "\xE1\xB9\x9D" }, + { 7900, "\xE1\xBB\x9D" }, + { 7964, "\xE1\xBC\x94" }, + { 8092, "\xE1\xBC\xA4\xCE\xB9" }, + { 66588, "\xF0\x90\x91\x84" }, +}, cm29[] = { + { 221, "\xC3\xBD" }, + { 413, "\xC9\xB2" }, + { 925, "\xCE\xBD" }, + { 1053, "\xD0\xBD" }, + { 7965, "\xE1\xBC\x95" }, + { 8029, "\xE1\xBD\x95" }, + { 8093, "\xE1\xBC\xA5\xCE\xB9" }, + { 66589, "\xF0\x90\x91\x85" }, +}, cm30[] = { + { 222, "\xC3\xBE" }, + { 286, "\xC4\x9F" }, + { 350, "\xC5\x9F" }, + { 478, "\xC7\x9F" }, + { 542, "\xC8\x9F" }, + { 926, "\xCE\xBE" }, + { 990, "\xCF\x9F" }, + { 1054, "\xD0\xBE" }, + { 1182, "\xD2\x9F" }, + { 1246, "\xD3\x9F" }, + { 7710, "\xE1\xB8\x9F" }, + { 7774, "\xE1\xB9\x9F" }, + { 7902, "\xE1\xBB\x9F" }, + { 8094, "\xE1\xBC\xA6\xCE\xB9" }, + { 66590, "\xF0\x90\x91\x86" }, +}, cm31[] = { + { 223, "ss" }, + { 415, "\xC9\xB5" }, + { 927, "\xCE\xBF" }, + { 1055, "\xD0\xBF" }, + { 8031, "\xE1\xBD\x97" }, + { 8095, "\xE1\xBC\xA7\xCE\xB9" }, + { 66591, "\xF0\x90\x91\x87" }, +}, cm32[] = { + { 288, "\xC4\xA1" }, + { 352, "\xC5\xA1" }, + { 416, "\xC6\xA1" }, + { 480, "\xC7\xA1" }, + { 544, "\xC6\x9E" }, + { 928, "\xCF\x80" }, + { 992, "\xCF\xA1" }, + { 1056, "\xD1\x80" }, + { 1120, "\xD1\xA1" }, + { 1184, "\xD2\xA1" }, + { 1248, "\xD3\xA1" }, + { 7712, "\xE1\xB8\xA1" }, + { 7776, "\xE1\xB9\xA1" }, + { 7840, "\xE1\xBA\xA1" }, + { 7904, "\xE1\xBB\xA1" }, + { 8096, "\xE1\xBD\xA0\xCE\xB9" }, + { 8544, "\xE2\x85\xB0" }, + { 66592, "\xF0\x90\x91\x88" }, +}, cm33[] = { + { 929, "\xCF\x81" }, + { 1057, "\xD1\x81" }, + { 8097, "\xE1\xBD\xA1\xCE\xB9" }, + { 8545, "\xE2\x85\xB1" }, + { 65313, "\xEF\xBD\x81" }, + { 66593, "\xF0\x90\x91\x89" }, +}, cm34[] = { + { 290, "\xC4\xA3" }, + { 354, "\xC5\xA3" }, + { 418, "\xC6\xA3" }, + { 482, "\xC7\xA3" }, + { 546, "\xC8\xA3" }, + { 994, "\xCF\xA3" }, + { 1058, "\xD1\x82" }, + { 1122, "\xD1\xA3" }, + { 1186, "\xD2\xA3" }, + { 1250, "\xD3\xA3" }, + { 7714, "\xE1\xB8\xA3" }, + { 7778, "\xE1\xB9\xA3" }, + { 7842, "\xE1\xBA\xA3" }, + { 7906, "\xE1\xBB\xA3" }, + { 8098, "\xE1\xBD\xA2\xCE\xB9" }, + { 8162, "\xCF\x85\xCC\x88\xCC\x80" }, + { 8546, "\xE2\x85\xB2" }, + { 65314, "\xEF\xBD\x82" }, + { 66594, "\xF0\x90\x91\x8A" }, +}, cm35[] = { + { 931, "\xCF\x83" }, + { 1059, "\xD1\x83" }, + { 8099, "\xE1\xBD\xA3\xCE\xB9" }, + { 8163, "\xCF\x85\xCC\x88\xCC\x81" }, + { 8547, "\xE2\x85\xB3" }, + { 65315, "\xEF\xBD\x83" }, + { 66595, "\xF0\x90\x91\x8B" }, +}, cm36[] = { + { 292, "\xC4\xA5" }, + { 356, "\xC5\xA5" }, + { 420, "\xC6\xA5" }, + { 484, "\xC7\xA5" }, + { 548, "\xC8\xA5" }, + { 932, "\xCF\x84" }, + { 996, "\xCF\xA5" }, + { 1060, "\xD1\x84" }, + { 1124, "\xD1\xA5" }, + { 1188, "\xD2\xA5" }, + { 1252, "\xD3\xA5" }, + { 7716, "\xE1\xB8\xA5" }, + { 7780, "\xE1\xB9\xA5" }, + { 7844, "\xE1\xBA\xA5" }, + { 7908, "\xE1\xBB\xA5" }, + { 8100, "\xE1\xBD\xA4\xCE\xB9" }, + { 8164, "\xCF\x81\xCC\x93" }, + { 8548, "\xE2\x85\xB4" }, + { 65316, "\xEF\xBD\x84" }, + { 66596, "\xF0\x90\x91\x8C" }, +}, cm37[] = { + { 933, "\xCF\x85" }, + { 1061, "\xD1\x85" }, + { 8101, "\xE1\xBD\xA5\xCE\xB9" }, + { 8549, "\xE2\x85\xB5" }, + { 65317, "\xEF\xBD\x85" }, + { 66597, "\xF0\x90\x91\x8D" }, +}, cm38[] = { + { 294, "\xC4\xA7" }, + { 358, "\xC5\xA7" }, + { 422, "\xCA\x80" }, + { 486, "\xC7\xA7" }, + { 550, "\xC8\xA7" }, + { 934, "\xCF\x86" }, + { 998, "\xCF\xA7" }, + { 1062, "\xD1\x86" }, + { 1126, "\xD1\xA7" }, + { 1190, "\xD2\xA7" }, + { 1254, "\xD3\xA7" }, + { 7718, "\xE1\xB8\xA7" }, + { 7782, "\xE1\xB9\xA7" }, + { 7846, "\xE1\xBA\xA7" }, + { 7910, "\xE1\xBB\xA7" }, + { 8102, "\xE1\xBD\xA6\xCE\xB9" }, + { 8166, "\xCF\x85\xCD\x82" }, + { 8486, "\xCF\x89" }, + { 8550, "\xE2\x85\xB6" }, + { 65318, "\xEF\xBD\x86" }, + { 66598, "\xF0\x90\x91\x8E" }, +}, cm39[] = { + { 423, "\xC6\xA8" }, + { 935, "\xCF\x87" }, + { 1063, "\xD1\x87" }, + { 8103, "\xE1\xBD\xA7\xCE\xB9" }, + { 8167, "\xCF\x85\xCC\x88\xCD\x82" }, + { 8551, "\xE2\x85\xB7" }, + { 65319, "\xEF\xBD\x87" }, + { 66599, "\xF0\x90\x91\x8F" }, +}, cm40[] = { + { 296, "\xC4\xA9" }, + { 360, "\xC5\xA9" }, + { 488, "\xC7\xA9" }, + { 552, "\xC8\xA9" }, + { 936, "\xCF\x88" }, + { 1000, "\xCF\xA9" }, + { 1064, "\xD1\x88" }, + { 1128, "\xD1\xA9" }, + { 1192, "\xD2\xA9" }, + { 1256, "\xD3\xA9" }, + { 7720, "\xE1\xB8\xA9" }, + { 7784, "\xE1\xB9\xA9" }, + { 7848, "\xE1\xBA\xA9" }, + { 7912, "\xE1\xBB\xA9" }, + { 7976, "\xE1\xBC\xA0" }, + { 8040, "\xE1\xBD\xA0" }, + { 8104, "\xE1\xBD\xA0\xCE\xB9" }, + { 8168, "\xE1\xBF\xA0" }, + { 8552, "\xE2\x85\xB8" }, + { 65320, "\xEF\xBD\x88" }, +}, cm41[] = { + { 425, "\xCA\x83" }, + { 937, "\xCF\x89" }, + { 1065, "\xD1\x89" }, + { 7977, "\xE1\xBC\xA1" }, + { 8041, "\xE1\xBD\xA1" }, + { 8105, "\xE1\xBD\xA1\xCE\xB9" }, + { 8169, "\xE1\xBF\xA1" }, + { 8553, "\xE2\x85\xB9" }, + { 65321, "\xEF\xBD\x89" }, +}, cm42[] = { + { 298, "\xC4\xAB" }, + { 362, "\xC5\xAB" }, + { 490, "\xC7\xAB" }, + { 554, "\xC8\xAB" }, + { 938, "\xCF\x8A" }, + { 1002, "\xCF\xAB" }, + { 1066, "\xD1\x8A" }, + { 1130, "\xD1\xAB" }, + { 1194, "\xD2\xAB" }, + { 1258, "\xD3\xAB" }, + { 7722, "\xE1\xB8\xAB" }, + { 7786, "\xE1\xB9\xAB" }, + { 7850, "\xE1\xBA\xAB" }, + { 7914, "\xE1\xBB\xAB" }, + { 7978, "\xE1\xBC\xA2" }, + { 8042, "\xE1\xBD\xA2" }, + { 8106, "\xE1\xBD\xA2\xCE\xB9" }, + { 8170, "\xE1\xBD\xBA" }, + { 8490, "k" }, + { 8554, "\xE2\x85\xBA" }, + { 65322, "\xEF\xBD\x8A" }, +}, cm43[] = { + { 939, "\xCF\x8B" }, + { 1067, "\xD1\x8B" }, + { 7979, "\xE1\xBC\xA3" }, + { 8043, "\xE1\xBD\xA3" }, + { 8107, "\xE1\xBD\xA3\xCE\xB9" }, + { 8171, "\xE1\xBD\xBB" }, + { 8491, "\xC3\xA5" }, + { 8555, "\xE2\x85\xBB" }, + { 65323, "\xEF\xBD\x8B" }, +}, cm44[] = { + { 300, "\xC4\xAD" }, + { 364, "\xC5\xAD" }, + { 428, "\xC6\xAD" }, + { 492, "\xC7\xAD" }, + { 556, "\xC8\xAD" }, + { 1004, "\xCF\xAD" }, + { 1068, "\xD1\x8C" }, + { 1132, "\xD1\xAD" }, + { 1196, "\xD2\xAD" }, + { 1260, "\xD3\xAD" }, + { 7724, "\xE1\xB8\xAD" }, + { 7788, "\xE1\xB9\xAD" }, + { 7852, "\xE1\xBA\xAD" }, + { 7916, "\xE1\xBB\xAD" }, + { 7980, "\xE1\xBC\xA4" }, + { 8044, "\xE1\xBD\xA4" }, + { 8108, "\xE1\xBD\xA4\xCE\xB9" }, + { 8172, "\xE1\xBF\xA5" }, + { 8556, "\xE2\x85\xBC" }, + { 65324, "\xEF\xBD\x8C" }, +}, cm45[] = { + { 1069, "\xD1\x8D" }, + { 7981, "\xE1\xBC\xA5" }, + { 8045, "\xE1\xBD\xA5" }, + { 8109, "\xE1\xBD\xA5\xCE\xB9" }, + { 8557, "\xE2\x85\xBD" }, + { 65325, "\xEF\xBD\x8D" }, +}, cm46[] = { + { 302, "\xC4\xAF" }, + { 366, "\xC5\xAF" }, + { 430, "\xCA\x88" }, + { 494, "\xC7\xAF" }, + { 558, "\xC8\xAF" }, + { 1006, "\xCF\xAF" }, + { 1070, "\xD1\x8E" }, + { 1134, "\xD1\xAF" }, + { 1198, "\xD2\xAF" }, + { 1262, "\xD3\xAF" }, + { 7726, "\xE1\xB8\xAF" }, + { 7790, "\xE1\xB9\xAF" }, + { 7854, "\xE1\xBA\xAF" }, + { 7918, "\xE1\xBB\xAF" }, + { 7982, "\xE1\xBC\xA6" }, + { 8046, "\xE1\xBD\xA6" }, + { 8110, "\xE1\xBD\xA6\xCE\xB9" }, + { 8558, "\xE2\x85\xBE" }, + { 65326, "\xEF\xBD\x8E" }, +}, cm47[] = { + { 431, "\xC6\xB0" }, + { 1071, "\xD1\x8F" }, + { 7983, "\xE1\xBC\xA7" }, + { 8047, "\xE1\xBD\xA7" }, + { 8111, "\xE1\xBD\xA7\xCE\xB9" }, + { 8559, "\xE2\x85\xBF" }, + { 65327, "\xEF\xBD\x8F" }, +}, cm48[] = { + { 304, "i\xCC\x87" }, + { 368, "\xC5\xB1" }, + { 496, "j\xCC\x8C" }, + { 560, "\xC8\xB1" }, + { 944, "\xCF\x85\xCC\x88\xCC\x81" }, + { 1008, "\xCE\xBA" }, + { 1136, "\xD1\xB1" }, + { 1200, "\xD2\xB1" }, + { 1264, "\xD3\xB1" }, + { 7728, "\xE1\xB8\xB1" }, + { 7792, "\xE1\xB9\xB1" }, + { 7856, "\xE1\xBA\xB1" }, + { 7920, "\xE1\xBB\xB1" }, + { 65328, "\xEF\xBD\x90" }, +}, cm49[] = { + { 433, "\xCA\x8A" }, + { 497, "\xC7\xB3" }, + { 1009, "\xCF\x81" }, + { 1329, "\xD5\xA1" }, + { 65329, "\xEF\xBD\x91" }, +}, cm50[] = { + { 306, "\xC4\xB3" }, + { 370, "\xC5\xB3" }, + { 434, "\xCA\x8B" }, + { 498, "\xC7\xB3" }, + { 562, "\xC8\xB3" }, + { 1138, "\xD1\xB3" }, + { 1202, "\xD2\xB3" }, + { 1266, "\xD3\xB3" }, + { 1330, "\xD5\xA2" }, + { 7730, "\xE1\xB8\xB3" }, + { 7794, "\xE1\xB9\xB3" }, + { 7858, "\xE1\xBA\xB3" }, + { 7922, "\xE1\xBB\xB3" }, + { 8114, "\xE1\xBD\xB0\xCE\xB9" }, + { 8178, "\xE1\xBD\xBC\xCE\xB9" }, + { 65330, "\xEF\xBD\x92" }, +}, cm51[] = { + { 435, "\xC6\xB4" }, + { 1331, "\xD5\xA3" }, + { 8115, "\xCE\xB1\xCE\xB9" }, + { 8179, "\xCF\x89\xCE\xB9" }, + { 65331, "\xEF\xBD\x93" }, +}, cm52[] = { + { 308, "\xC4\xB5" }, + { 372, "\xC5\xB5" }, + { 500, "\xC7\xB5" }, + { 1012, "\xCE\xB8" }, + { 1140, "\xD1\xB5" }, + { 1204, "\xD2\xB5" }, + { 1268, "\xD3\xB5" }, + { 1332, "\xD5\xA4" }, + { 7732, "\xE1\xB8\xB5" }, + { 7796, "\xE1\xB9\xB5" }, + { 7860, "\xE1\xBA\xB5" }, + { 7924, "\xE1\xBB\xB5" }, + { 8116, "\xCE\xAC\xCE\xB9" }, + { 8180, "\xCF\x8E\xCE\xB9" }, + { 65332, "\xEF\xBD\x94" }, +}, cm53[] = { + { 181, "\xCE\xBC" }, + { 437, "\xC6\xB6" }, + { 1013, "\xCE\xB5" }, + { 1333, "\xD5\xA5" }, + { 65333, "\xEF\xBD\x95" }, +}, cm54[] = { + { 310, "\xC4\xB7" }, + { 374, "\xC5\xB7" }, + { 502, "\xC6\x95" }, + { 1142, "\xD1\xB7" }, + { 1206, "\xD2\xB7" }, + { 1334, "\xD5\xA6" }, + { 7734, "\xE1\xB8\xB7" }, + { 7798, "\xE1\xB9\xB7" }, + { 7862, "\xE1\xBA\xB7" }, + { 7926, "\xE1\xBB\xB7" }, + { 8118, "\xCE\xB1\xCD\x82" }, + { 8182, "\xCF\x89\xCD\x82" }, + { 9398, "\xE2\x93\x90" }, + { 65334, "\xEF\xBD\x96" }, +}, cm55[] = { + { 439, "\xCA\x92" }, + { 503, "\xC6\xBF" }, + { 1015, "\xCF\xB8" }, + { 1335, "\xD5\xA7" }, + { 8119, "\xCE\xB1\xCD\x82\xCE\xB9" }, + { 8183, "\xCF\x89\xCD\x82\xCE\xB9" }, + { 9399, "\xE2\x93\x91" }, + { 65335, "\xEF\xBD\x97" }, +}, cm56[] = { + { 376, "\xC3\xBF" }, + { 440, "\xC6\xB9" }, + { 504, "\xC7\xB9" }, + { 1144, "\xD1\xB9" }, + { 1208, "\xD2\xB9" }, + { 1272, "\xD3\xB9" }, + { 1336, "\xD5\xA8" }, + { 7736, "\xE1\xB8\xB9" }, + { 7800, "\xE1\xB9\xB9" }, + { 7864, "\xE1\xBA\xB9" }, + { 7928, "\xE1\xBB\xB9" }, + { 7992, "\xE1\xBC\xB0" }, + { 8120, "\xE1\xBE\xB0" }, + { 8184, "\xE1\xBD\xB8" }, + { 9400, "\xE2\x93\x92" }, + { 65336, "\xEF\xBD\x98" }, +}, cm57[] = { + { 313, "\xC4\xBA" }, + { 377, "\xC5\xBA" }, + { 1017, "\xCF\xB2" }, + { 1337, "\xD5\xA9" }, + { 7993, "\xE1\xBC\xB1" }, + { 8121, "\xE1\xBE\xB1" }, + { 8185, "\xE1\xBD\xB9" }, + { 9401, "\xE2\x93\x93" }, + { 65337, "\xEF\xBD\x99" }, +}, cm58[] = { + { 506, "\xC7\xBB" }, + { 1018, "\xCF\xBB" }, + { 1146, "\xD1\xBB" }, + { 1210, "\xD2\xBB" }, + { 1338, "\xD5\xAA" }, + { 7738, "\xE1\xB8\xBB" }, + { 7802, "\xE1\xB9\xBB" }, + { 7866, "\xE1\xBA\xBB" }, + { 7994, "\xE1\xBC\xB2" }, + { 8122, "\xE1\xBD\xB0" }, + { 8186, "\xE1\xBD\xBC" }, + { 9402, "\xE2\x93\x94" }, + { 65338, "\xEF\xBD\x9A" }, +}, cm59[] = { + { 315, "\xC4\xBC" }, + { 379, "\xC5\xBC" }, + { 1339, "\xD5\xAB" }, + { 7995, "\xE1\xBC\xB3" }, + { 8123, "\xE1\xBD\xB1" }, + { 8187, "\xE1\xBD\xBD" }, + { 9403, "\xE2\x93\x95" }, +}, cm60[] = { + { 444, "\xC6\xBD" }, + { 508, "\xC7\xBD" }, + { 1148, "\xD1\xBD" }, + { 1212, "\xD2\xBD" }, + { 1340, "\xD5\xAC" }, + { 7740, "\xE1\xB8\xBD" }, + { 7804, "\xE1\xB9\xBD" }, + { 7868, "\xE1\xBA\xBD" }, + { 7996, "\xE1\xBC\xB4" }, + { 8124, "\xCE\xB1\xCE\xB9" }, + { 8188, "\xCF\x89\xCE\xB9" }, + { 9404, "\xE2\x93\x96" }, +}, cm61[] = { + { 317, "\xC4\xBE" }, + { 381, "\xC5\xBE" }, + { 1341, "\xD5\xAD" }, + { 7997, "\xE1\xBC\xB5" }, + { 9405, "\xE2\x93\x97" }, +}, cm62[] = { + { 510, "\xC7\xBF" }, + { 1150, "\xD1\xBF" }, + { 1214, "\xD2\xBF" }, + { 1342, "\xD5\xAE" }, + { 7742, "\xE1\xB8\xBF" }, + { 7806, "\xE1\xB9\xBF" }, + { 7870, "\xE1\xBA\xBF" }, + { 7998, "\xE1\xBC\xB6" }, + { 8126, "\xCE\xB9" }, + { 9406, "\xE2\x93\x98" }, +}, cm63[] = { + { 319, "\xC5\x80" }, + { 383, "s" }, + { 1343, "\xD5\xAF" }, + { 7999, "\xE1\xBC\xB7" }, + { 9407, "\xE2\x93\x99" }, +}; + +static const struct cm *const cm[] = { cm0, cm1, cm2, cm3, cm4, cm5, cm6, cm7, cm8, cm9, cm10, cm11, cm12, cm13, cm14, cm15, cm16, cm17, cm18, cm19, cm20, cm21, cm22, cm23, cm24, cm25, cm26, cm27, cm28, cm29, cm30, cm31, cm32, cm33, cm34, cm35, cm36, cm37, cm38, cm39, cm40, cm41, cm42, cm43, cm44, cm45, cm46, cm47, cm48, cm49, cm50, cm51, cm52, cm53, cm54, cm55, cm56, cm57, cm58, cm59, cm60, cm61, cm62, cm63 }; +static const size_t cmn[] = { 15, 11, 18, 11, 18, 12, 18, 13, 19, 14, 22, 13, 20, 11, 19, 11, 19, 10, 18, 11, 19, 10, 21, 10, 19, 11, 18, 10, 16, 8, 15, 7, 18, 6, 19, 7, 20, 6, 21, 8, 20, 9, 21, 9, 20, 6, 19, 7, 14, 5, 16, 5, 15, 5, 14, 8, 16, 9, 13, 7, 12, 5, 10, 5 }; +#define CM_MASK 63 +/* arch-tag:2dc53cdcaba8a55c2982ad113f4ebae2 */ diff --git a/lib/charset.c b/lib/charset.c new file mode 100644 index 0000000..e8f577a --- /dev/null +++ b/lib/charset.c @@ -0,0 +1,146 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <iconv.h> +#include <string.h> +#include <errno.h> +#include <langinfo.h> + +#include "mem.h" +#include "log.h" +#include "charset.h" +#include "configuration.h" +#include "utf8.h" +#include "vector.h" + +static void *convert(const char *from, const char *to, + const void *ptr, size_t n) { + iconv_t i; + size_t len; + char *buf = 0, *s, *d; + size_t bufsize = 0, sl, dl; + + if((i = iconv_open(to, from)) == (iconv_t)-1) + fatal(errno, "error calling iconv_open"); + do { + bufsize = bufsize ? 2 * bufsize : 32; + buf = xrealloc_noptr(buf, bufsize); + iconv(i, 0, 0, 0, 0); + s = (char *)ptr; + sl = n; + d = buf; + dl = bufsize; + /* (void *) to work around FreeBSD's nonstandard iconv prototype */ + len = iconv(i, (void *)&s, &sl, &d, &dl); + } while(len == (size_t)-1 && errno == E2BIG); + iconv_close(i); + if(len == (size_t)-1) { + error(errno, "error converting from %s to %s", from, to); + return 0; + } + return buf; +} + +/* not everybody's iconv supports UCS-4, and it's inconvenient to have to know + * our endianness, and it's easy to convert it ourselves, so we do */ +uint32_t *utf82ucs4(const char *mb) { + struct dynstr_ucs4 d; + uint32_t c; + + dynstr_ucs4_init(&d); + while(*mb) { + PARSE_UTF8(mb, c, + error(0, "invalid UTF-8 sequence"); return 0;); + dynstr_ucs4_append(&d, c); + } + dynstr_ucs4_terminate(&d); + return d.vec; +} + +char *ucs42utf8(const uint32_t *u) { + struct dynstr d; + uint32_t c; + + dynstr_init(&d); + while((c = *u++)) { + if(c < 0x80) + dynstr_append(&d, c); + else if(c < 0x800) { + dynstr_append(&d, 0xC0 | (c >> 6)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else if(c < 0x10000) { + dynstr_append(&d, 0xE0 | (c >> 12)); + dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else if(c < 0x110000) { + dynstr_append(&d, 0xF0 | (c >> 18)); + dynstr_append(&d, 0x80 | ((c >> 12) & 0x3F)); + dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else { + error(0, "invalid UCS-4 character"); + return 0; + } + } + dynstr_terminate(&d); + return d.vec; +} + +char *mb2utf8(const char *mb) { + return convert(nl_langinfo(CODESET), "UTF-8", mb, strlen(mb) + 1); +} + +char *utf82mb(const char *utf8) { + return convert("UTF-8", nl_langinfo(CODESET), utf8, strlen(utf8) + 1); +} + +char *any2utf8(const char *from, const char *any) { + return convert(from, "UTF-8", any, strlen(any) + 1); +} + +char *any2mb(const char *from, const char *any) { + if(from) return convert(from, nl_langinfo(CODESET), any, strlen(any) + 1); + else return xstrdup(any); +} + +char *any2any(const char *from, + const char *to, + const char *any) { + if(from || to) return convert(from, to, any, strlen(any) + 1); + else return xstrdup(any); +} + +int ucs4cmp(const uint32_t *a, const uint32_t *b) { + while(*a && *b && *a == *b) ++a, ++b; + if(*a > *b) return 1; + else if(*a < *b) return -1; + else return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:30ec6c45260bef9d03ef04d194bf9e9e */ diff --git a/lib/charset.h b/lib/charset.h new file mode 100644 index 0000000..3dcd756 --- /dev/null +++ b/lib/charset.h @@ -0,0 +1,68 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef CHARSET_H +#define CHARSET_H + +/* Character encoding conversion routines */ + +uint32_t *utf82ucs4(const char *mb); +char *ucs42utf8(const uint32_t *u); +char *mb2utf8(const char *mb); +char *utf82mb(const char *utf8); +/* various conversions, between multibyte strings (mb) in + * whatever the current encoding is, and UTF-8 strings (utf8). On + * error, a null pointer is returned and @errno@ set. */ + +char *any2utf8(const char *from/*encoding*/, + const char *any/*string*/); +/* arbitrary conversions from any null-free byte-based encoding that + * iconv knows about to UTF-8 */ + +char *any2mb(const char *from/*encoding or 0*/, + const char *any/*string*/); +/* Arbitrary conversions from any null-free byte-based encoding that + * iconv knows about to a multibyte string. If FROM is 0 then ANY is + * returned unchanged. */ + +char *any2any(const char *from/*encoding or 0*/, + const char *to/*encoding to 0*/, + const char *any/*string*/); +/* Arbitrary conversions between any null-free byte-based encodings + * that iconv knows. If FROM and TO are both 0 then ANY is returned + * unchanged. */ + + +static inline char *nullcheck(char *s) { + if(!s) exitfn(1); /* assume an error already reported */ + return s; +} + +int ucs4cmp(const uint32_t *a, const uint32_t *b); +/* like strcmp */ + +#endif /* CHARSET_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:ca7783e592109d7b7078175bd301faf7 */ diff --git a/lib/client-common.c b/lib/client-common.c new file mode 100644 index 0000000..1e8fbac --- /dev/null +++ b/lib/client-common.c @@ -0,0 +1,86 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <errno.h> +#include <netdb.h> + +#include "log.h" +#include "configuration.h" +#include "client-common.h" +#include "addr.h" + +int with_sockaddr(void *c, + int (*function)(void *c, + const struct sockaddr *sa, + socklen_t len, + const char *ident)) { + const char *path; + struct sockaddr_un su; + struct addrinfo *res; + char *name; + int n; + + static const struct addrinfo pref = { + 0, + PF_INET, + SOCK_STREAM, + IPPROTO_TCP, + 0, + 0, + 0, + 0 + }; + + if(config->connect.n) { + res = get_address(&config->connect, &pref, &name); + if(!res) return -1; + n = function(c, res->ai_addr, res->ai_addrlen, name); + freeaddrinfo(res); + return n; + } else { + path = config_get_file("socket"); + if(strlen(path) >= sizeof su.sun_path) { + error(errno, "socket path is too long"); + return -1; + } + memset(&su, 0, sizeof su); + su.sun_family = AF_UNIX; + strcpy(su.sun_path, path); + return function(c, (struct sockaddr *)&su, sizeof su, path); + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:NhE5Xyy+Tzv6rhKtZ1jNlg */ diff --git a/lib/client-common.h b/lib/client-common.h new file mode 100644 index 0000000..0387eb8 --- /dev/null +++ b/lib/client-common.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CLIENT_COMMON_H +#define CLIENT_COMMON_H + +int with_sockaddr(void *c, + int (*function)(void *c, + const struct sockaddr *sa, + socklen_t len, + const char *ident)); + +#endif /* CLIENT_COMMON_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:S5yQBwyuy9z8e9CFf74pgA */ diff --git a/lib/client.c b/lib/client.c new file mode 100644 index 0000000..ddf88bb --- /dev/null +++ b/lib/client.c @@ -0,0 +1,613 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <netdb.h> +#include <stdlib.h> + +#include "log.h" +#include "mem.h" +#include "configuration.h" +#include "queue.h" +#include "client.h" +#include "charset.h" +#include "hex.h" +#include "split.h" +#include "vector.h" +#include "inputline.h" +#include "kvp.h" +#include "syscalls.h" +#include "printf.h" +#include "sink.h" +#include "addr.h" +#include "authhash.h" +#include "client-common.h" + +struct disorder_client { + FILE *fpin, *fpout; + char *ident; + char *user; + int verbose; +}; + +disorder_client *disorder_new(int verbose) { + disorder_client *c = xmalloc(sizeof (struct disorder_client)); + + c->verbose = verbose; + return c; +} + +/* read a response line. + * If @rp@ is not a null pointer, returns the whole response through it. + * Return value is the response code, or -1 on error. */ +static int response(disorder_client *c, char **rp) { + char *r; + + if(inputline(c->ident, c->fpin, &r, '\n')) + return -1; + D(("response: %s", r)); + if(rp) + *rp = r; + if(r[0] >= '0' && r[0] <= '9' + && r[1] >= '0' && r[1] <= '9' + && r[2] >= '0' && r[2] <= '9' + && r[3] == ' ') + return (r[0] * 10 + r[1]) * 10 + r[2] - 111 * '0'; + else { + error(0, "invalid reply format from %s", c->ident); + return -1; + } +} + +/* Read a response. + * If @rp@ is not a null pointer then the response text (excluding + * the status code) is returned through it, UNLESS the response code + * is xx9. + * Return value is 0 for 2xx responses and -1 otherwise. + */ +static int check_response(disorder_client *c, char **rp) { + int rc; + char *r; + + if((rc = response(c, &r)) == -1) + return -1; + else if(rc / 100 == 2) { + if(rp) + *rp = (rc % 10 == 9) ? 0 : xstrdup(r + 4); + return 0; + } else { + if(c->verbose) + error(0, "from %s: %s", c->ident, utf82mb(r)); + return -1; + } +} + +static int disorder_simple_v(disorder_client *c, + char **rp, + const char *cmd, va_list ap) { + const char *arg; + struct dynstr d; + + if(cmd) { + dynstr_init(&d); + dynstr_append_string(&d, cmd); + while((arg = va_arg(ap, const char *))) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(arg)); + } + dynstr_append(&d, '\n'); + dynstr_terminate(&d); + D(("command: %s", d.vec)); + if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) { + error(errno, "error writing to %s", c->ident); + return -1; + } + } + return check_response(c, rp); +} + +/* Execute a simple command with any number of arguments. + * @rp@ and return value as for check_response(). + */ +static int disorder_simple(disorder_client *c, + char **rp, + const char *cmd, ...) { + va_list ap; + int ret; + + va_start(ap, cmd); + ret = disorder_simple_v(c, rp, cmd, ap); + va_end(ap); + return ret; +} + +static int connect_sock(void *vc, + const struct sockaddr *sa, + socklen_t len, + const char *ident) { + const char *username, *password; + disorder_client *c = vc; + int n; + + if(!(username = config->username)) { + error(0, "no username configured"); + return -1; + } + if(!(password = config->password)) { + for(n = 0; (n < config->allow.n + && strcmp(config->allow.s[n].s[0], username)); ++n) + ; + if(n < config->allow.n) + password = config->allow.s[n].s[1]; + else { + error(0, "no password configured"); + return -1; + } + } + return disorder_connect_sock(c, sa, len, username, password, ident); +} + +int disorder_connect(disorder_client *c) { + return with_sockaddr(c, connect_sock); +} + +static int check_running(void attribute((unused)) *c, + const struct sockaddr *sa, + socklen_t len, + const char attribute((unused)) *ident) { + int fd, ret; + + if((fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) + fatal(errno, "error calling socket"); + if(connect(fd, sa, len) < 0) { + if(errno == ECONNREFUSED || errno == ENOENT) + ret = 0; + else + fatal(errno, "error calling connect"); + } else + ret = 1; + xclose(fd); + return ret; +} + +int disorder_running(disorder_client *c) { + return with_sockaddr(c, check_running); +} + +int disorder_connect_sock(disorder_client *c, + const struct sockaddr *sa, + socklen_t len, + const char *username, + const char *password, + const char *ident) { + int fd = -1, fd2 = -1; + unsigned char *nonce; + size_t nl; + const char *res; + char *r; + + if(!password) { + error(0, "no password found"); + return -1; + } + c->fpin = c->fpout = 0; + if((fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { + error(errno, "error calling socket"); + return -1; + } + if(connect(fd, sa, len) < 0) { + error(errno, "error calling connect"); + goto error; + } + if((fd2 = dup(fd)) < 0) { + error(errno, "error calling dup"); + goto error; + } + if(!(c->fpin = fdopen(fd, "rb"))) { + error(errno, "error calling fdopen"); + goto error; + } + fd = -1; + if(!(c->fpout = fdopen(fd2, "wb"))) { + error(errno, "error calling fdopen"); + goto error; + } + fd2 = -1; + c->ident = xstrdup(ident); + if(disorder_simple(c, &r, 0, (const char *)0)) + return -1; + if(!(nonce = unhex(r, &nl))) + return -1; + if(!(res = authhash(nonce, nl, password))) goto error; + if(disorder_simple(c, 0, "user", username, res, (char *)0)) + return -1; + c->user = xstrdup(username); + return 0; +error: + if(c->fpin) fclose(c->fpin); + if(c->fpout) fclose(c->fpout); + if(fd2 != -1) close(fd2); + if(fd != -1) close(fd); + return -1; +} + +int disorder_close(disorder_client *c) { + int ret = 0; + + if(c->fpin) { + if(fclose(c->fpin) < 0) { + error(errno, "error calling fclose"); + ret = -1; + } + c->fpin = 0; + } + if(c->fpout) { + if(fclose(c->fpout) < 0) { + error(errno, "error calling fclose"); + ret = -1; + } + c->fpout = 0; + } + return 0; +} + +int disorder_become(disorder_client *c, const char *user) { + if(disorder_simple(c, 0, "become", user, (char *)0)) return -1; + c->user = xstrdup(user); + return 0; +} + +int disorder_play(disorder_client *c, const char *track) { + return disorder_simple(c, 0, "play", track, (char *)0); +} + +int disorder_remove(disorder_client *c, const char *track) { + return disorder_simple(c, 0, "remove", track, (char *)0); +} + +int disorder_move(disorder_client *c, const char *track, int delta) { + char d[16]; + + byte_snprintf(d, sizeof d, "%d", delta); + return disorder_simple(c, 0, "move", track, d, (char *)0); +} + +int disorder_enable(disorder_client *c) { + return disorder_simple(c, 0, "enable", (char *)0); +} + +int disorder_disable(disorder_client *c) { + return disorder_simple(c, 0, "disable", (char *)0); +} + +int disorder_scratch(disorder_client *c, const char *id) { + return disorder_simple(c, 0, "scratch", id, (char *)0); +} + +int disorder_shutdown(disorder_client *c) { + return disorder_simple(c, 0, "shutdown", (char *)0); +} + +int disorder_reconfigure(disorder_client *c) { + return disorder_simple(c, 0, "reconfigure", (char *)0); +} + +int disorder_rescan(disorder_client *c) { + return disorder_simple(c, 0, "rescan", (char *)0); +} + +int disorder_version(disorder_client *c, char **rp) { + return disorder_simple(c, rp, "version", (char *)0); +} + +static void client_error(const char *msg, + void attribute((unused)) *u) { + error(0, "error parsing reply: %s", msg); +} + +int disorder_playing(disorder_client *c, struct queue_entry **qp) { + char *r; + struct queue_entry *q; + + if(disorder_simple(c, &r, "playing", (char *)0)) + return -1; + if(r) { + q = xmalloc(sizeof *q); + if(queue_unmarshall(q, r, client_error, 0)) + return -1; + *qp = q; + } else + *qp = 0; + return 0; +} + +static int disorder_somequeue(disorder_client *c, + const char *cmd, struct queue_entry **qp) { + struct queue_entry *qh, **qt = &qh, *q; + char *l; + + if(disorder_simple(c, 0, cmd, (char *)0)) + return -1; + while(inputline(c->ident, c->fpin, &l, '\n') >= 0) { + if(!strcmp(l, ".")) { + *qt = 0; + *qp = qh; + return 0; + } + q = xmalloc(sizeof *q); + if(!queue_unmarshall(q, l, client_error, 0)) { + *qt = q; + qt = &q->next; + } + } + if(ferror(c->fpin)) + error(errno, "error reading %s", c->ident); + else + error(0, "error reading %s: unexpected EOF", c->ident); + return -1; +} + +int disorder_recent(disorder_client *c, struct queue_entry **qp) { + return disorder_somequeue(c, "recent", qp); +} + +int disorder_queue(disorder_client *c, struct queue_entry **qp) { + return disorder_somequeue(c, "queue", qp); +} + +static int readlist(disorder_client *c, char ***vecp, int *nvecp) { + char *l; + struct vector v; + + vector_init(&v); + while(inputline(c->ident, c->fpin, &l, '\n') >= 0) { + if(!strcmp(l, ".")) { + vector_terminate(&v); + if(nvecp) + *nvecp = v.nvec; + *vecp = v.vec; + return 0; + } + vector_append(&v, l + (*l == '.')); + } + if(ferror(c->fpin)) + error(errno, "error reading %s", c->ident); + else + error(0, "error reading %s: unexpected EOF", c->ident); + return -1; +} + +static int disorder_simple_list(disorder_client *c, + char ***vecp, int *nvecp, + const char *cmd, ...) { + va_list ap; + int ret; + + va_start(ap, cmd); + ret = disorder_simple_v(c, 0, cmd, ap); + va_end(ap); + if(ret) return ret; + return readlist(c, vecp, nvecp); +} + +int disorder_directories(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "dirs", dir, re, (char *)0); +} + +int disorder_files(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "files", dir, re, (char *)0); +} + +int disorder_allfiles(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "allfiles", dir, re, (char *)0); +} + +char *disorder_user(disorder_client *c) { + return c->user; +} + +int disorder_set(disorder_client *c, const char *track, + const char *key, const char *value) { + return disorder_simple(c, 0, "set", track, key, value, (char *)0); +} + +int disorder_unset(disorder_client *c, const char *track, + const char *key) { + return disorder_simple(c, 0, "unset", track, key, (char *)0); +} + +int disorder_get(disorder_client *c, + const char *track, const char *key, char **valuep) { + return disorder_simple(c, valuep, "get", track, key, (char *)0); +} + +static void pref_error_handler(const char *msg, + void attribute((unused)) *u) { + error(0, "error handling 'prefs' reply: %s", msg); +} + +int disorder_prefs(disorder_client *c, const char *track, struct kvp **kp) { + char **vec, **pvec; + int nvec, npvec, n; + struct kvp *k; + + if(disorder_simple_list(c, &vec, &nvec, "prefs", track, (char *)0)) + return -1; + for(n = 0; n < nvec; ++n) { + if(!(pvec = split(vec[n], &npvec, SPLIT_QUOTES, pref_error_handler, 0))) + return -1; + if(npvec != 2) { + pref_error_handler("malformed response", 0); + return -1; + } + *kp = k = xmalloc(sizeof *k); + k->name = pvec[0]; + k->value = pvec[1]; + kp = &k->next; + } + *kp = 0; + return 0; +} + +static int boolean(const char *cmd, const char *value, + int *flagp) { + if(!strcmp(value, "yes")) *flagp = 1; + else if(!strcmp(value, "no")) *flagp = 0; + else { + error(0, "malformed response to '%s'", cmd); + return -1; + } + return 0; +} + +int disorder_exists(disorder_client *c, const char *track, int *existsp) { + char *v; + + if(disorder_simple(c, &v, "exists", track, (char *)0)) return -1; + return boolean("exists", v, existsp); +} + +int disorder_enabled(disorder_client *c, int *enabledp) { + char *v; + + if(disorder_simple(c, &v, "enabled", (char *)0)) return -1; + return boolean("enabled", v, enabledp); +} + +int disorder_length(disorder_client *c, const char *track, + long *valuep) { + char *value; + + if(disorder_simple(c, &value, "length", track, (char *)0)) return -1; + *valuep = atol(value); + return 0; +} + +int disorder_search(disorder_client *c, const char *terms, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "search", terms, (char *)0); +} + +int disorder_random_enable(disorder_client *c) { + return disorder_simple(c, 0, "random-enable", (char *)0); +} + +int disorder_random_disable(disorder_client *c) { + return disorder_simple(c, 0, "random-disable", (char *)0); +} + +int disorder_random_enabled(disorder_client *c, int *enabledp) { + char *v; + + if(disorder_simple(c, &v, "random-enabled", (char *)0)) return -1; + return boolean("random-enabled", v, enabledp); +} + +int disorder_stats(disorder_client *c, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "stats", (char *)0); +} + +int disorder_set_volume(disorder_client *c, int left, int right) { + char *ls, *rs; + + if(byte_asprintf(&ls, "%d", left) < 0 + || byte_asprintf(&rs, "%d", right) < 0) + return -1; + if(disorder_simple(c, 0, "volume", ls, rs, (char *)0)) return -1; + return 0; +} + +int disorder_get_volume(disorder_client *c, int *left, int *right) { + char *r; + + if(disorder_simple(c, &r, "volume", (char *)0)) return -1; + if(sscanf(r, "%d %d", left, right) != 2) { + error(0, "error parsing response to 'volume': '%s'", r); + return -1; + } + return 0; +} + +int disorder_log(disorder_client *c, struct sink *s) { + char *l; + + if(disorder_simple(c, 0, "log", (char *)0)) return -1; + while(inputline(c->ident, c->fpin, &l, '\n') >= 0 && strcmp(l, ".")) + if(sink_printf(s, "%s\n", l) < 0) return -1; + if(ferror(c->fpin) || feof(c->fpin)) return -1; + return 0; +} + +int disorder_part(disorder_client *c, char **partp, + const char *track, const char *context, const char *part) { + return disorder_simple(c, partp, "part", track, context, part, (char *)0); +} + +int disorder_resolve(disorder_client *c, char **trackp, const char *track) { + return disorder_simple(c, trackp, "resolve", track, (char *)0); +} + +int disorder_pause(disorder_client *c) { + return disorder_simple(c, 0, "pause", (char *)0); +} + +int disorder_resume(disorder_client *c) { + return disorder_simple(c, 0, "resume", (char *)0); +} + +int disorder_tags(disorder_client *c, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "tags", (char *)0); +} + +int disorder_set_global(disorder_client *c, + const char *key, const char *value) { + return disorder_simple(c, 0, "set-global", key, value, (char *)0); +} + +int disorder_unset_global(disorder_client *c, const char *key) { + return disorder_simple(c, 0, "unset-global", key, (char *)0); +} + +int disorder_get_global(disorder_client *c, const char *key, char **valuep) { + return disorder_simple(c, valuep, "get-global", key, (char *)0); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3937adbfa9480384606631d8e0365885 */ diff --git a/lib/client.h b/lib/client.h new file mode 100644 index 0000000..b3c18a5 --- /dev/null +++ b/lib/client.h @@ -0,0 +1,192 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CLIENT_H +#define CLIENT_H + +/* A simple synchronous client interface. */ + +typedef struct disorder_client disorder_client; + +struct queue_entry; +struct kvp; +struct sink; + +/* Parameter strings (e.g. @track@) are UTF-8 unless specified + * otherwise. */ + +disorder_client *disorder_new(int verbose); +/* create a new disorder_client */ + +int disorder_running(disorder_client *c); +/* return 1 if the server is running, else 0 */ + +int disorder_connect(disorder_client *c); +/* connect a disorder_client using the default settings */ + +int disorder_connect_sock(disorder_client *c, + const struct sockaddr *sa, + socklen_t len, + const char *username, + const char *password, + const char *ident); +/* connect a disorder_client */ + +int disorder_close(disorder_client *c); +/* close a disorder_client */ + +int disorder_become(disorder_client *c, const char *user); +/* become another user (trusted users only) */ + +int disorder_version(disorder_client *c, char **versionp); +/* get the server version */ + +int disorder_play(disorder_client *c, const char *track); +/* add a track to the queue */ + +int disorder_remove(disorder_client *c, const char *track); +/* remove a track from the queue */ + +int disorder_move(disorder_client *c, const char *track, int delta); +/* move a track in the queue @delta@ steps towards the head */ + +int disorder_enable(disorder_client *c); +/* enable playing if it is not already enabled */ + +int disorder_disable(disorder_client *c); +/* disable playing if it is not already disabled. */ + +int disorder_scratch(disorder_client *c, const char *id); +/* scratch the currently playing track. If @id@ is not a null pointer + * then the scratch will be ignored if the ID does not mactch. */ + +int disorder_shutdown(disorder_client *c); +/* shut down the server immediately */ + +int disorder_reconfigure(disorder_client *c); +/* re-read the configuration file */ + +int disorder_rescan(disorder_client *c); +/* initiate a rescan */ + +int disorder_playing(disorder_client *c, struct queue_entry **qp); +/* get the details of the currently playing track (null pointer if + * nothing playing). The first entry in the list is the next track to + * be played. */ + +int disorder_recent(disorder_client *c, struct queue_entry **qp); +/* get a list of recently played tracks. The LAST entry in the list + * is last track to have been played. */ + +int disorder_queue(disorder_client *c, struct queue_entry **qp); +/* get the queue */ + +int disorder_directories(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get subdirectories of @dir@, or of the root if @dir@ is an null + * pointer */ + +int disorder_files(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get list of files in @dir@ */ + +int disorder_allfiles(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get list of files and directories in @dir@ */ + +char *disorder_user(disorder_client *c); +/* remind ourselves what user we went in as */ + +int disorder_exists(disorder_client *c, const char *track, int *existsp); +/* set @*existsp@ to 1 if the track exists, else 0 */ + +int disorder_enabled(disorder_client *c, int *enabledp); +/* set @*enabledp@ to 1 if playing enabled, else 0 */ + +int disorder_set(disorder_client *c, const char *track, + const char *key, const char *value); +int disorder_unset(disorder_client *c, const char *track, + const char *key); +int disorder_get(disorder_client *c, const char *track, const char *key, + char **valuep); +int disorder_prefs(disorder_client *c, const char *track, + struct kvp **kp); +/* set, unset, get, list preferences */ + +int disorder_length(disorder_client *c, const char *track, + long *valuep); +/* get the length of a track in seconds, if it is known */ + +int disorder_search(disorder_client *c, const char *terms, + char ***vecp, int *nvecp); +/* get a list of tracks matching @words@ */ + +int disorder_random_enable(disorder_client *c); +/* enable random play if it is not already enabled */ + +int disorder_random_disable(disorder_client *c); +/* disable random play if it is not already disabled */ + +int disorder_random_enabled(disorder_client *c, int *enabledp); +/* determine whether random play is enabled */ + +int disorder_stats(disorder_client *c, + char ***vecp, int *nvecp); +/* get server statistics */ + +int disorder_set_volume(disorder_client *c, int left, int right); +int disorder_get_volume(disorder_client *c, int *left, int *right); +/* get or set the volume */ + +int disorder_log(disorder_client *c, struct sink *s); +/* send log output to @s@ */ + +int disorder_part(disorder_client *c, char **partp, + const char *track, const char *context, const char *part); +/* get a track name part */ + +int disorder_resolve(disorder_client *c, char **trackp, const char *track); +/* resolve a track name */ + +int disorder_pause(disorder_client *c); +/* Pause the currently playing track. */ + +int disorder_resume(disorder_client *c); +/* Resume after a pause. */ + +int disorder_tags(disorder_client *c, + char ***vecp, int *nvecp); +/* get known tags */ + +int disorder_set_global(disorder_client *c, + const char *key, const char *value); +int disorder_unset_global(disorder_client *c, const char *key); +int disorder_get_global(disorder_client *c, const char *key, char **valuep); +/* get/unset/set global prefs */ + +#endif /* CLIENT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:9707959522fafb36d7bccfd1b154fd84 */ diff --git a/lib/configuration.c b/lib/configuration.c new file mode 100644 index 0000000..164f374 --- /dev/null +++ b/lib/configuration.c @@ -0,0 +1,934 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <ctype.h> +#include <stddef.h> +#include <pwd.h> +#include <langinfo.h> +#include <pcre.h> +#include <signal.h> + +#include "configuration.h" +#include "mem.h" +#include "log.h" +#include "split.h" +#include "syscalls.h" +#include "table.h" +#include "inputline.h" +#include "charset.h" +#include "defs.h" +#include "mixer.h" +#include "printf.h" +#include "regsub.h" +#include "signame.h" + +char *configfile; + +struct config_state { + const char *path; + int line; + struct config *config; +}; + +struct config *config; + +struct conf { + const char *name; + size_t offset; + const struct conftype *type; + int (*validate)(const struct config_state *cs, + int nvec, char **vec); +}; + +struct conftype { + int (*set)(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec); + void (*free)(struct config *c, const struct conf *whoami); +}; + +#define ADDRESS(C, TYPE) ((TYPE *)((char *)(C) + whoami->offset)) +#define VALUE(C, TYPE) (*ADDRESS(C, TYPE)) + +static int set_signal(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + + if(nvec != 1) { + error(0, "%s:%d: '%s' requires one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if((n = find_signal(vec[0])) == -1) { + error(0, "%s:%d: unknown signal '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + VALUE(cs->config, int) = n; + return 0; +} + +static int set_collections(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct collectionlist *cl; + + if(nvec != 3) { + error(0, "%s:%d: '%s' requires three arguments", + cs->path, cs->line, whoami->name); + return -1; + } + if(vec[2][0] != '/') { + error(0, "%s:%d: collection root must start with '/'", + cs->path, cs->line); + return -1; + } + if(vec[2][1] && vec[2][strlen(vec[2])-1] == '/') { + error(0, "%s:%d: collection root must not end with '/'", + cs->path, cs->line); + return -1; + } + cl = ADDRESS(cs->config, struct collectionlist); + ++cl->n; + cl->s = xrealloc(cl->s, cl->n * sizeof (struct collection)); + cl->s[cl->n - 1].module = xstrdup(vec[0]); + cl->s[cl->n - 1].encoding = xstrdup(vec[1]); + cl->s[cl->n - 1].root = xstrdup(vec[2]); + return 0; +} + +static int set_boolean(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int state; + + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if(!strcmp(vec[0], "yes")) state = 1; + else if(!strcmp(vec[0], "no")) state = 0; + else { + error(0, "%s:%d: argument to '%s' must be 'yes' or 'no'", + cs->path, cs->line, whoami->name); + return -1; + } + VALUE(cs->config, int) = state; + return 0; +} + +static int set_string(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + VALUE(cs->config, char *) = xstrdup(vec[0]); + return 0; +} + +static int set_stringlist(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *sl; + + sl = ADDRESS(cs->config, struct stringlist); + sl->n = 0; + for(n = 0; n < nvec; ++n) { + sl->n++; + sl->s = xrealloc(sl->s, (sl->n * sizeof (char *))); + sl->s[sl->n - 1] = xstrdup(vec[n]); + } + return 0; +} + +static int set_integer(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + char *e; + + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if(xstrtol(ADDRESS(cs->config, long), vec[0], &e, 0)) { + error(errno, "%s:%d: converting integer", cs->path, cs->line); + return -1; + } + if(*e) { + error(0, "%s:%d: invalid integer syntax", cs->path, cs->line); + return -1; + } + return 0; +} + +static int set_stringlist_accum(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *s; + struct stringlistlist *sll; + + sll = ADDRESS(cs->config, struct stringlistlist); + sll->n++; + sll->s = xrealloc(sll->s, (sll->n * sizeof (struct stringlist))); + s = &sll->s[sll->n - 1]; + s->n = nvec; + s->s = xmalloc((nvec + 1) * sizeof (char *)); + for(n = 0; n < nvec; ++n) + s->s[n] = xstrdup(vec[n]); + return 0; +} + +static int set_string_accum(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *sl; + + sl = ADDRESS(cs->config, struct stringlist); + for(n = 0; n < nvec; ++n) { + sl->n++; + sl->s = xrealloc(sl->s, (sl->n * sizeof (char *))); + sl->s[sl->n - 1] = xstrdup(vec[n]); + } + return 0; +} + +static int set_restrict(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + unsigned r = 0; + int n, i; + + static const struct restriction { + const char *name; + unsigned bit; + } restrictions[] = { + { "remove", RESTRICT_REMOVE }, + { "scratch", RESTRICT_SCRATCH }, + { "move", RESTRICT_MOVE }, + }; + + for(n = 0; n < nvec; ++n) { + if((i = TABLE_FIND(restrictions, struct restriction, name, vec[n])) < 0) { + error(0, "%s:%d: invalid restriction '%s'", + cs->path, cs->line, vec[n]); + return -1; + } + r |= restrictions[i].bit; + } + VALUE(cs->config, unsigned) = r; + return 0; +} + +static int set_namepart(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct namepartlist *npl = ADDRESS(cs->config, struct namepartlist); + unsigned reflags; + const char *errstr; + int erroffset, n; + pcre *re; + + if(nvec < 3) { + error(0, "%s:%d: namepart needs at least 3 arguments", cs->path, cs->line); + return -1; + } + if(nvec > 5) { + error(0, "%s:%d: namepart needs at most 5 arguments", cs->path, cs->line); + return -1; + } + reflags = nvec >= 5 ? regsub_flags(vec[4]) : 0; + if(!(re = pcre_compile(vec[1], + PCRE_UTF8 + |regsub_compile_options(reflags), + &errstr, &erroffset, 0))) { + error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)", + cs->path, cs->line, vec[1], errstr, erroffset); + return -1; + } + 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].replace = xstrdup(vec[2]); + npl->s[npl->n].context = xstrdup(vec[3]); + npl->s[npl->n].reflags = reflags; + ++npl->n; + /* XXX a bit of a bodge; relies on there being very few parts. */ + for(n = 0; (n < cs->config->nparts + && strcmp(cs->config->parts[n], vec[0])); ++n) + ; + if(n >= cs->config->nparts) { + cs->config->parts = xrealloc(cs->config->parts, + (cs->config->nparts + 1) * sizeof (char *)); + cs->config->parts[cs->config->nparts++] = xstrdup(vec[0]); + } + return 0; +} + +static int set_transform(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct transformlist *tl = ADDRESS(cs->config, struct transformlist); + pcre *re; + unsigned reflags; + const char *errstr; + int erroffset; + + if(nvec < 3) { + error(0, "%s:%d: transform needs at least 3 arguments", cs->path, cs->line); + return -1; + } + if(nvec > 5) { + error(0, "%s:%d: transform needs at most 5 arguments", cs->path, cs->line); + return -1; + } + reflags = (nvec >= 5 ? regsub_flags(vec[4]) : 0); + if(!(re = pcre_compile(vec[1], + PCRE_UTF8 + |regsub_compile_options(reflags), + &errstr, &erroffset, 0))) { + error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)", + cs->path, cs->line, vec[1], errstr, erroffset); + return -1; + } + tl->t = xrealloc(tl->t, (tl->n + 1) * sizeof (struct namepart)); + tl->t[tl->n].type = xstrdup(vec[0]); + tl->t[tl->n].context = xstrdup(vec[3] ? vec[3] : "*"); + tl->t[tl->n].re = re; + tl->t[tl->n].replace = xstrdup(vec[2]); + tl->t[tl->n].flags = reflags; + ++tl->n; + return 0; +} + +/* free functions */ + +static void free_none(struct config attribute((unused)) *c, + const struct conf attribute((unused)) *whoami) { +} + +static void free_string(struct config *c, + const struct conf *whoami) { + xfree(VALUE(c, char *)); +} + +static void free_stringlist(struct config *c, + const struct conf *whoami) { + int n; + struct stringlist *sl = ADDRESS(c, struct stringlist); + + for(n = 0; n < sl->n; ++n) + xfree(sl->s[n]); + xfree(sl->s); +} + +static void free_stringlistlist(struct config *c, + const struct conf *whoami) { + int n, m; + struct stringlistlist *sll = ADDRESS(c, struct stringlistlist); + struct stringlist *sl; + + for(n = 0; n < sll->n; ++n) { + sl = &sll->s[n]; + for(m = 0; m < sl->n; ++m) + xfree(sl->s[m]); + xfree(sl->s); + } + xfree(sll->s); +} + +static void free_collectionlist(struct config *c, + const struct conf *whoami) { + struct collectionlist *cll = ADDRESS(c, struct collectionlist); + struct collection *cl; + int n; + + for(n = 0; n < cll->n; ++n) { + cl = &cll->s[n]; + xfree(cl->module); + xfree(cl->encoding); + xfree(cl->root); + } + xfree(cll->s); +} + +static void free_namepartlist(struct config *c, + const struct conf *whoami) { + struct namepartlist *npl = ADDRESS(c, struct namepartlist); + struct namepart *np; + int n; + + for(n = 0; n < npl->n; ++n) { + np = &npl->s[n]; + xfree(np->part); + pcre_free(np->re); /* ...whatever pcre_free is set to. */ + xfree(np->replace); + xfree(np->context); + } + xfree(npl->s); +} + +static void free_transformlist(struct config *c, + const struct conf *whoami) { + struct transformlist *tl = ADDRESS(c, struct transformlist); + struct transform *t; + int n; + + for(n = 0; n < tl->n; ++n) { + t = &tl->t[n]; + xfree(t->type); + pcre_free(t->re); /* ...whatever pcre_free is set to. */ + xfree(t->replace); + xfree(t->context); + } + xfree(tl->t); +} + +/* configuration types */ + +static const struct conftype + type_signal = { set_signal, free_none }, + type_collections = { set_collections, free_collectionlist }, + type_boolean = { set_boolean, free_none }, + type_string = { set_string, free_string }, + type_stringlist = { set_stringlist, free_stringlist }, + type_integer = { set_integer, free_none }, + type_stringlist_accum = { set_stringlist_accum, free_stringlistlist }, + type_string_accum = { set_string_accum, free_stringlist }, + type_restrict = { set_restrict, free_none }, + type_namepart = { set_namepart, free_namepartlist }, + type_transform = { set_transform, free_transformlist }; + +/* specific validation routine */ + +#define VALIDATE_FILE(test, what) do { \ + struct stat sb; \ + int n; \ + \ + for(n = 0; n < nvec; ++n) { \ + if(stat(vec[n], &sb) < 0) { \ + error(errno, "%s:%d: %s", cs->path, cs->line, vec[n]); \ + return -1; \ + } \ + if(!test(sb.st_mode)) { \ + error(0, "%s:%d: %s is not a %s", \ + cs->path, cs->line, vec[n], what); \ + return -1; \ + } \ + } \ +} while(0) + +static int validate_isdir(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISDIR, "directory"); + return 0; +} + +static int validate_isreg(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISREG, "regular file"); + return 0; +} + +static int validate_ischr(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISCHR, "character device"); + return 0; +} + +static int validate_player(const struct config_state *cs, + int nvec, + char attribute((unused)) **vec) { + if(nvec < 2) { + error(0, "%s:%d: should be at least 'player PATTERN MODULE'", + cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_allow(const struct config_state *cs, + int nvec, + char attribute((unused)) **vec) { + if(nvec != 2) { + error(0, "%s:%d: must be 'allow NAME PASS'", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_non_negative(const struct config_state *cs, + int nvec, char **vec) { + long n; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + if(xstrtol(&n, vec[0], 0, 0)) { + error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno)); + return -1; + } + if(n < 0) { + error(0, "%s:%d: must not be negative", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_positive(const struct config_state *cs, + int nvec, char **vec) { + long n; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + if(xstrtol(&n, vec[0], 0, 0)) { + error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno)); + return -1; + } + if(n <= 0) { + error(0, "%s:%d: must be positive", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_isauser(const struct config_state *cs, + int attribute((unused)) nvec, + char **vec) { + struct passwd *pw; + + if(!(pw = getpwnam(vec[0]))) { + error(0, "%s:%d: no such user as '%s'", cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +static int validate_channel(const struct config_state *cs, + int attribute((unused)) nvec, + char **vec) { + if(mixer_channel(vec[0]) == -1) { + error(0, "%s:%d: invalid channel '%s'", cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +static int validate_any(const struct config_state attribute((unused)) *cs, + int attribute((unused)) nvec, + char attribute((unused)) **vec) { + return 0; +} + +static int validate_url(const struct config_state attribute((unused)) *cs, + int attribute((unused)) nvec, + char **vec) { + const char *s; + int n; + /* absoluteURI = scheme ":" ( hier_part | opaque_part ) + scheme = alpha *( alpha | digit | "+" | "-" | "." ) */ + s = vec[0]; + n = strspn(s, ("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789")); + if(s[n] != ':') { + error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]); + return -1; + } + if(!strncmp(s, "http:", 5) + || !strncmp(s, "https:", 6)) { + s += n + 1; + /* we only do a rather cursory check */ + if(strncmp(s, "//", 2)) { + error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]); + return -1; + } + } + return 0; +} + +static int validate_alias(const struct config_state *cs, + int nvec, + char **vec) { + const char *s; + int in_brackets = 0, c; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + s = vec[0]; + while((c = (unsigned char)*s++)) { + if(in_brackets) { + if(c == '}') + in_brackets = 0; + else if(!isalnum(c)) { + error(0, "%s:%d: invalid part name in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + } else { + if(c == '{') { + in_brackets = 1; + if(*s == '/') + ++s; + } else if(c == '\\') { + if(!(c = (unsigned char)*s++)) { + error(0, "%s:%d: unterminated escape in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } else if(c != '\\' && c != '{') { + error(0, "%s:%d: invalid escape in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + } + } + ++s; + } + if(in_brackets) { + error(0, "%s:%d: unterminated part name in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +/* configuration table */ + +#define C(x) #x, offsetof(struct config, x) +#define C2(x,y) #x, offsetof(struct config, y) + +static const struct conf conf[] = { + { C(alias), &type_string, validate_alias }, + { C(allow), &type_stringlist_accum, validate_allow }, + { C(channel), &type_string, validate_channel }, + { 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_any }, + { C(device), &type_string, validate_any }, + { C(gap), &type_integer, validate_non_negative }, + { C(history), &type_integer, validate_positive }, + { C(home), &type_string, validate_isdir }, + { C(listen), &type_stringlist, validate_any }, + { C(lock), &type_boolean, validate_any }, + { C(mixer), &type_string, validate_ischr }, + { C(namepart), &type_namepart, validate_any }, + { C2(nice, nice_rescan), &type_integer, validate_non_negative }, + { C(nice_rescan), &type_integer, validate_non_negative }, + { C(nice_server), &type_integer, validate_any }, + { C(nice_speaker), &type_integer, validate_any }, + { C(password), &type_string, validate_any }, + { C(player), &type_stringlist_accum, validate_player }, + { C(plugins), &type_string_accum, validate_isdir }, + { C(prefsync), &type_integer, validate_positive }, + { C(refresh), &type_integer, validate_positive }, + { C2(restrict, restrictions), &type_restrict, validate_any }, + { C(scratch), &type_string_accum, validate_isreg }, + { C(signal), &type_signal, validate_any }, + { C(stopword), &type_string_accum, validate_any }, + { C(templates), &type_string_accum, validate_isdir }, + { C(transform), &type_transform, validate_any }, + { C(trust), &type_string_accum, validate_any }, + { C(url), &type_string, validate_url }, + { C(user), &type_string, validate_isauser }, + { C(username), &type_string, validate_any }, +}; + +/* find a configuration item's definition by key */ +static const struct conf *find(const char *key) { + int n; + + if((n = TABLE_FIND(conf, struct conf, name, key)) < 0) + return 0; + return &conf[n]; +} + +/* set a new configuration value */ +static int config_set(const struct config_state *cs, + int nvec, char **vec) { + const struct conf *which; + + D(("config_set %s", vec[0])); + if(!(which = find(vec[0]))) { + error(0, "%s:%d: unknown configuration key '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + return (which->validate(cs, nvec - 1, vec + 1) + || which->type->set(cs, which, nvec - 1, vec + 1)); +} + +static void config_error(const char *msg, void *u) { + const struct config_state *cs = u; + + error(0, "%s:%d: %s", cs->path, cs->line, msg); +} + +/* include a file by name */ +static int config_include(struct config *c, const char *path) { + FILE *fp; + char *buffer, *inputbuffer, **vec; + int n, ret = 0; + struct config_state cs; + + cs.path = path; + cs.line = 0; + cs.config = c; + D(("%s: reading configuration", path)); + if(!(fp = fopen(path, "r"))) { + error(errno, "error opening %s", path); + return -1; + } + while(!inputline(path, fp, &inputbuffer, '\n')) { + ++cs.line; + if(!(buffer = mb2utf8(inputbuffer))) { + error(errno, "%s:%d: cannot convert to UTF-8", cs.path, cs.line); + ret = -1; + xfree(inputbuffer); + continue; + } + xfree(inputbuffer); + if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES, + config_error, &cs))) { + ret = -1; + xfree(buffer); + continue; + } + if(n) { + if(!strcmp(vec[0], "include")) { + if(n != 2) { + error(0, "%s:%d: must be 'include PATH'", cs.path, cs.line); + ret = -1; + } else + config_include(c, vec[1]); + } else + ret |= config_set(&cs, n, vec); + } + for(n = 0; vec[n]; ++n) xfree(vec[n]); + xfree(vec); + xfree(buffer); + } + if(ferror(fp)) { + error(errno, "error reading %s", path); + ret = -1; + } + fclose(fp); + return ret; +} + +/* make a new default config */ +static struct config *config_default(void) { + struct config *c = xmalloc(sizeof *c); + const char *logname; + struct passwd *pw; + + /* Strings had better be xstrdup'd as they will get freed at some point. */ + c->gap = 2; + c->history = 60; + c->home = xstrdup(pkgstatedir); + if(!(pw = getpwuid(getuid()))) + fatal(0, "cannot determine our username"); + logname = pw->pw_name; + c->username = xstrdup(logname); + c->refresh = 15; + c->prefsync = 3600; + c->signal = SIGKILL; + c->alias = xstrdup("{/artist}{/album}{/title}{ext}"); + c->lock = 1; + c->device = xstrdup("default"); + c->nice_rescan = 10; + return c; +} + +static char *get_file(struct config *c, const char *name) { + char *s; + + byte_xasprintf(&s, "%s/%s", c->home, name); + return s; +} + +static void set_configfile(void) { + if(!configfile) + byte_xasprintf(&configfile, "%s/config", pkgconfdir); +} + +/* free the config file */ +static void config_free(struct config *c) { + int n; + + if(c) { + for(n = 0; n < (int)(sizeof conf / sizeof *conf); ++n) + conf[n].type->free(c, &conf[n]); + for(n = 0; n < c->nparts; ++n) + xfree(c->parts[n]); + xfree(c->parts); + xfree(c); + } +} + +static void config_postdefaults(struct config *c) { + struct config_state cs; + const struct conf *whoami; + int n; + + static const char *namepart[][4] = { + { "title", "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" }, + { "title", "/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort" }, + { "album", "/([^/]+)/[^/]+$", "$1", "*" }, + { "artist", "/([^/]+)/[^/]+/[^/]+$", "$1", "*" }, + { "ext", "(\\.[a-zA-Z0-9]+)$", "$1", "*" }, + }; +#define NNAMEPART (int)(sizeof namepart / sizeof *namepart) + + static const char *transform[][5] = { + { "track", "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" }, + { "track", "^.*/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort", "" }, + { "dir", "^.*/([^/]+)$", "$1", "*", "" }, + { "dir", "^(the) ([^/]*)", "$2, $1", "sort", "i", }, + { "dir", "[[:punct:]]", "", "sort", "g", } + }; +#define NTRANSFORM (int)(sizeof transform / sizeof *transform) + + cs.path = "<internal>"; + cs.line = 0; + cs.config = c; + if(!c->namepart.n) { + whoami = find("namepart"); + for(n = 0; n < NNAMEPART; ++n) + set_namepart(&cs, whoami, 4, (char **)namepart[n]); + } + if(!c->transform.n) { + whoami = find("transform"); + for(n = 0; n < NTRANSFORM; ++n) + set_transform(&cs, whoami, 5, (char **)transform[n]); + } +} + +/* re-read the config file */ +int config_read() { + struct config *c; + char *privconf; + struct passwd *pw; + + set_configfile(); + c = config_default(); + if(config_include(c, configfile)) + return -1; + /* if we can read the private config file, do */ + if((privconf = config_private()) + && access(privconf, R_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* if there's a per-user system config file for this user, read it */ + if(!(pw = getpwuid(getuid()))) + fatal(0, "cannot determine our username"); + if((privconf = config_usersysconf(pw)) + && access(privconf, F_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* if we have a password file, read it */ + if((privconf = config_userconf(getenv("HOME"), pw)) + && access(privconf, F_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* install default namepart and transform settings */ + config_postdefaults(c); + /* everything is good so we shall use the new config */ + config_free(config); + config = c; + return 0; +} + +char *config_private(void) { + char *s; + + set_configfile(); + byte_xasprintf(&s, "%s.private", configfile); + return s; +} + +char *config_userconf(const char *home, const struct passwd *pw) { + char *s; + + byte_xasprintf(&s, "%s/.disorder/passwd", home ? home : pw->pw_dir); + return s; +} + +char *config_usersysconf(const struct passwd *pw ) { + char *s; + + set_configfile(); + if(!strchr(pw->pw_name, '/')) { + byte_xasprintf(&s, "%s.%s", configfile, pw->pw_name); + return s; + } else + return 0; +} + +char *config_get_file(const char *name) { + return get_file(config, name); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:ede19ed49dca6136ba864ed0a4988b34 */ diff --git a/lib/configuration.h b/lib/configuration.h new file mode 100644 index 0000000..d647219 --- /dev/null +++ b/lib/configuration.h @@ -0,0 +1,155 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CONFIGURATION_H +#define CONFIGURATION_H + +struct real_pcre; + +/* Configuration is kept in a @struct config@; the live configuration + * is always pointed to by @config@. Values in @config@ are UTF-8 encoded. + */ + +struct stringlist { + int n; + char **s; +}; + +struct stringlistlist { + int n; + struct stringlist *s; +}; + +struct collection { + char *module; + char *encoding; + char *root; +}; + +struct collectionlist { + int n; + struct collection *s; +}; + +struct namepart { + char *part; /* part */ + struct real_pcre *re; /* regexp */ + char *replace; /* replacement string */ + char *context; /* context glob */ + unsigned reflags; /* regexp flags */ +}; + +struct namepartlist { + int n; + struct namepart *s; +}; + +struct transform { + char *type; /* track or dir */ + char *context; /* sort or choose */ + char *replace; /* substitution string */ + struct real_pcre *re; /* compiled re */ + unsigned flags; /* regexp flags */ +}; + +struct transformlist { + int n; + struct transform *t; +}; + +struct config { + /* server config */ + struct stringlistlist player; /* players */ + struct stringlistlist allow; /* allowed users */ + struct stringlist scratch; /* scratch tracks */ + long gap; /* gap between tracks */ + long history; /* length of history */ + struct stringlist trust; /* trusted users */ + const char *user; /* user to run as */ + long nice_rescan; /* rescan subprocess niceness */ + struct stringlist plugins; /* plugin path */ + struct stringlist stopword; /* stopwords for track search */ + struct collectionlist collection; /* track collections */ + long checkpoint_kbyte; + long checkpoint_min; + char *mixer; /* mixer device file */ + char *channel; /* mixer channel */ + long prefsync; /* preflog sync intreval */ + struct stringlist listen; /* secondary listen address */ + const char *alias; /* alias format */ + int lock; /* server takes a lock */ + long nice_server; /* nice value for server */ + long nice_speaker; /* nice value for speaker */ + /* shared client/server config */ + const char *home; /* home directory for state files */ + /* client config */ + const char *username, *password; /* our own username and password */ + struct stringlist connect; /* connect address */ + /* web config */ + struct stringlist templates; /* template path */ + const char *url; /* canonical URL */ + long refresh; /* maximum refresh period */ + unsigned restrictions; /* restrictions */ +#define RESTRICT_SCRATCH 1 +#define RESTRICT_REMOVE 2 +#define RESTRICT_MOVE 4 + struct namepartlist namepart; /* transformations */ + int signal; /* termination signal */ + const char *device; /* ALSA output device */ + struct transformlist transform; /* path name transformations */ + + /* derived values: */ + int nparts; /* number of distinct name parts */ + char **parts; /* name part list */ +}; + +extern struct config *config; +/* the current configuration */ + +int config_read(void); +/* re-read config, return 0 on success or non-0 on error. + * Only updates @config@ if the new configuration is valid. */ + +char *config_get_file(const char *name); +/* get a filename within the home directory */ + +struct passwd; + +char *config_userconf(const char *home, const struct passwd *pw); +/* get the user's own private conffile, assuming their home dir is + * @home@ if not null and using @pw@ otherwise */ + +char *config_usersysconf(const struct passwd *pw ); +/* get the user's conffile in /etc */ + +char *config_private(void); +/* get the private config file */ + +extern char *configfile; + +#endif /* CONFIGURATION_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:bf8be7718c5e6348d0c922dfa66b85f9 */ diff --git a/lib/defs.c b/lib/defs.c new file mode 100644 index 0000000..2bc9296 --- /dev/null +++ b/lib/defs.c @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "defs.h" +#include "definitions.h" + +const char disorder_version_string[] = VERSION; +const char pkglibdir[] = PKGLIBDIR; +const char pkgconfdir[] = PKGCONFDIR; +const char pkgstatedir[] = PKGSTATEDIR; +const char pkgdatadir[] = PKGDATADIR; + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:m7TX2pyO7AO3F/rjc8kqYA */ diff --git a/lib/defs.h b/lib/defs.h new file mode 100644 index 0000000..52271b3 --- /dev/null +++ b/lib/defs.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DEFS_H +#define DEFS_H + +extern const char disorder_version_string[]; +extern const char pkglibdir[]; +extern const char pkgconfdir[]; +extern const char pkgstatedir[]; +extern const char pkgdatadir[]; + +#endif /* DEFS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:6CvTBnYnuaIeb/cbLdo9wg */ diff --git a/lib/disorder.h b/lib/disorder.h new file mode 100644 index 0000000..ded021f --- /dev/null +++ b/lib/disorder.h @@ -0,0 +1,215 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DISORDER_H +#define DISORDER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* memory allocation **********************************************************/ + +void *disorder_malloc(size_t); +void *disorder_realloc(void *, size_t); +/* As malloc/realloc, but + * 1) succeed or call fatal + * 2) always clear (the unused part of) the new allocation + * 3) are garbage-collected + */ + +void *disorder_malloc_noptr(size_t); +void *disorder_realloc_noptr(void *, size_t); +char *disorder_strdup(const char *); +char *disorder_strndup(const char *, size_t); +/* As malloc/realloc/strdup, but + * 1) succeed or call fatal + * 2) are garbage-collected + * 3) allocated space must not contain any pointers + * + * {xmalloc,xrealloc}_noptr don't promise to clear the new space + */ + +#ifdef __GNUC__ + +int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) + __attribute__((format (printf, 3, 4))); +/* like snprintf */ + +int disorder_asprintf(char **rp, const char *fmt, ...) + __attribute__((format (printf, 2, 3))); +/* like asprintf but uses xmalloc_noptr() */ + +#else + +int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...); +/* like snprintf */ + +int disorder_asprintf(char **rp, const char *fmt, ...); +/* like asprintf but uses xmalloc_noptr() */ + +#endif + + +/* logging ********************************************************************/ + +void disorder_error(int errno_value, const char *fmt, ...); +/* report an error. If errno_value is nonzero then the errno string + * is included. */ + +void disorder_fatal(int errno_value, const char *fmt, ...); +/* report an error and terminate. If errno_value is nonzero then the + * errno string is included. This is the only safe way to terminate + * the process. */ + +void disorder_info(const char *fmt, ...); +/* log a message. */ + +/* track database *************************************************************/ + +int disorder_track_exists(const char *track); +/* return true if the track exists. */ + +const char *disorder_track_get_data(const char *track, const char *key); +/* get the value for @key@ (xstrdup'd) */ + +int disorder_track_set_data(const char *track, + const char *key, const char *value); +/* set the value of @key@ to @value@, or remove it if @value@ is a null + * pointer. Return 0 on success, -1 on error. */ + +const char *disorder_track_random(void); /* server plugins only */ +/* return the name of a random track */ + +/* plugin interfaces **********************************************************/ + +long disorder_tracklength(const char *track, const char *path); +/* compute the length of the track. @track@ is the UTF-8 name of the + * track, @path@ is the file system name (or 0 for tracks that don't + * exist in the filesystem). The return value should be a positive + * number of seconds, 0 for unknown or -1 if an error occurred. */ + +void disorder_scan(const char *root); +/* write a list of path names below @root@ to standard output. */ + +int disorder_check(const char *root, const char *path); +/* Recheck a track, given its root and path name. Return 1 if it + * exists, 0 if it does not exist and -1 if an error occurred. */ + +void disorder_notify_play(const char *track, + const char *submitter); +/* we're going to play @track@. It was submitted by @submitter@ + * (might be a null pointer) */ + +void disorder_notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +/* @scratcher@ scratched @track@ after @seconds@. It was submitted by + * @submitter@ (might be a null pointer) */ + +void disorder_notify_not_scratched(const char *track, + const char *submitter); +/* @track@ (submitted by @submitter@, which might be a null pointer) + * was not scratched. */ + +void disorder_notify_queue(const char *track, + const char *submitter); +/* @track@ added to the queue by @submitter@ (never a null pointer) */ + +void disorder_notify_queue_remove(const char *track, + const char *remover); +/* @track@ removed from the queue by @remover@ (never a null pointer) */ + +void disorder_notify_queue_move(const char *track, + const char *mover); +/* @track@ moved in the queue by @mover@ (never a null pointer) */ + +void disorder_notify_pause(const char *track, + const char *pauser); +/* TRACK was paused by PAUSER (might be a null pointer) */ + +void disorder_notify_resume(const char *track, + const char *resumer); +/* TRACK was resumed by PAUSER (might be a null pointer) */ + +/* player plugin interface ****************************************************/ + +extern const unsigned long disorder_player_type; + +#define DISORDER_PLAYER_STANDALONE 0x00000000 +/* this player plays sound directly */ + +#define DISORDER_PLAYER_RAW 0x00000001 +/* player that sends raw samples to $DISORDER_RAW_FD */ + +#define DISORDER_PLAYER_TYPEMASK 0x000000ff +/* mask for player types */ + +#define DISORDER_PLAYER_PREFORK 0x00000100 +/* call prefork function */ + +#define DISORDER_PLAYER_PAUSES 0x00000200 +/* supports pausing */ + +void *disorder_play_prefork(const char *track); +/* Called outside the fork. Should not block. Returns a null pointer + * on error. + * + * If _play_prefork is called then its return value is used as the + * DATA argument to the following functions. Otherwise the value of + * DATA argument is indeterminate and must not be used. */ + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void *data); +/* Called to play a track. Should either exec or only return when the + * track has finished. Should not call exit() (except after a + * succesful exec). Allowed to call _Exit(). */ + +int disorder_play_pause(long *playedp, void *data); +/* Pauses the playing track. If the track can be paused returns 0 and + * stores the number of seconds so far played via PLAYEDP, or sets it + * to -1 if this is not known. If the track cannot be paused then + * returns -1. Should not block. + */ + +void disorder_play_resume(void *data); +/* Restarts play after a pause. PLAYED is the value returned from the + * original pause operation. Should not block. */ + +void disorder_play_cleanup(void *data); +/* called to clean up DATA. Should not block. */ + +#ifdef __cplusplus +}; +#endif + +#endif /* DISORDER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e9581e408f14d9382e27fbc06755baa6 */ diff --git a/lib/eclient.c b/lib/eclient.c new file mode 100644 index 0000000..5e6cd6f --- /dev/null +++ b/lib/eclient.c @@ -0,0 +1,1234 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <netdb.h> +#include <stdlib.h> +#include <assert.h> +#include <inttypes.h> +#include <stddef.h> + +#include "log.h" +#include "mem.h" +#include "configuration.h" +#include "queue.h" +#include "eclient.h" +#include "charset.h" +#include "hex.h" +#include "split.h" +#include "vector.h" +#include "inputline.h" +#include "kvp.h" +#include "syscalls.h" +#include "printf.h" +#include "addr.h" +#include "authhash.h" +#include "table.h" +#include "client-common.h" + +/* TODO: more commands */ + +/* Types *********************************************************************/ + +enum client_state { + state_disconnected, /* not connected */ + state_connecting, /* waiting for connect() */ + state_connected, /* connected but not authenticated */ + state_idle, /* not doing anything */ + state_cmdresponse, /* waiting for command resonse */ + state_body, /* accumulating body */ + state_log, /* monitoring log */ +}; + +static const char *const states[] = { + "disconnected", + "connecting", + "connected", + "idle", + "cmdresponse", + "body", + "log" +}; + +struct operation; /* forward decl */ + +typedef void operation_callback(disorder_eclient *c, struct operation *op); + +/* A pending operation. This can be either a command or part of the + * authentication protocol. In the former case new commands are appended to + * the list, in the latter case they are inserted at the front. */ +struct operation { + struct operation *next; /* next operation */ + char *cmd; /* command to send or 0 */ + operation_callback *opcallback; /* internal completion callback */ + void (*completed)(); /* user completion callback or 0 */ + void *v; /* data for COMPLETED */ + disorder_eclient *client; /* owning client */ + int sent; /* true if sent to server */ +}; + +struct disorder_eclient { + const char *ident; + int fd; + enum client_state state; /* current state */ + int authenticated; /* true when authenicated */ + struct dynstr output; /* output buffer */ + struct dynstr input; /* input buffer */ + int eof; /* input buffer is at EOF */ + /* error reporting callbacks */ + const disorder_eclient_callbacks *callbacks; + void *u; + /* operation queuue */ + struct operation *ops, **opstail; + /* accumulated response */ + int rc; /* response code */ + char *line; /* complete line */ + struct vector vec; /* body */ + /* log client callback */ + const disorder_eclient_log_callbacks *log_callbacks; + void *log_v; + unsigned long statebits; /* current state */ +}; + +/* Forward declarations ******************************************************/ + +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident); +static void process_line(disorder_eclient *c, char *line); +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident); +static void maybe_connected(disorder_eclient *c); +static void authbanner_opcallback(disorder_eclient *c, + struct operation *op); +static void authuser_opcallback(disorder_eclient *c, + struct operation *op); +static void complete(disorder_eclient *c); +static void send_output(disorder_eclient *c); +static void put(disorder_eclient *c, const char *s, size_t n); +static void read_input(disorder_eclient *c); +static void stash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, + ...); +static void log_opcallback(disorder_eclient *c, struct operation *op); +static void logline(disorder_eclient *c, const char *line); +static void logentry_completed(disorder_eclient *c, int nvec, char **vec); +static void logentry_failed(disorder_eclient *c, int nvec, char **vec); +static void logentry_moved(disorder_eclient *c, int nvec, char **vec); +static void logentry_playing(disorder_eclient *c, int nvec, char **vec); +static void logentry_queue(disorder_eclient *c, int nvec, char **vec); +static void logentry_recent_added(disorder_eclient *c, int nvec, char **vec); +static void logentry_recent_removed(disorder_eclient *c, int nvec, char **vec); +static void logentry_removed(disorder_eclient *c, int nvec, char **vec); +static void logentry_scratched(disorder_eclient *c, int nvec, char **vec); +static void logentry_state(disorder_eclient *c, int nvec, char **vec); +static void logentry_volume(disorder_eclient *c, int nvec, char **vec); + +/* Tables ********************************************************************/ + +static const struct logentry_handler { + const char *name; + int min, max; + void (*handler)(disorder_eclient *c, + int nvec, + char **vec); +} logentry_handlers[] = { +#define LE(X, MIN, MAX) { #X, MIN, MAX, logentry_##X } + LE(completed, 1, 1), + LE(failed, 2, 2), + LE(moved, 1, 1), + LE(playing, 1, 2), + LE(queue, 2, INT_MAX), + LE(recent_added, 2, INT_MAX), + LE(recent_removed, 1, 1), + LE(removed, 1, 2), + LE(scratched, 2, 2), + LE(state, 1, 1), + LE(volume, 2, 2) +}; + +/* Setup and teardown ********************************************************/ + +disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, + void *u) { + disorder_eclient *c = xmalloc(sizeof *c); + D(("disorder_eclient_new")); + c->fd = -1; + c->callbacks = cb; + c->u = u; + c->opstail = &c->ops; + vector_init(&c->vec); + dynstr_init(&c->input); + dynstr_init(&c->output); + return c; +} + +void disorder_eclient_close(disorder_eclient *c) { + struct operation *op; + + D(("disorder_eclient_close")); + if(c->fd != -1) { + D(("disorder_eclient_close closing fd %d", c->fd)); + c->callbacks->poll(c->u, c, c->fd, 0); + xclose(c->fd); + c->fd = -1; + c->state = state_disconnected; + } + c->output.nvec = 0; + c->input.nvec = 0; + c->eof = 0; + c->authenticated = 0; + /* We'll need to resend all operations */ + for(op = c->ops; op; op = op->next) + op->sent = 0; +} + +/* Error reporting ***********************************************************/ + +/* called when a connection error occurs */ +static int comms_error(disorder_eclient *c, const char *fmt, ...) { + va_list ap; + char *s; + + D(("comms_error")); + va_start(ap, fmt); + byte_xvasprintf(&s, fmt, ap); + va_end(ap); + disorder_eclient_close(c); + c->callbacks->comms_error(c->u, s); + return -1; +} + +/* called when the server reports an error */ +static int protocol_error(disorder_eclient *c, struct operation *op, + int code, const char *fmt, ...) { + va_list ap; + char *s; + + D(("protocol_error")); + va_start(ap, fmt); + byte_xvasprintf(&s, fmt, ap); + va_end(ap); + c->callbacks->protocol_error(c->u, op->v, code, s); + return -1; +} + +/* State machine *************************************************************/ + +void disorder_eclient_polled(disorder_eclient *c, unsigned mode) { + struct operation *op; + + D(("disorder_eclient_polled fd=%d state=%s mode=[%s %s]", + c->fd, states[c->state], + mode & DISORDER_POLL_READ ? "READ" : "", + mode & DISORDER_POLL_WRITE ? "WRITE" : "")); + /* The pattern here is to check each possible state in turn and try to + * advance (though on error we might go back). If we advance we leave open + * the possibility of falling through to the next state, but we set the mode + * bits to 0, to avoid false positives (which matter more in some cases than + * others). */ + + if(c->state == state_disconnected) { + D(("state_disconnected")); + with_sockaddr(c, start_connect); + /* might now be state_disconnected (on error), state_connecting (slow + * connect) or state_connected (fast connect). If state_disconnected then + * we just rely on a periodic callback from the event loop sometime. */ + mode = 0; + } + + if(c->state == state_connecting && mode) { + D(("state_connecting")); + maybe_connected(c); + /* Might be state_disconnected (on error) or state_connected (on success). + * In the former case we rely on the event loop for a periodic callback to + * retry. */ + mode = 0; + } + + if(c->state == state_connected) { + D(("state_connected")); + /* We just connected. Initiate the authentication protocol. */ + stash_command(c, 1/*queuejump*/, authbanner_opcallback, + 0/*completed*/, 0/*v*/, 0/*cmd*/); + /* We never stay is state_connected very long. We could in principle jump + * straight to state_cmdresponse since there's actually no command to + * send, but that would arguably be cheating. */ + c->state = state_idle; + } + + if(c->state == state_idle) { + D(("state_idle")); + /* We are connected, and have finished any command we set off, look for + * some work to do */ + if(c->ops) { + D(("have ops")); + if(c->authenticated) { + /* Transmit all unsent operations */ + for(op = c->ops; op; op = op->next) { + if(!op->sent) { + put(c, op->cmd, strlen(op->cmd)); + op->sent = 1; + } + } + } else { + /* Just send the head operation */ + if(c->ops->cmd && !c->ops->sent) { + put(c, c->ops->cmd, strlen(c->ops->cmd)); + c->ops->sent = 1; + } + } + /* Awaiting response for the operation at the head of the list */ + c->state = state_cmdresponse; + } else + /* genuinely idle */ + c->callbacks->report(c->u, 0); + } + + if(c->state == state_cmdresponse + || c->state == state_body + || c->state == state_log) { + D(("state_%s", states[c->state])); + /* We are awaiting a response */ + if(mode & DISORDER_POLL_WRITE) send_output(c); + if(mode & DISORDER_POLL_READ) read_input(c); + /* There are a couple of reasons we might want to re-enter the state + * machine from the top. state_idle is obvious: there may be further + * commands to process. Re-entering on state_disconnected means that we + * immediately retry connection if a comms error occurs during a command. + * This is different to the case where a connection fails, where we await a + * spontaneous call to initiate the retry. */ + switch(c->state) { + case state_disconnected: /* lost connection */ + case state_idle: /* completed a command */ + D(("retrying")); + disorder_eclient_polled(c, 0); + return; + default: + break; + } + } + + /* Figure out what to set the mode to */ + switch(c->state) { + case state_disconnected: + D(("state_disconnected (2)")); + /* Probably an error occurred. Await a retry. */ + mode = 0; + break; + case state_connecting: + D(("state_connecting (2)")); + /* Waiting for connect to complete */ + mode = DISORDER_POLL_READ|DISORDER_POLL_WRITE; + break; + case state_connected: + D(("state_connected (2)")); + assert(!"should never be in state_connected here"); + break; + case state_idle: + D(("state_idle (2)")); + /* Connected but nothing to do. */ + mode = 0; + break; + case state_cmdresponse: + case state_body: + case state_log: + D(("state_%s (2)", states[c->state])); + /* Gathering a response. Wait for input. */ + mode = DISORDER_POLL_READ; + /* Flush any pending output. */ + if(c->output.nvec) mode |= DISORDER_POLL_WRITE; + break; + } + D(("fd=%d new mode [%s %s]", + c->fd, + mode & DISORDER_POLL_READ ? "READ" : "", + mode & DISORDER_POLL_WRITE ? "WRITE" : "")); + if(c->fd != -1) c->callbacks->poll(c->u, c, c->fd, mode); +} + +/* Called to start connecting */ +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident) { + disorder_eclient *c = cc; + + D(("start_connect")); + c->ident = xstrdup(ident); + if(c->fd != -1) { + xclose(c->fd); + c->fd = -1; + } + if((c->fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) + return comms_error(c, "socket: %s", strerror(errno)); + c->eof = 0; + nonblock(c->fd); + if(connect(c->fd, sa, len) < 0) { + switch(errno) { + case EINTR: + case EINPROGRESS: + c->state = state_connecting; + /* We are called from _polled so the state machine will get to do its + * thing */ + return 0; + default: + /* Signal the error to the caller. */ + return comms_error(c, "connecting to %s: %s", ident, strerror(errno)); + } + } else + c->state = state_connected; + return 0; +} + +/* Called when maybe connected */ +static void maybe_connected(disorder_eclient *c) { + /* We either connected, or got an error. */ + int err; + socklen_t len = sizeof err; + + D(("maybe_connected")); + /* Work around over-enthusiastic error slippage */ + if(getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) + err = errno; + if(err) { + /* The connection failed */ + comms_error(c, "connecting to %s: %s", c->ident, strerror(err)); + /* sets state_disconnected */ + } else { + /* The connection succeeded */ + c->state = state_connected; + } +} + +/* Authentication ************************************************************/ + +static void authbanner_opcallback(disorder_eclient *c, + struct operation *op) { + size_t nonce_len; + const unsigned char *nonce; + const char *res; + + D(("authbanner_opcallback")); + if(c->rc / 100 != 2) { + /* Banner told us to go away. We cannot proceed. */ + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + disorder_eclient_close(c); + return; + } + nonce = unhex(c->line + 4, &nonce_len); + res = authhash(nonce, nonce_len, config->password); + stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/, + "user", quoteutf8(config->username), quoteutf8(res), + (char *)0); +} + +static void authuser_opcallback(disorder_eclient *c, + struct operation *op) { + D(("authuser_opcallback")); + if(c->rc / 100 != 2) { + /* Wrong password or something. We cannot proceed. */ + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + disorder_eclient_close(c); + return; + } + /* OK, we're authenticated now. */ + c->authenticated = 1; + if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback)) + /* We are a log client, switch to logging mode */ + stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v, + "log", (char *)0); +} + +/* Output ********************************************************************/ + +/* Chop N bytes off the front of a dynstr */ +static void consume(struct dynstr *d, int n) { + D(("consume %d", n)); + assert(d->nvec >= n); + memmove(d->vec, d->vec + n, d->nvec - n); + d->nvec -= n; +} + +/* Write some bytes */ +static void put(disorder_eclient *c, const char *s, size_t n) { + D(("put %d %.*s", c->fd, (int)n, s)); + dynstr_append_bytes(&c->output, s, n); +} + +/* Called when we can write to our FD, or at any other time */ +static void send_output(disorder_eclient *c) { + int n; + + D(("send_output %d bytes pending", c->output.nvec)); + if(c->state > state_connecting && c->output.nvec) { + n = write(c->fd, c->output.vec, c->output.nvec); + if(n < 0) { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + comms_error(c, "writing to %s: %s", c->ident, strerror(errno)); + break; + } + } else + consume(&c->output, n); + } +} + +/* Input *********************************************************************/ + +/* Called when c->fd might be readable, or at any other time */ +static void read_input(disorder_eclient *c) { + char *nl; + int n; + char buffer[512]; + + D(("read_input in state %s", states[c->state])); + if(c->state <= state_connected) return; /* ignore bogus calls */ + /* read some more input */ + n = read(c->fd, buffer, sizeof buffer); + if(n < 0) { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + comms_error(c, "reading from %s: %s", c->ident, strerror(errno)); + break; + } + return; /* no new input to process */ + } else if(n) { + D(("read %d bytes: [%.*s]", n, n, buffer)); + dynstr_append_bytes(&c->input, buffer, n); + } else + c->eof = 1; + /* might have more than one line to process */ + while(c->state > state_connecting + && (nl = memchr(c->input.vec, '\n', c->input.nvec))) { + process_line(c, xstrndup(c->input.vec, nl - c->input.vec)); + /* we might have disconnected along the way, which zogs the input buffer */ + if(c->state > state_connecting) + consume(&c->input, (nl - c->input.vec) + 1); + } + if(c->eof) + comms_error(c, "reading from %s: server disconnected", c->ident); +} + +/* called with a line that has just been read */ +static void process_line(disorder_eclient *c, char *line) { + D(("process_line %d [%s]", c->fd, line)); + switch(c->state) { + case state_cmdresponse: + /* This is the first line of a response */ + if(!(line[0] >= '0' && line[0] <= '9' + && line[1] >= '0' && line[1] <= '9' + && line[2] >= '0' && line[2] <= '9' + && line[3] == ' ')) + fatal(0, "invalid response from server: %s", line); + c->rc = (line[0] * 10 + line[1]) * 10 + line[2] - 111 * '0'; + c->line = line; + switch(c->rc % 10) { + case 3: + /* We need to collect the body. */ + c->state = state_body; + c->vec.nvec = 0; + break; + case 4: + assert(c->log_callbacks != 0); + if(c->log_callbacks->connected) + c->log_callbacks->connected(c->log_v); + c->state = state_log; + break; + default: + /* We've got the whole response. Go into the idle state so the state + * machine knows we're done and then call the operation callback. */ + complete(c); + break; + } + break; + case state_body: + if(strcmp(line, ".")) { + /* A line from the body */ + vector_append(&c->vec, line + (line[0] == '.')); + } else { + /* End of the body. */ + vector_terminate(&c->vec); + complete(c); + } + break; + case state_log: + if(strcmp(line, ".")) { + logline(c, line + (line[0] == '.')); + } else + complete(c); + break; + default: + assert(!"wrong state for location"); + break; + } +} + +/* Called when an operation completes */ +static void complete(disorder_eclient *c) { + struct operation *op; + + D(("complete")); + /* Pop the operation off the queue */ + op = c->ops; + c->ops = op->next; + if(c->opstail == &op->next) + c->opstail = &c->ops; + /* If we've pipelined a command ahead then we go straight to cmdresponser. + * Otherwise we go to idle, which will arrange further sends. */ + c->state = c->ops && c->ops->sent ? state_cmdresponse : state_idle; + op->opcallback(c, op); + /* Note that we always call the opcallback even on error, though command + * opcallbacks generally always do the same error handling, i.e. just call + * protocol_error(). It's the auth* opcallbacks that have different + * behaviour. */ +} + +/* Operation setup ***********************************************************/ + +static void stash_command_vector(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + int ncmd, + char **cmd) { + struct operation *op = xmalloc(sizeof *op); + struct dynstr d; + int n; + + if(cmd) { + dynstr_init(&d); + for(n = 0; n < ncmd; ++n) { + if(n) + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(cmd[n])); + } + dynstr_append(&d, '\n'); + dynstr_terminate(&d); + op->cmd = d.vec; + } else + op->cmd = 0; /* usually, awaiting challenge */ + op->opcallback = opcallback; + op->completed = completed; + op->v = v; + op->next = 0; + op->client = c; + assert(op->sent == 0); + if(queuejump) { + /* Authentication operations jump the queue of useful commands */ + op->next = c->ops; + c->ops = op; + if(c->opstail == &c->ops) + c->opstail = &op->next; + for(op = c->ops; op; op = op->next) + assert(!op->sent); + } else { + *c->opstail = op; + c->opstail = &op->next; + } +} + +static void vstash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, va_list ap) { + char *arg; + struct vector vec; + + D(("vstash_command %s", cmd ? cmd : "NULL")); + if(cmd) { + vector_init(&vec); + vector_append(&vec, (char *)cmd); + while((arg = va_arg(ap, char *))) + vector_append(&vec, arg); + stash_command_vector(c, queuejump, opcallback, completed, v, + vec.nvec, vec.vec); + } else + stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0); +} + +static void stash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, + ...) { + va_list ap; + + va_start(ap, cmd); + vstash_command(c, queuejump, opcallback, completed, v, cmd, ap); + va_end(ap); +} + +/* Command support ***********************************************************/ + +/* for commands with a simple string response */ +static void string_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("string_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_string_response *)op->completed)(op->v, c->line + 4); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands with a simple integer response */ +static void integer_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("string_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_integer_response *)op->completed) + (op->v, strtol(c->line + 4, 0, 10)); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands with no response */ +static void no_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("no_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_no_response *)op->completed)(op->v); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* error callback for queue_unmarshall */ +static void eclient_queue_error(const char *msg, + void *u) { + struct operation *op = u; + + protocol_error(op->client, op, -1, "error parsing queue entry: %s", msg); +} + +/* for commands that expect a queue dump */ +static void queue_response_opcallback(disorder_eclient *c, + struct operation *op) { + int n; + struct queue_entry *q, *qh = 0, **qtail = &qh, *qlast = 0; + + D(("queue_response_callback")); + if(c->rc / 100 == 2) { + /* parse the queue */ + for(n = 0; n < c->vec.nvec; ++n) { + q = xmalloc(sizeof *q); + D(("queue_unmarshall %s", c->vec.vec[n])); + if(!queue_unmarshall(q, c->vec.vec[n], eclient_queue_error, op)) { + q->prev = qlast; + *qtail = q; + qtail = &q->next; + qlast = q; + } + } + if(op->completed) + ((disorder_eclient_queue_response *)op->completed)(op->v, qh); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for 'playing' */ +static void playing_response_opcallback(disorder_eclient *c, + struct operation *op) { + struct queue_entry *q; + + D(("playing_response_callback")); + if(c->rc / 100 == 2) { + switch(c->rc % 10) { + case 2: + if(queue_unmarshall(q = xmalloc(sizeof *q), c->line + 4, + eclient_queue_error, c)) + return; + break; + case 9: + q = 0; + break; + default: + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + return; + } + if(op->completed) + ((disorder_eclient_queue_response *)op->completed)(op->v, q); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands that expect a list of some sort */ +static void list_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("list_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_list_response *)op->completed)(op->v, + c->vec.nvec, + c->vec.vec); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for volume */ +static void volume_response_opcallback(disorder_eclient *c, + struct operation *op) { + int l, r; + + D(("volume_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) { + if(sscanf(c->line + 4, "%d %d", &l, &r) != 2 || l < 0 || r < 0) + protocol_error(c, op, -1, "%s: invalid volume response: %s", + c->ident, c->line); + else + ((disorder_eclient_volume_response *)op->completed)(op->v, l, r); + } + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +static int simple(disorder_eclient *c, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, ...) { + va_list ap; + + va_start(ap, cmd); + vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap); + va_end(ap); + /* Give the state machine a kick, since we might be in state_idle */ + disorder_eclient_polled(c, 0); + return 0; +} + +/* Commands ******************************************************************/ + +int disorder_eclient_version(disorder_eclient *c, + disorder_eclient_string_response *completed, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "version", (char *)0); +} + +int disorder_eclient_namepart(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + const char *context, + const char *part, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "part", track, context, part, (char *)0); +} + +int disorder_eclient_play(disorder_eclient *c, + const char *track, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "play", track, (char *)0); +} + +int disorder_eclient_pause(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "pause", (char *)0); +} + +int disorder_eclient_resume(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "resume", (char *)0); +} + +int disorder_eclient_scratch(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "scratch", id, (char *)0); +} + +int disorder_eclient_scratch_playing(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return disorder_eclient_scratch(c, 0, completed, v); +} + +int disorder_eclient_remove(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "remove", id, (char *)0); +} + +int disorder_eclient_moveafter(disorder_eclient *c, + const char *target, + int nids, + const char **ids, + disorder_eclient_no_response *completed, + void *v) { + struct vector vec; + int n; + + vector_init(&vec); + vector_append(&vec, (char *)"moveafter"); + vector_append(&vec, (char *)target); + for(n = 0; n < nids; ++n) + vector_append(&vec, (char *)ids[n]); + stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v, + vec.nvec, vec.vec); + disorder_eclient_polled(c, 0); + return 0; +} + +int disorder_eclient_recent(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, queue_response_opcallback, (void (*)())completed, v, + "recent", (char *)0); +} + +int disorder_eclient_queue(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, queue_response_opcallback, (void (*)())completed, v, + "queue", (char *)0); +} + +int disorder_eclient_files(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "files", dir, re, (char *)0); +} + +int disorder_eclient_dirs(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "dirs", dir, re, (char *)0); +} + +int disorder_eclient_playing(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, playing_response_opcallback, (void (*)())completed, v, + "playing", (char *)0); +} + +int disorder_eclient_length(disorder_eclient *c, + disorder_eclient_integer_response *completed, + const char *track, + void *v) { + return simple(c, integer_response_opcallback, (void (*)())completed, v, + "length", track, (char *)0); +} + +int disorder_eclient_volume(disorder_eclient *c, + disorder_eclient_volume_response *completed, + int l, int r, + void *v) { + char sl[64], sr[64]; + + if(l < 0 && r < 0) { + return simple(c, volume_response_opcallback, (void (*)())completed, v, + "volume", (char *)0); + } else if(l >= 0 && r >= 0) { + assert(l <= 100); + assert(r <= 100); + byte_snprintf(sl, sizeof sl, "%d", l); + byte_snprintf(sr, sizeof sr, "%d", r); + return simple(c, volume_response_opcallback, (void (*)())completed, v, + "volume", sl, sr, (char *)0); + } else { + assert(!"invalid arguments to disorder_eclient_volume"); + return -1; /* gcc is being dim */ + } +} + +int disorder_eclient_enable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "enable", (char *)0); +} + +int disorder_eclient_disable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "disable", (char *)0); +} + +int disorder_eclient_random_enable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "random-enable", (char *)0); +} + +int disorder_eclient_random_disable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "random-disable", (char *)0); +} + +int disorder_eclient_get(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, const char *pref, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "get", track, pref, (char *)0); +} + +int disorder_eclient_set(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + const char *value, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "set", track, pref, value, (char *)0); +} + +int disorder_eclient_unset(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "unset", track, pref, (char *)0); +} + +int disorder_eclient_resolve(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "resolve", track, (char *)0); +} + +int disorder_eclient_search(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *terms, + void *v) { + if(!split(terms, 0, SPLIT_QUOTES, 0, 0)) return -1; + return simple(c, list_response_opcallback, (void (*)())completed, v, + "search", terms, (char *)0); +} + +/* Log clients ***************************************************************/ + +int disorder_eclient_log(disorder_eclient *c, + const disorder_eclient_log_callbacks *callbacks, + void *v) { + if(c->log_callbacks) return -1; + c->log_callbacks = callbacks; + c->log_v = v; + stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v, + "log", (char *)0); + return 0; +} + +/* If we get here we've stopped being a log client */ +static void log_opcallback(disorder_eclient *c, + struct operation attribute((unused)) *op) { + D(("log_opcallback")); + c->log_callbacks = 0; + c->log_v = 0; +} + +/* error callback for log line parsing */ +static void logline_error(const char *msg, void *u) { + disorder_eclient *c = u; + protocol_error(c, c->ops, -1, "error parsing log line: %s", msg); +} + +/* process a single log line */ +static void logline(disorder_eclient *c, const char *line) { + int nvec, n; + char **vec; + uintmax_t when; + + D(("log_opcallback [%s]", line)); + vec = split(line, &nvec, SPLIT_QUOTES, logline_error, c); + if(nvec < 2) return; /* probably an error, already + * reported */ + if(sscanf(vec[0], "%"SCNxMAX, &when) != 1) { + /* probably the wrong side of a format change */ + protocol_error(c, c->ops, -1, "invalid log timestamp '%s'", vec[0]); + return; + } + /* TODO: do something with the time */ + n = TABLE_FIND(logentry_handlers, struct logentry_handler, name, vec[1]); + if(n < 0) return; /* probably a future command */ + vec += 2; + nvec -= 2; + if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max) + return; + logentry_handlers[n].handler(c, nvec, vec); +} + +static void logentry_completed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->completed) return; + c->log_callbacks->completed(c->log_v, vec[0]); +} + +static void logentry_failed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->failed)return; + c->log_callbacks->failed(c->log_v, vec[0], vec[1]); +} + +static void logentry_moved(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->moved) return; + c->log_callbacks->moved(c->log_v, vec[0]); +} + +static void logentry_playing(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->playing) return; + c->log_callbacks->playing(c->log_v, vec[0], vec[1]); +} + +static void logentry_queue(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + struct queue_entry *q; + + if(!c->log_callbacks->completed) return; + q = xmalloc(sizeof *q); + if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c)) + return; /* bogus */ + c->log_callbacks->queue(c->log_v, q); +} + +static void logentry_recent_added(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + struct queue_entry *q; + + if(!c->log_callbacks->recent_added) return; + q = xmalloc(sizeof *q); + if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c)) + return; /* bogus */ + c->log_callbacks->recent_added(c->log_v, q); +} + +static void logentry_recent_removed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->recent_removed) return; + c->log_callbacks->recent_removed(c->log_v, vec[0]); +} + +static void logentry_removed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->removed) return; + c->log_callbacks->removed(c->log_v, vec[0], vec[1]); +} + +static void logentry_scratched(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->scratched) return; + c->log_callbacks->scratched(c->log_v, vec[0], vec[1]); +} + +static const struct { + unsigned long bit; + const char *enable; + const char *disable; +} statestrings[] = { + { DISORDER_PLAYING_ENABLED, "enable_play", "disable_play" }, + { DISORDER_RANDOM_ENABLED, "enable_random", "disable_random" }, + { DISORDER_TRACK_PAUSED, "pause", "resume" }, +}; +#define NSTATES (int)(sizeof states / sizeof *states) + +static void logentry_state(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + int n; + + for(n = 0; n < NSTATES; ++n) + if(!strcmp(vec[0], statestrings[n].enable)) { + c->statebits |= statestrings[n].bit; + break; + } else if(!strcmp(vec[0], statestrings[n].disable)) { + c->statebits &= ~statestrings[n].bit; + break; + } + if(!c->log_callbacks->state) return; + c->log_callbacks->state(c->log_v, c->statebits); +} + +static void logentry_volume(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + long l, r; + + if(!c->log_callbacks->volume) return; + if(xstrtol(&l, vec[0], 0, 10) + || xstrtol(&r, vec[1], 0, 10) + || l < 0 || l > INT_MAX + || r < 0 || r > INT_MAX) + return; /* bogus */ + c->log_callbacks->volume(c->log_v, (int)l, (int)r); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:61ONz2p/LWaDRnToGI2+fg */ diff --git a/lib/eclient.h b/lib/eclient.h new file mode 100644 index 0000000..caeb81c --- /dev/null +++ b/lib/eclient.h @@ -0,0 +1,271 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef ECLIENT_H +#define ECLIENT_H + +/* Asynchronous client interface. You must provide disorder_client_poll(). */ + +typedef struct disorder_eclient disorder_eclient; + +struct queue_entry; + +#define DISORDER_POLL_READ 1u /* want to read FD */ +#define DISORDER_POLL_WRITE 2u /* want to write FD */ + +/* Callbacks for all clients. These must all be valid. */ +typedef struct disorder_eclient_callbacks { + void (*comms_error)(void *u, const char *msg); + /* Called when a communication error (e.g. connected refused) occurs. U + * comes from the _new() call and MSG describes the problem.*/ + + void (*protocol_error)(void *u, void *v, int code, const char *msg); + /* Called when a command fails (including initial authorization). U comes + * from the _new() call, V from the failed command or a null pointer if the + * error is in setup and MSG describes the problem. */ + + void (*poll)(void *u, disorder_eclient *c, int fd, unsigned mode); + /* Set poll/select flags for FD according to MODE. FD will never be -1. + * Before FD is closed, you will always get a call with MODE=0. U comes from + * the _new() call. */ + + void (*report)(void *u, const char *msg); + /* Called from time to time to report what's doing. Called with MSG=0 + * when the client goes idle.*/ +} disorder_eclient_callbacks; + +/* Callbacks for log clients. All of these are allowed to be a null pointers + * in which case you don't get told about that log event. */ +typedef struct disorder_eclient_log_callbacks { + void (*connected)(void *v); + /* Called on (re-)connection */ + + /* See disorder_protocol(5) for documentation for the rest */ + + void (*completed)(void *v, const char *track); + void (*failed)(void *v, const char *track, const char *status); + void (*moved)(void *v, const char *user); + void (*playing)(void *v, const char *track, const char *user/*maybe 0*/); + void (*queue)(void *v, struct queue_entry *q); + void (*recent_added)(void *v, struct queue_entry *q); + void (*recent_removed)(void *v, const char *id); + void (*removed)(void *v, const char *id, const char *user/*maybe 0*/); + void (*scratched)(void *v, const char *track, const char *user); + void (*state)(void *v, unsigned long state); + void (*volume)(void *v, int left, int right); +} disorder_eclient_log_callbacks; + +/* State bits */ +#define DISORDER_PLAYING_ENABLED 0x00000001 /* play is enabled */ +#define DISORDER_RANDOM_ENABLED 0x00000002 /* random play is enabled */ +#define DISORDER_TRACK_PAUSED 0x00000004 /* track is paused */ + +struct queue_entry; +struct kvp; +struct sink; + +/* Completion callbacks. These provide the result of operations to the caller. + * It is always allowed for these to be null pointers if you don't care about + * the result. */ + +typedef void disorder_eclient_no_response(void *v); +/* completion callback with no data */ + +typedef void disorder_eclient_string_response(void *v, const char *value); +/* completion callback with a string result */ + +typedef void disorder_eclient_integer_response(void *v, long value); +/* completion callback with a integer result */ + +typedef void disorder_eclient_volume_response(void *v, int l, int r); +/* completion callback with a pair of integer results */ + +typedef void disorder_eclient_queue_response(void *v, struct queue_entry *q); +/* completion callback for queue/recent listing */ + +typedef void disorder_eclient_list_response(void *v, int nvec, char **vec); +/* completion callback for file listing etc */ + +disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, + void *u); +/* Create a new client */ + +void disorder_eclient_close(disorder_eclient *c); +/* Close C */ + +void disorder_eclient_polled(disorder_eclient *c, unsigned mode); +/* Should be called when c's FD is readable and/or writable, and in any case + * from time to time (so that retries work). */ + +int disorder_eclient_version(disorder_eclient *c, + disorder_eclient_string_response *completed, + void *v); +/* fetch the server version */ + +int disorder_eclient_play(disorder_eclient *c, + const char *track, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_pause(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_resume(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_scratch(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v); +/* scratch a track by ID */ + +int disorder_eclient_scratch_playing(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* scratch the playing track whatever it is */ + +int disorder_eclient_remove(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v); +/* remove a track from the queue */ + +int disorder_eclient_moveafter(disorder_eclient *c, + const char *target, + int nids, + const char **ids, + disorder_eclient_no_response *completed, + void *v); +/* move tracks within the queue */ + +int disorder_eclient_playing(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* find the currently playing track (0 for none) */ + +int disorder_eclient_queue(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* list recently played tracks */ + +int disorder_eclient_recent(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* list recently played tracks */ + +int disorder_eclient_files(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v); +/* list files in a directory, matching RE if not a null pointer */ + +int disorder_eclient_dirs(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v); +/* list directories in a directory, matching RE if not a null pointer */ + +int disorder_eclient_namepart(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + const char *context, + const char *part, + void *v); +/* look up a track name part */ + +int disorder_eclient_length(disorder_eclient *c, + disorder_eclient_integer_response *completed, + const char *track, + void *v); +/* look up a track name length */ + +int disorder_eclient_volume(disorder_eclient *c, + disorder_eclient_volume_response *callback, + int l, int r, + void *v); +/* If L and R are both -ve gets the volume. + * If neither are -ve then sets the volume. + * Otherwise asserts! + */ + +int disorder_eclient_enable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_disable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_random_enable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_random_disable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +/* Enable/disable play/random play */ + +int disorder_eclient_resolve(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + void *v); +/* Resolve aliases */ + +int disorder_eclient_log(disorder_eclient *c, + const disorder_eclient_log_callbacks *callbacks, + void *v); +/* Make this a log client (forever - it automatically becomes one again upon + * reconnection) */ + +int disorder_eclient_get(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, const char *pref, + void *v); +int disorder_eclient_set(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + const char *value, + void *v); +int disorder_eclient_unset(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + void *v); +/* Get/set preference values */ + +int disorder_eclient_search(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *terms, + void *v); + +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:YrGF8JfZVK701dhnUAll8w */ diff --git a/lib/event.c b/lib/event.c new file mode 100644 index 0000000..b49b2e1 --- /dev/null +++ b/lib/event.c @@ -0,0 +1,844 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <fcntl.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/resource.h> +#include <sys/wait.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <errno.h> +#include <string.h> +#include <limits.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <stdio.h> +#include "event.h" +#include "mem.h" +#include "log.h" +#include "syscalls.h" +#include "printf.h" +#include "sink.h" + +struct timeout { + struct timeout *next; + struct timeval when; + ev_timeout_callback *callback; + void *u; + int resolve; +}; + +struct fd { + int fd; + ev_fd_callback *callback; + void *u; +}; + +struct fdmode { + fd_set enabled; + fd_set tripped; + int nfds, fdslots; + struct fd *fds; + int maxfd; +}; + +struct signal { + struct sigaction oldsa; + ev_signal_callback *callback; + void *u; +}; + +struct child { + pid_t pid; + int options; + ev_child_callback *callback; + void *u; +}; + +struct ev_source { + struct fdmode mode[ev_nmodes]; + struct timeout *timeouts; + struct signal signals[NSIG]; + sigset_t sigmask; + int escape; + int sigpipe[2]; + int nchildren, nchildslots; + struct child *children; +}; + +static const char *modenames[] = { "read", "write", "except" }; + +/* utilities ******************************************************************/ + +static inline int gt(const struct timeval *a, const struct timeval *b) { + if(a->tv_sec > b->tv_sec) + return 1; + if(a->tv_sec == b->tv_sec + && a->tv_usec > b->tv_usec) + return 1; + return 0; +} + +static inline int ge(const struct timeval *a, const struct timeval *b) { + return !gt(b, a); +} + +/* creation *******************************************************************/ + +ev_source *ev_new(void) { + ev_source *ev = xmalloc(sizeof *ev); + int n; + + memset(ev, 0, sizeof *ev); + for(n = 0; n < ev_nmodes; ++n) + FD_ZERO(&ev->mode[n].enabled); + ev->sigpipe[0] = ev->sigpipe[1] = -1; + sigemptyset(&ev->sigmask); + return ev; +} + +/* event loop *****************************************************************/ + +int ev_run(ev_source *ev) { + for(;;) { + struct timeval now; + struct timeval delta; + int n, mode; + int ret; + int maxfd; + struct timeout *t, **tt; + + xgettimeofday(&now, 0); + /* Handle timeouts. We don't want to handle any timeouts that are added + * while we're handling them (otherwise we'd have to break out of infinite + * loops, preferrably without starving better-behaved subsystems). Hence + * the slightly complicated two-phase approach here. */ + for(t = ev->timeouts; + t && ge(&now, &t->when); + t = t->next) { + t->resolve = 1; + D(("calling timeout for %ld.%ld callback %p %p", + (long)t->when.tv_sec, (long)t->when.tv_usec, + (void *)t->callback, t->u)); + ret = t->callback(ev, &now, t->u); + if(ret) + return ret; + } + tt = &ev->timeouts; + while((t = *tt)) { + if(t->resolve) + *tt = t->next; + else + tt = &t->next; + } + maxfd = 0; + for(mode = 0; mode < ev_nmodes; ++mode) { + ev->mode[mode].tripped = ev->mode[mode].enabled; + if(ev->mode[mode].maxfd > maxfd) + maxfd = ev->mode[mode].maxfd; + } + xsigprocmask(SIG_UNBLOCK, &ev->sigmask, 0); + do { + if(ev->timeouts) { + xgettimeofday(&now, 0); + delta.tv_sec = ev->timeouts->when.tv_sec - now.tv_sec; + delta.tv_usec = ev->timeouts->when.tv_usec - now.tv_usec; + if(delta.tv_usec < 0) { + delta.tv_usec += 1000000; + --delta.tv_sec; + } + if(delta.tv_sec < 0) + delta.tv_sec = delta.tv_usec = 0; + n = select(maxfd + 1, + &ev->mode[ev_read].tripped, + &ev->mode[ev_write].tripped, + &ev->mode[ev_except].tripped, + &delta); + } else { + n = select(maxfd + 1, + &ev->mode[ev_read].tripped, + &ev->mode[ev_write].tripped, + &ev->mode[ev_except].tripped, + 0); + } + } while(n < 0 && errno == EINTR); + xsigprocmask(SIG_BLOCK, &ev->sigmask, 0); + if(n < 0) { + error(errno, "error calling select"); + return -1; + } + if(n > 0) { + /* if anything deranges the meaning of an fd, or re-orders the + * fds[] tables, we'd better give up; such operations will + * therefore set @escape@. */ + ev->escape = 0; + for(mode = 0; mode < ev_nmodes && !ev->escape; ++mode) + for(n = 0; n < ev->mode[mode].nfds && !ev->escape; ++n) { + int fd = ev->mode[mode].fds[n].fd; + if(FD_ISSET(fd, &ev->mode[mode].tripped)) { + D(("calling %s fd %d callback %p %p", modenames[mode], fd, + (void *)ev->mode[mode].fds[n].callback, + ev->mode[mode].fds[n].u)); + ret = ev->mode[mode].fds[n].callback(ev, fd, + ev->mode[mode].fds[n].u); + if(ret) + return ret; + } + } + } + /* we'll pick up timeouts back round the loop */ + } +} + +/* file descriptors ***********************************************************/ + +int ev_fd(ev_source *ev, + ev_fdmode mode, + int fd, + ev_fd_callback *callback, + void *u) { + int n; + + D(("registering %s fd %d callback %p %p", modenames[mode], fd, + (void *)callback, u)); + assert(mode < ev_nmodes); + if(ev->mode[mode].nfds >= ev->mode[mode].fdslots) { + ev->mode[mode].fdslots = (ev->mode[mode].fdslots + ? 2 * ev->mode[mode].fdslots : 16); + D(("expanding %s fd table to %d entries", modenames[mode], + ev->mode[mode].fdslots)); + ev->mode[mode].fds = xrealloc(ev->mode[mode].fds, + ev->mode[mode].fdslots * sizeof (struct fd)); + } + n = ev->mode[mode].nfds++; + FD_SET(fd, &ev->mode[mode].enabled); + ev->mode[mode].fds[n].fd = fd; + ev->mode[mode].fds[n].callback = callback; + ev->mode[mode].fds[n].u = u; + if(fd > ev->mode[mode].maxfd) + ev->mode[mode].maxfd = fd; + ev->escape = 1; + return 0; +} + +int ev_fd_cancel(ev_source *ev, ev_fdmode mode, int fd) { + int n; + int maxfd; + + D(("cancelling mode %s fd %d", modenames[mode], fd)); + /* find the right struct fd */ + for(n = 0; n < ev->mode[mode].nfds && fd != ev->mode[mode].fds[n].fd; ++n) + ; + assert(n < ev->mode[mode].nfds); + /* swap in the last fd and reduce the count */ + if(n != ev->mode[mode].nfds - 1) + ev->mode[mode].fds[n] = ev->mode[mode].fds[ev->mode[mode].nfds - 1]; + --ev->mode[mode].nfds; + /* if that was the biggest fd, find the new biggest one */ + if(fd == ev->mode[mode].maxfd) { + maxfd = 0; + for(n = 0; n < ev->mode[mode].nfds; ++n) + if(ev->mode[mode].fds[n].fd > maxfd) + maxfd = ev->mode[mode].fds[n].fd; + ev->mode[mode].maxfd = maxfd; + } + /* don't tell select about this fd any more */ + FD_CLR(fd, &ev->mode[mode].enabled); + ev->escape = 1; + return 0; +} + +int ev_fd_enable(ev_source *ev, ev_fdmode mode, int fd) { + D(("enabling mode %s fd %d", modenames[mode], fd)); + FD_SET(fd, &ev->mode[mode].enabled); + return 0; +} + +int ev_fd_disable(ev_source *ev, ev_fdmode mode, int fd) { + D(("disabling mode %s fd %d", modenames[mode], fd)); + FD_CLR(fd, &ev->mode[mode].enabled); + FD_CLR(fd, &ev->mode[mode].tripped); + return 0; +} + +/* timeouts *******************************************************************/ + +int ev_timeout(ev_source *ev, + ev_timeout_handle *handlep, + const struct timeval *when, + ev_timeout_callback *callback, + void *u) { + struct timeout *t, *p, **pp; + + D(("registering timeout at %ld.%ld callback %p %p", + when ? (long)when->tv_sec : 0, when ? (long)when->tv_usec : 0, + (void *)callback, u)); + t = xmalloc(sizeof *t); + if(when) + t->when = *when; + t->callback = callback; + t->u = u; + pp = &ev->timeouts; + while((p = *pp) && gt(&t->when, &p->when)) + pp = &p->next; + t->next = p; + *pp = t; + if(handlep) + *handlep = t; + return 0; +} + +int ev_timeout_cancel(ev_source *ev, + ev_timeout_handle handle) { + struct timeout *t = handle, *p, **pp; + + for(pp = &ev->timeouts; (p = *pp) && p != t; pp = &p->next) + ; + if(p) { + *pp = p->next; + return 0; + } else + return -1; +} + +/* signals ********************************************************************/ + +static int sigfd[NSIG]; + +static void sighandler(int s) { + unsigned char sc = s; + static const char errmsg[] = "error writing to signal pipe"; + + /* probably the reader has stopped listening for some reason */ + if(write(sigfd[s], &sc, 1) < 0) { + write(2, errmsg, sizeof errmsg - 1); + abort(); + } +} + +static int signal_read(ev_source *ev, + int attribute((unused)) fd, + void attribute((unused)) *u) { + unsigned char s; + int n; + int ret; + + if((n = read(ev->sigpipe[0], &s, 1)) == 1) + if((ret = ev->signals[s].callback(ev, s, ev->signals[s].u))) + return ret; + assert(n != 0); + if(n < 0 && (errno != EINTR && errno != EAGAIN)) { + error(errno, "error reading from signal pipe %d", ev->sigpipe[0]); + return -1; + } + return 0; +} + +static void close_sigpipe(ev_source *ev) { + int save_errno = errno; + + xclose(ev->sigpipe[0]); + xclose(ev->sigpipe[1]); + ev->sigpipe[0] = ev->sigpipe[1] = -1; + errno = save_errno; +} + +int ev_signal(ev_source *ev, + int sig, + ev_signal_callback *callback, + void *u) { + int n; + struct sigaction sa; + + D(("registering signal %d handler callback %p %p", sig, (void *)callback, u)); + assert(sig > 0); + assert(sig < NSIG); + assert(sig <= UCHAR_MAX); + if(ev->sigpipe[0] == -1) { + D(("creating signal pipe")); + xpipe(ev->sigpipe); + D(("signal pipe is %d, %d", ev->sigpipe[0], ev->sigpipe[1])); + for(n = 0; n < 2; ++n) { + nonblock(ev->sigpipe[n]); + cloexec(ev->sigpipe[n]); + } + if(ev_fd(ev, ev_read, ev->sigpipe[0], signal_read, 0)) { + close_sigpipe(ev); + return -1; + } + } + sigaddset(&ev->sigmask, sig); + xsigprocmask(SIG_BLOCK, &ev->sigmask, 0); + sigfd[sig] = ev->sigpipe[1]; + ev->signals[sig].callback = callback; + ev->signals[sig].u = u; + sa.sa_handler = sighandler; + sigfillset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + xsigaction(sig, &sa, &ev->signals[sig].oldsa); + ev->escape = 1; + return 0; +} + +int ev_signal_cancel(ev_source *ev, + int sig) { + sigset_t ss; + + xsigaction(sig, &ev->signals[sig].oldsa, 0); + ev->signals[sig].callback = 0; + ev->escape = 1; + sigdelset(&ev->sigmask, sig); + sigemptyset(&ss); + sigaddset(&ss, sig); + xsigprocmask(SIG_UNBLOCK, &ss, 0); + return 0; +} + +void ev_signal_atfork(ev_source *ev) { + int sig; + + if(ev->sigpipe[0] != -1) { + /* revert any handled signals to their original state */ + for(sig = 1; sig < NSIG; ++sig) { + if(ev->signals[sig].callback != 0) + xsigaction(sig, &ev->signals[sig].oldsa, 0); + } + /* and then unblock them */ + xsigprocmask(SIG_UNBLOCK, &ev->sigmask, 0); + /* don't want a copy of the signal pipe open inside the fork */ + xclose(ev->sigpipe[0]); + xclose(ev->sigpipe[1]); + } +} + +/* child processes ************************************************************/ + +static int sigchld_callback(ev_source *ev, + int attribute((unused)) sig, + void attribute((unused)) *u) { + struct rusage ru; + pid_t r; + int status, n, ret, revisit; + + do { + revisit = 0; + for(n = 0; n < ev->nchildren; ++n) { + r = wait4(ev->children[n].pid, + &status, + ev->children[n].options | WNOHANG, + &ru); + if(r > 0) { + ev_child_callback *c = ev->children[n].callback; + void *cu = ev->children[n].u; + + if(WIFEXITED(status) || WIFSIGNALED(status)) + ev_child_cancel(ev, r); + revisit = 1; + if((ret = c(ev, r, status, &ru, cu))) + return ret; + } else if(r < 0) { + /* We should "never" get an ECHILD but it can in fact happen. For + * instance on Linux 2.4.31, and probably other versions, if someone + * straces a child process and then a different child process + * terminates, when we wait4() the trace process we will get ECHILD + * because it has been reparented to strace. Obviously this is a + * hopeless design flaw in the tracing infrastructure, but we don't + * want the disorder server to bomb out because of it. So we just log + * the problem and ignore it. + */ + error(errno, "error calling wait4 for PID %lu (broken ptrace?)", + (unsigned long)ev->children[n].pid); + if(errno != ECHILD) + return -1; + } + } + } while(revisit); + return 0; +} + +int ev_child_setup(ev_source *ev) { + D(("installing SIGCHLD handler")); + return ev_signal(ev, SIGCHLD, sigchld_callback, 0); +} + +int ev_child(ev_source *ev, + pid_t pid, + int options, + ev_child_callback *callback, + void *u) { + int n; + + D(("registering child handling %ld options %d callback %p %p", + (long)pid, options, (void *)callback, u)); + assert(ev->signals[SIGCHLD].callback == sigchld_callback); + if(ev->nchildren >= ev->nchildslots) { + ev->nchildslots = ev->nchildslots ? 2 * ev->nchildslots : 16; + ev->children = xrealloc(ev->children, + ev->nchildslots * sizeof (struct child)); + } + n = ev->nchildren++; + ev->children[n].pid = pid; + ev->children[n].options = options; + ev->children[n].callback = callback; + ev->children[n].u = u; + return 0; +} + +int ev_child_cancel(ev_source *ev, + pid_t pid) { + int n; + + for(n = 0; n < ev->nchildren && ev->children[n].pid != pid; ++n) + ; + assert(n < ev->nchildren); + if(n != ev->nchildren - 1) + ev->children[n] = ev->children[ev->nchildren - 1]; + --ev->nchildren; + return 0; +} + +/* socket listeners ***********************************************************/ + +struct listen_state { + ev_listen_callback *callback; + void *u; +}; + +static int listen_callback(ev_source *ev, int fd, void *u) { + const struct listen_state *l = u; + int newfd; + union { + struct sockaddr_in in; +#if HAVE_STRUCT_SOCKADDR_IN6 + struct sockaddr_in6 in6; +#endif + struct sockaddr_un un; + struct sockaddr sa; + } addr; + socklen_t addrlen; + int ret; + + D(("callback for listener fd %d", fd)); + while((addrlen = sizeof addr), + (newfd = accept(fd, &addr.sa, &addrlen)) >= 0) { + if((ret = l->callback(ev, newfd, &addr.sa, addrlen, l->u))) + return ret; + } + switch(errno) { + case EINTR: + case EAGAIN: + break; +#ifdef ECONNABORTED + case ECONNABORTED: + error(errno, "error calling accept"); + break; +#endif +#ifdef EPROTO + case EPROTO: + /* XXX on some systems EPROTO should be fatal, but we don't know if + * we're running on one of them */ + error(errno, "error calling accept"); + break; +#endif + default: + fatal(errno, "error calling accept"); + break; + } + if(errno != EINTR && errno != EAGAIN) + error(errno, "error calling accept"); + return 0; +} + +int ev_listen(ev_source *ev, + int fd, + ev_listen_callback *callback, + void *u) { + struct listen_state *l = xmalloc(sizeof *l); + + D(("registering listener fd %d callback %p %p", fd, (void *)callback, u)); + l->callback = callback; + l->u = u; + return ev_fd(ev, ev_read, fd, listen_callback, l); +} + +int ev_listen_cancel(ev_source *ev, int fd) { + D(("cancelling listener fd %d", fd)); + return ev_fd_cancel(ev, ev_read, fd); +} + +/* buffer *********************************************************************/ + +struct buffer { + char *base, *start, *end, *top; +}; + +/* make sure there is @bytes@ available at @b->end@ */ +static void buffer_space(struct buffer *b, size_t bytes) { + D(("buffer_space %p %p %p %p want %lu", + (void *)b->base, (void *)b->start, (void *)b->end, (void *)b->top, + (unsigned long)bytes)); + if(b->start == b->end) + b->start = b->end = b->base; + if((size_t)(b->top - b->end) < bytes) { + if((size_t)((b->top - b->end) + (b->start - b->base)) < bytes) { + size_t newspace = b->end - b->start + bytes, n; + char *newbase; + + for(n = 16; n < newspace; n *= 2) + ; + newbase = xmalloc_noptr(n); + memcpy(newbase, b->start, b->end - b->start); + b->base = newbase; + b->end = newbase + (b->end - b->start); + b->top = newbase + n; + b->start = newbase; /* must be last */ + } else { + memmove(b->base, b->start, b->end - b->start); + b->end = b->base + (b->end - b->start); + b->start = b->base; + } + } + D(("result %p %p %p %p", + (void *)b->base, (void *)b->start, (void *)b->end, (void *)b->top)); +} + +/* buffered writer ************************************************************/ + +struct ev_writer { + struct sink s; + struct buffer b; + int fd; + int eof; + ev_error_callback *callback; + void *u; + ev_source *ev; +}; + +static int writer_callback(ev_source *ev, int fd, void *u) { + ev_writer *w = u; + int n; + + n = write(fd, w->b.start, w->b.end - w->b.start); + D(("callback for writer fd %d, %ld bytes, n=%d, errno=%d", + fd, (long)(w->b.end - w->b.start), n, errno)); + if(n >= 0) { + w->b.start += n; + if(w->b.start == w->b.end) { + if(w->eof) { + ev_fd_cancel(ev, ev_write, fd); + return w->callback(ev, fd, 0, w->u); + } else + ev_fd_disable(ev, ev_write, fd); + } + } else { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + ev_fd_cancel(ev, ev_write, fd); + return w->callback(ev, fd, errno, w->u); + } + } + return 0; +} + +static int ev_writer_write(struct sink *sk, const void *s, int n) { + ev_writer *w = (ev_writer *)sk; + + buffer_space(&w->b, n); + if(w->b.start == w->b.end) + ev_fd_enable(w->ev, ev_write, w->fd); + memcpy(w->b.end, s, n); + w->b.end += n; + return 0; +} + +ev_writer *ev_writer_new(ev_source *ev, + int fd, + ev_error_callback *callback, + void *u) { + ev_writer *w = xmalloc(sizeof *w); + + D(("registering writer fd %d callback %p %p", fd, (void *)callback, u)); + w->s.write = ev_writer_write; + w->fd = fd; + w->callback = callback; + w->u = u; + w->ev = ev; + if(ev_fd(ev, ev_write, fd, writer_callback, w)) + return 0; + ev_fd_disable(ev, ev_write, fd); + return w; +} + +struct sink *ev_writer_sink(ev_writer *w) { + return &w->s; +} + +static int writer_shutdown(ev_source *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_writer *w = u; + + return w->callback(ev, w->fd, 0, w->u); +} + +int ev_writer_close(ev_writer *w) { + D(("close writer fd %d", w->fd)); + w->eof = 1; + if(w->b.start == w->b.end) { + /* we're already finished */ + ev_fd_cancel(w->ev, ev_write, w->fd); + return ev_timeout(w->ev, 0, 0, writer_shutdown, w); + } + return 0; +} + +int ev_writer_cancel(ev_writer *w) { + D(("cancel writer fd %d", w->fd)); + return ev_fd_cancel(w->ev, ev_write, w->fd); +} + +int ev_writer_flush(ev_writer *w) { + return writer_callback(w->ev, w->fd, w); +} + +/* buffered reader ************************************************************/ + +struct ev_reader { + struct buffer b; + int fd; + ev_reader_callback *callback; + ev_error_callback *error_callback; + void *u; + ev_source *ev; + int eof; +}; + +static int reader_callback(ev_source *ev, int fd, void *u) { + ev_reader *r = u; + int n; + + buffer_space(&r->b, 1); + n = read(fd, r->b.end, r->b.top - r->b.end); + D(("read fd %d buffer %d returned %d errno %d", + fd, (int)(r->b.top - r->b.end), n, errno)); + if(n > 0) { + r->b.end += n; + return r->callback(ev, r, fd, r->b.start, r->b.end - r->b.start, 0, r->u); + } else if(n == 0) { + r->eof = 1; + ev_fd_cancel(ev, ev_read, fd); + return r->callback(ev, r, fd, r->b.start, r->b.end - r->b.start, 1, r->u); + } else { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + ev_fd_cancel(ev, ev_read, fd); + return r->error_callback(ev, fd, errno, r->u); + } + } + return 0; +} + +ev_reader *ev_reader_new(ev_source *ev, + int fd, + ev_reader_callback *callback, + ev_error_callback *error_callback, + void *u) { + ev_reader *r = xmalloc(sizeof *r); + + D(("registering reader fd %d callback %p %p %p", + fd, (void *)callback, (void *)error_callback, u)); + r->fd = fd; + r->callback = callback; + r->error_callback = error_callback; + r->u = u; + r->ev = ev; + if(ev_fd(ev, ev_read, fd, reader_callback, r)) + return 0; + return r; +} + +void ev_reader_buffer(ev_reader *r, size_t nbytes) { + buffer_space(&r->b, nbytes - (r->b.end - r->b.start)); +} + +void ev_reader_consume(ev_reader *r, size_t n) { + r->b.start += n; +} + +int ev_reader_cancel(ev_reader *r) { + D(("cancel reader fd %d", r->fd)); + return ev_fd_cancel(r->ev, ev_read, r->fd); +} + +int ev_reader_disable(ev_reader *r) { + D(("disable reader fd %d", r->fd)); + return r->eof ? 0 : ev_fd_disable(r->ev, ev_read, r->fd); +} + +static int reader_continuation(ev_source attribute((unused)) *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_reader *r = u; + + D(("reader continuation callback fd %d", r->fd)); + if(ev_fd_enable(r->ev, ev_read, r->fd)) return -1; + return r->callback(ev, r, r->fd, r->b.start, r->b.end - r->b.start, r->eof, r->u); +} + +int ev_reader_incomplete(ev_reader *r) { + if(ev_fd_disable(r->ev, ev_read, r->fd)) return -1; + return ev_timeout(r->ev, 0, 0, reader_continuation, r); +} + +static int reader_enabled(ev_source *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_reader *r = u; + + D(("reader enabled callback fd %d", r->fd)); + return r->callback(ev, r, r->fd, r->b.start, r->b.end - r->b.start, r->eof, r->u); +} + +int ev_reader_enable(ev_reader *r) { + D(("enable reader fd %d", r->fd)); + return ((r->eof ? 0 : ev_fd_enable(r->ev, ev_read, r->fd)) + || ev_timeout(r->ev, 0, 0, reader_enabled, r)) ? -1 : 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:a81dd5068039481faac3eea28c995570 */ diff --git a/lib/event.h b/lib/event.h new file mode 100644 index 0000000..32bc1d4 --- /dev/null +++ b/lib/event.h @@ -0,0 +1,248 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef EVENT_H +#define EVENT_H + +typedef struct ev_source ev_source; + +struct rusage; +struct sink; + +ev_source *ev_new(void); +/* create a new event loop */ + +int ev_run(ev_source *ev); +/* run an event loop. If any callback returns nonzero then that value + * is returned. If an error occurs then -1 is returned and @error@ is + * called. */ + +/* file descriptors ***********************************************************/ + +typedef enum { + ev_read, + ev_write, + ev_except, + + ev_nmodes +} ev_fdmode; + +typedef int ev_fd_callback(ev_source *ev, int fd, void *u); +/* signature for fd callback functions */ + +int ev_fd(ev_source *ev, + ev_fdmode mode, + int fd, + ev_fd_callback *callback, + void *u); +/* register a callback on a file descriptor */ + +int ev_fd_cancel(ev_source *ev, + ev_fdmode mode, + int fd); +/* cancel a callback on a file descriptor */ + +int ev_fd_disable(ev_source *ev, + ev_fdmode mode, + int fd); +/* temporarily disable callbacks on a file descriptor */ + +int ev_fd_enable(ev_source *ev, + ev_fdmode mode, + int fd); +/* re-enable callbacks on a file descriptor */ + +/* timeouts *******************************************************************/ + +typedef int ev_timeout_callback(ev_source *ev, + const struct timeval *now, + void *u); +/* signature for timeout callback functions */ + +typedef void *ev_timeout_handle; + +int ev_timeout(ev_source *ev, + ev_timeout_handle *handlep, + const struct timeval *when, + ev_timeout_callback *callback, + void *u); +/* register a timeout callback. If @handlep@ is not a null pointer then a + * handle suitable for ev_timeout_cancel() below is returned through it. */ + +int ev_timeout_cancel(ev_source *ev, + ev_timeout_handle handle); +/* cancel a timeout callback */ + +/* signals ********************************************************************/ + +typedef int ev_signal_callback(ev_source *ev, + int sig, + void *u); +/* signature for signal callback functions */ + +int ev_signal(ev_source *ev, + int sig, + ev_signal_callback *callback, + void *u); +/* register a signal callback */ + +int ev_signal_cancel(ev_source *ev, + int sig); +/* cancel a signal callback */ + +void ev_signal_atfork(ev_source *ev); +/* unhandle and unblock handled signals - call after calling fork and + * then setting @exitfn@ */ + +/* child processes ************************************************************/ + +typedef int ev_child_callback(ev_source *ev, + pid_t pid, + int status, + const struct rusage *rusage, + void *u); +/* signature for child wait callbacks */ + +int ev_child_setup(ev_source *ev); +/* must be called exactly once before @ev_child@ */ + +int ev_child(ev_source *ev, + pid_t pid, + int options, + ev_child_callback *callback, + void *u); +/* register a child callback. @options@ must be 0 or WUNTRACED. */ + +int ev_child_cancel(ev_source *ev, + pid_t pid); +/* cancel a child callback. */ + +/* socket listeners ***********************************************************/ + +typedef int ev_listen_callback(ev_source *ev, + int newfd, + const struct sockaddr *remote, + socklen_t rlen, + void *u); +/* callback when a connection arrives. */ + +int ev_listen(ev_source *ev, + int fd, + ev_listen_callback *callback, + void *u); +/* register a socket listener callback. @bind@ and @listen@ should + * already have been called. */ + +int ev_listen_cancel(ev_source *ev, + int fd); +/* cancel a socket listener callback */ + +/* buffered writer ************************************************************/ + +typedef struct ev_writer ev_writer; + +typedef int ev_error_callback(ev_source *ev, + int fd, + int errno_value, + void *u); +/* called when an error occurs on a writer. Called with @errno_value@ + * of 0 when finished. */ + +ev_writer *ev_writer_new(ev_source *ev, + int fd, + ev_error_callback *callback, + void *u); +/* create a new buffered writer, writing to @fd@. Calls @error@ if an + * error occurs. */ + +int ev_writer_close(ev_writer *w); +/* close a writer (i.e. promise not to write to it any more) */ + +int ev_writer_cancel(ev_writer *w); +/* cancel a writer */ + +int ev_writer_flush(ev_writer *w); +/* attempt to flush the buffer */ + +struct sink *ev_writer_sink(ev_writer *w) attribute((const)); +/* return a sink for the writer - use this to actually write to it */ + +/* buffered reader ************************************************************/ + +typedef struct ev_reader ev_reader; + +typedef int ev_reader_callback(ev_source *ev, + ev_reader *reader, + int fd, + void *ptr, + size_t bytes, + int eof, + void *u); +/* Called when data is read or an error occurs. @ptr@ and @bytes@ + * indicate the amount of data available. @eof@ will be 1 at eof. */ + +ev_reader *ev_reader_new(ev_source *ev, + int fd, + ev_reader_callback *callback, + ev_error_callback *error_callback, + void *u); +/* register a new reader. @callback@ will be called whenever data is + * available. */ + +void ev_reader_buffer(ev_reader *r, size_t nbytes); +/* specify a buffer size *case */ + +void ev_reader_consume(ev_reader *r, size_t nbytes); +/* consume @nbytes@ bytes. */ + +int ev_reader_cancel(ev_reader *r); +/* cancel a reader */ + +int ev_reader_disable(ev_reader *r); +/* disable reading */ + +int ev_reader_incomplete(ev_reader *r); +/* callback didn't fully process buffer, but would like another + * callback (used where processing more would block too long) */ + +int ev_reader_enable(ev_reader *r); +/* enable reading. If there is unconsumed data then you get a + * callback next time round the event loop even if nothing new has + * been read. + * + * The idea is in your read callback you come across a line (or + * whatever) that can't be processed immediately. So you set up + * processing and disable reading. Later when you finish processing + * you re-enable. You'll automatically get another callback pretty + * much direct from the event loop (not from inside ev_reader_enable) + * so you can handle the next line (or whatever) if the whole thing + * has in fact already arrived. + */ + +#endif /* EVENT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:8e6f230cabf206361c14897f1e03b536 */ diff --git a/lib/eventlog.c b/lib/eventlog.c new file mode 100644 index 0000000..c374e4a --- /dev/null +++ b/lib/eventlog.c @@ -0,0 +1,93 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "mem.h" +#include "vector.h" +#include "printf.h" +#include "eventlog.h" +#include "split.h" + +static struct eventlog_output *outputs; + +void eventlog_add(struct eventlog_output *lo) { + lo->next = outputs; + outputs = lo; +} + +void eventlog_remove(struct eventlog_output *lo) { + struct eventlog_output *p, **pp; + + for(pp = &outputs; (p = *pp) && p != lo; pp = &p->next) + ; + if(p == lo) + *pp = lo->next; +} + +static void veventlog(const char *keyword, const char *raw, va_list ap) { + struct eventlog_output *p; + struct dynstr d; + const char *param; + + dynstr_init(&d); + dynstr_append_string(&d, keyword); + while((param = va_arg(ap, const char *))) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(param)); + } + if(raw) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, raw); + } + dynstr_terminate(&d); + for(p = outputs; p; p = p->next) + p->fn(d.vec, p->user); +} + +void eventlog_raw(const char *keyword, const char *raw, ...) { + va_list ap; + + va_start(ap, raw); + veventlog(keyword, raw, ap); + va_end(ap); +} + +void eventlog(const char *keyword, ...) { + va_list ap; + + va_start(ap, keyword); + veventlog(keyword, 0, ap); + va_end(ap); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:Io9E4IlqSMg7Kum3sq080Q */ diff --git a/lib/eventlog.h b/lib/eventlog.h new file mode 100644 index 0000000..102956e --- /dev/null +++ b/lib/eventlog.h @@ -0,0 +1,51 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef EVENTLOG_H +#define EVENTLOG_H + +/* definition of an event log output. The caller must allocate these + * (since log.c isn't allowed to perform memory allocation). */ +struct eventlog_output { + struct eventlog_output *next; + void (*fn)(const char *msg, void *user); + void *user; +}; + +void eventlog_add(struct eventlog_output *lo); +/* add a log output */ + +void eventlog_remove(struct eventlog_output *lo); +/* remove a log output */ + +void eventlog(const char *keyword, ...); +void eventlog_raw(const char *keyword, const char *raw, ...); +/* send a message to the event log */ + +#endif /* EVENTLOG_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:CdZJyFnrtJFEjyo4+7W3cA */ diff --git a/lib/filepart.c b/lib/filepart.c new file mode 100644 index 0000000..575cbc5 --- /dev/null +++ b/lib/filepart.c @@ -0,0 +1,71 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "filepart.h" +#include "mem.h" + +char *d_dirname(const char *path) { + const char *s; + + if((s = strrchr(path, '/'))) { + if(s == path) + return xstrdup("/"); + else + return xstrndup(path, s - path); + } else + return xstrdup("."); +} + +static const char *find_extension(const char *path) { + const char *q = path + strlen(path); + + while(q > path && strchr("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789", *q)) + --q; + return *q == '.' ? q : 0; +} + +const char *strip_extension(const char *path) { + const char *q = find_extension(path); + + return q ? xstrndup(path, q - path) : path; +} + +const char *extension(const char *path) { + const char *q = find_extension(path); + + return q ? q : ""; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:4WKGtvPyLNQ6h3I0n2cMIg */ diff --git a/lib/filepart.h b/lib/filepart.h new file mode 100644 index 0000000..3e616d4 --- /dev/null +++ b/lib/filepart.h @@ -0,0 +1,43 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef FILEPART_H +#define FILEPART_H + +char *d_dirname(const char *path); +/* return the directory name part of @path@ */ + +const char *strip_extension(const char *path); +/* return a filename with the extension stripped */ + +const char *extension(const char *path); +/* return just the extension, or "" */ + +#endif /* FILEPART_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:3bMQUaQm+LrL1Lr12rjgfw */ diff --git a/lib/fprintf.c b/lib/fprintf.c new file mode 100644 index 0000000..f86ede3 --- /dev/null +++ b/lib/fprintf.c @@ -0,0 +1,52 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +int byte_vfprintf(FILE *fp, const char *fmt, va_list ap) { + return byte_vsinkprintf(sink_stdio(0, fp), fmt, ap); +} + +int byte_fprintf(FILE *fp, const char *fmt, ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vfprintf(fp, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:40b957f66080ab957a2f7ca2d963a12f */ diff --git a/lib/hash.c b/lib/hash.c new file mode 100644 index 0000000..2fc19c1 --- /dev/null +++ b/lib/hash.c @@ -0,0 +1,174 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "hash.h" +#include "mem.h" +#include "log.h" + +struct entry { + struct entry *next; /* next entry same key */ + size_t h; /* hash of KEY */ + const char *key; /* key of this entry */ + void *value; /* value of this entry */ +}; + +struct hash { + size_t nslots; /* number of slots */ + size_t nitems; /* total number of entries */ + struct entry **slots; /* table of slots */ + size_t valuesize; /* size of a value */ +}; + +static size_t hashfn(const char *key) { + size_t i = 0; + + while(*key) + i = 33 * i + (unsigned char)*key++; + return i; +} + +static void grow(hash *h) { + size_t n, newnslots; + struct entry **newslots, *e, *f; + + /* Allocate a new, larger array */ + newnslots = 2 * h->nslots; + newslots = xcalloc(newnslots, sizeof (struct entry *)); + /* Copy everything to it */ + for(n = 0; n < h->nslots; ++n) { + for(e = h->slots[n]; e; e = f) { + f = e->next; + e->next = newslots[e->h & (newnslots - 1)]; + newslots[e->h & (newnslots - 1)] = e; + } + } + h->slots = newslots; + h->nslots = newnslots; +} + +hash *hash_new(size_t valuesize) { + hash *h = xmalloc(sizeof *h); + + h->nslots = 256; + h->slots = xcalloc(h->nslots, sizeof (struct slot *)); + h->valuesize = valuesize; + return h; +} + +int hash_add(hash *h, const char *key, const void *value, int mode) { + size_t n = hashfn(key); + struct entry *e; + + for(e = h->slots[n & (h->nslots - 1)]; e; e = e->next) + if(e->h == n || !strcmp(e->key, key)) + break; + if(e) { + /* This key is already present. */ + if(mode == HASH_INSERT) return -1; + if(value) memcpy(e->value, value, h->valuesize); + return 0; + } else { + /* This key is absent. */ + if(mode == HASH_REPLACE) return -1; + if(h->nitems >= h->nslots) /* bound mean chain length */ + grow(h); + e = xmalloc(sizeof *e); + e->next = h->slots[n & (h->nslots - 1)]; + e->h = n; + e->key = xstrdup(key); + e->value = xmalloc(h->valuesize); + if(value) memcpy(e->value, value, h->valuesize); + h->slots[n & (h->nslots - 1)] = e; + ++h->nitems; + return 0; + } +} + +int hash_remove(hash *h, const char *key) { + size_t n = hashfn(key); + struct entry *e, **ee; + + for(ee = &h->slots[n & (h->nslots - 1)]; (e = *ee); ee = &e->next) + if(e->h == n || !strcmp(e->key, key)) + break; + if(e) { + *ee = e->next; + --h->nitems; + return 0; + } else + return -1; +} + +void *hash_find(hash *h, const char *key) { + size_t n = hashfn(key); + struct entry *e; + + for(e = h->slots[n & (h->nslots - 1)]; e; e = e->next) + if(e->h == n || !strcmp(e->key, key)) + return e->value; + return 0; +} + +int hash_foreach(hash *h, + int (*callback)(const char *key, void *value, void *u), + void *u) { + size_t n; + int ret; + struct entry *e, *f; + + for(n = 0; n < h->nslots; ++n) + for(e = h->slots[n]; e; e = f) { + f = e->next; + if((ret = callback(e->key, e->value, u))) + return ret; + } + return 0; +} + +size_t hash_count(hash *h) { + return h->nitems; +} + +char **hash_keys(hash *h) { + size_t n; + char **vec = xcalloc(h->nitems + 1, sizeof (char *)), **vp = vec; + struct entry *e; + + for(n = 0; n < h->nslots; ++n) + for(e = h->slots[n]; e; e = e->next) + *vp++ = (char *)e->key; + *vp = 0; + return vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:8JLNu2iwue7D0MlS0/nR2g */ diff --git a/lib/hash.h b/lib/hash.h new file mode 100644 index 0000000..d8698f0 --- /dev/null +++ b/lib/hash.h @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef HASH_H +#define HASH_H + +typedef struct hash hash; + +hash *hash_new(size_t valuesize); +/* Create a new hash */ + +int hash_add(hash *h, const char *key, const void *value, int mode); +#define HASH_INSERT 0 +#define HASH_REPLACE 1 +#define HASH_INSERT_OR_REPLACE 2 +/* Insert/replace a value in the hash. Returns 0 on success, -1 on + * error. */ + +int hash_remove(hash *h, const char *key); +/* Remove a value in the hash. Returns 0 on success, -1 on error. */ + +void *hash_find(hash *h, const char *key); +/* Find a value in the hash. Returns a null pointer if not found. */ + +int hash_foreach(hash *h, + int (*callback)(const char *key, void *value, void *u), + void *u); +/* Visit all the elements in a hash in any old order. It's safe to remove + * items from inside the callback including the visited one. It is not safe to + * add items from inside the callback however. + * + * If the callback ever returns non-0 then that value is immediately returned. + * Otherwise the return value is 0. + */ + +size_t hash_count(hash *h); +/* Return the number of items in the hash */ + +char **hash_keys(hash *h); +/* Return all the keys of H */ + +#endif /* HASH_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:/YrjtmzK8ADmZ6iZKmX3og */ diff --git a/lib/hex.c b/lib/hex.c new file mode 100644 index 0000000..d2a2a00 --- /dev/null +++ b/lib/hex.c @@ -0,0 +1,94 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> + +#include "hex.h" +#include "mem.h" +#include "log.h" + +char *hex(const uint8_t *ptr, size_t n) { + char *buf = xmalloc_noptr(n * 2 + 1), *p = buf; + + while(n-- > 0) + p += sprintf(p, "%02x", (unsigned)*ptr++); + return buf; +} + +int unhexdigitq(int c) { + switch(c) { + case '0': return 0; + case '1': return 1; + case '2': return 2; + case '3': return 3; + case '4': return 4; + case '5': return 5; + case '6': return 6; + case '7': return 7; + case '8': return 8; + case '9': return 9; + case 'a': case 'A': return 10; + case 'b': case 'B': return 11; + case 'c': case 'C': return 12; + case 'd': case 'D': return 13; + case 'e': case 'E': return 14; + case 'f': case 'F': return 15; + default: return -1; + } +} + +int unhexdigit(int c) { + int d; + + if((d = unhexdigitq(c)) < 0) error(0, "invalid hex digit"); + return d; +} + +uint8_t *unhex(const char *s, size_t *np) { + size_t l; + uint8_t *buf, *p; + int d1, d2; + + if((l = strlen(s)) & 1) { + error(0, "hex string has odd length"); + return 0; + } + p = buf = xmalloc_noptr(l / 2); + while(*s) { + if((d1 = unhexdigit(*s++)) < 0) return 0; + if((d2 = unhexdigit(*s++)) < 0) return 0; + *p++ = d1 * 16 + d2; + } + if(np) + *np = l / 2; + return buf; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3d2adfde608c6d54dc2cf2b42d78e462 */ diff --git a/lib/hex.h b/lib/hex.h new file mode 100644 index 0000000..c6f9c96 --- /dev/null +++ b/lib/hex.h @@ -0,0 +1,44 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef HEX_H +#define HEX_H + +char *hex(const uint8_t *ptr, size_t n); +/* convert an octet-string to hex */ + +uint8_t *unhex(const char *s, size_t *np); +/* convert a hex string back to an octet string */ + +int unhexdigit(int c); +/* if @c@ is a hex digit, return its value. else return -1 and log an error. */ + +int unhexdigitq(int c); +/* same as unhexdigit() but doesn't issue an error message */ + +#endif /* HEX_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:fe1aee378d7330eb7a39283bfb195905 */ diff --git a/lib/inputline.c b/lib/inputline.c new file mode 100644 index 0000000..891bade --- /dev/null +++ b/lib/inputline.c @@ -0,0 +1,61 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <errno.h> +#include <string.h> + +#include "log.h" +#include "mem.h" +#include "vector.h" +#include "charset.h" +#include "inputline.h" + +int inputline(const char *tag, FILE *fp, char **lp, int newline) { + struct dynstr d; + int ch; + + dynstr_init(&d); + while((ch = getc(fp)), + (!ferror(fp) && !feof(fp) && ch != newline)) + dynstr_append(&d, ch); + if(ferror(fp)) { + error(errno, "error reading %s", tag); + return -1; + } else if(feof(fp)) { + if(d.nvec != 0) + error(0, "error reading %s: unexpected EOF", tag); + return -1; + } + dynstr_terminate(&d); + *lp = d.vec; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2df12a23b06659ceea8a6019550a65b4 */ diff --git a/lib/inputline.h b/lib/inputline.h new file mode 100644 index 0000000..74a8cbc --- /dev/null +++ b/lib/inputline.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef INPUTLINE_H +#define INPUTLINE_H + +int inputline(const char *tag, FILE *fp, char **lp, int newline); +/* read characters from @fp@ until @newline@ is encountered. Store + * them (excluding @newline@) via @lp@. Return 0 on success, -1 on + * error/eof. */ + +#endif /* INPUTLINE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:8baeb420a4c9595d8a5d1105d5e8a4f9 */ diff --git a/lib/kvp.c b/lib/kvp.c new file mode 100644 index 0000000..d3a80f5 --- /dev/null +++ b/lib/kvp.c @@ -0,0 +1,195 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stdio.h> + +#include "mem.h" +#include "kvp.h" +#include "log.h" +#include "vector.h" +#include "hex.h" +#include "sink.h" + +int urldecode(struct sink *sink, const char *ptr, size_t n) { + int c, d1, d2; + + while(n-- > 0) { + switch(c = *ptr++) { + case '%': + if((d1 = unhexdigit(ptr[0])) == -1 + || (d2 = unhexdigit(ptr[1])) == -1) + return -1; + c = d1 * 16 + d2; + ptr += 2; + n -= 2; + break; + case '+': + c = ' '; + break; + default: + break; + } + if(sink_writec(sink,c) < 0) + return -1; + } + return 0; +} + +static char *decode(const char *ptr, size_t n) { + struct dynstr d; + struct sink *s; + + dynstr_init(&d); + s = sink_dynstr(&d); + if(urldecode(s, ptr, n)) + return 0; + dynstr_terminate(&d); + return d.vec; +} + +struct kvp *kvp_urldecode(const char *ptr, size_t n) { + struct kvp *kvp, **kk = &kvp, *k; + const char *q, *r, *top = ptr + n, *next; + + while(ptr < top) { + *kk = k = xmalloc(sizeof *k); + if(!(q = memchr(ptr, '=', top - ptr))) + break; + if(!(k->name = decode(ptr, q - ptr))) break; + if((r = memchr(ptr, '&', top - ptr))) + next = r + 1; + else + next = r = top; + if(r < q) + break; + if(!(k->value = decode(q + 1, r - (q + 1)))) break; + kk = &k->next; + ptr = next; + } + *kk = 0; + return kvp; +} + +int urlencode(struct sink *sink, const char *s, size_t n) { + unsigned char c; + + while(n > 0) { + c = *s++; + n--; + switch(c) { + default: + if((c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z')) { + /* RFC2396 2.3 unreserved characters */ + case '-': + case '_': + case '.': + case '!': + case '~': + case '*': + case '\'': + case '(': + case ')': + /* additional unreserved characters */ + case '/': + if(sink_writec(sink, c) < 0) + return -1; + } else + if(sink_printf(sink, "%%%02x", (unsigned int)c) < 0) + return -1; + } + } + return 0; +} + +const char *urlencodestring(const char *s) { + struct dynstr d; + + dynstr_init(&d); + urlencode(sink_dynstr(&d), s, strlen(s)); + dynstr_terminate(&d); + return d.vec; +} + +char *kvp_urlencode(const struct kvp *kvp, size_t *np) { + struct dynstr d; + struct sink *sink; + + dynstr_init(&d); + sink = sink_dynstr(&d); + while(kvp) { + urlencode(sink, kvp->name, strlen(kvp->name)); + dynstr_append(&d, '='); + urlencode(sink, kvp->value, strlen(kvp->value)); + if((kvp = kvp->next)) + dynstr_append(&d, '&'); + + } + dynstr_terminate(&d); + if(np) + *np = d.nvec; + return d.vec; +} + +int kvp_set(struct kvp **kvpp, const char *name, const char *value) { + struct kvp *k, **kk; + + for(kk = kvpp; (k = *kk) && strcmp(name, k->name); kk = &k->next) + ; + if(k) { + if(value) { + if(strcmp(k->value, value)) { + k->value = xstrdup(value); + return 1; + } else + return 0; + } else { + *kk = k->next; + return 1; + } + } else { + if(value) { + *kk = k = xmalloc(sizeof *k); + k->name = xstrdup(name); + k->value = xstrdup(value); + return 1; + } else + return 0; + } +} + +const char *kvp_get(const struct kvp *kvp, const char *name) { + for(;kvp && strcmp(kvp->name, name); kvp = kvp->next) + ; + return kvp ? kvp->value : 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7c983bde915ec06fbda8d8cdc465dd9d */ diff --git a/lib/kvp.h b/lib/kvp.h new file mode 100644 index 0000000..52d9e49 --- /dev/null +++ b/lib/kvp.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef KVP_H +#define KVP_H + +struct dynstr; +struct sink; + +struct kvp { + struct kvp *next; /* next entry */ + const char *name; /* name */ + const char *value; /* value */ +}; + +struct kvp *kvp_urldecode(const char *ptr, size_t n); +/* url-decode [ptr,ptr+n) */ + +char *kvp_urlencode(const struct kvp *kvp, size_t *np); +/* url-encode @kvp@ into a null-terminated string. If @np@ is not + * null return the length thru it. */ + +int kvp_set(struct kvp **kvpp, const char *name, const char *value); +/* set @name@ to @value@. If @value@ is 0, remove @name@. + * Returns 1 if we made a real change, else 0. */ + +const char *kvp_get(const struct kvp *kvp, const char *name); +/* Get the value of @name@ */ + +int urldecode(struct sink *sink, const char *ptr, size_t n); +/* url-decode the @n@ bytes at @ptr@, writing the results to @s@. + * Return 0 on success, -1 on error. */ + +int urlencode(struct sink *sink, const char *s, size_t n); +/* url-encode the @n@ bytes at @s@, writing to @sink@. Return 0 on + * success, -1 on error. */ + +const char *urlencodestring(const char *s); +/* return the url-encoded form of @s@ */ + +#endif /* KVP_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:edb5787b529ef7b694efa4ce2c44ff3f */ diff --git a/lib/log-impl.h b/lib/log-impl.h new file mode 100644 index 0000000..27f0004 --- /dev/null +++ b/lib/log-impl.h @@ -0,0 +1,53 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +void disorder_fatal(int errno_value, const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_CRIT, errno_value, msg, ap); + va_end(ap); + if(getenv("DISORDER_FATAL_ABORT")) abort(); + exitfn(EXIT_FAILURE); +} + +void disorder_error(int errno_value, const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_ERR, errno_value, msg, ap); + va_end(ap); +} + +void disorder_info(const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_INFO, 0, msg, ap); + va_end(ap); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e499f9971df6553a14994bb186235869 */ diff --git a/lib/log.c b/lib/log.c new file mode 100644 index 0000000..e6539cf --- /dev/null +++ b/lib/log.c @@ -0,0 +1,180 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because the memory allocation functions report errors */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <syslog.h> +#include <sys/time.h> + +#include "log.h" +#include "disorder.h" +#include "printf.h" + +struct log_output { + void (*fn)(int pri, const char *msg, void *user); + void *user; +}; + +void (*exitfn)(int) attribute((noreturn)) = exit; +int debugging; +const char *progname; +const char *debug_filename; +int debug_lineno; +struct log_output *log_default = &log_stderr; + +static const char *debug_only; + +/* we might be receiving things in any old encoding, or binary rubbish in no + * encoding at all, so escape anything we don't like the look of */ +static void format(char buffer[], size_t bufsize, const char *fmt, va_list ap) { + char t[1024]; + const char *p; + int ch; + size_t n = 0; + + if(byte_vsnprintf(t, sizeof t, fmt, ap) < 0) + strcpy(t, "[byte_vsnprintf failed]"); + p = t; + while((ch = (unsigned char)*p++)) { + if(ch >= ' ' && ch <= 126) { + if(n < bufsize) buffer[n++] = ch; + } else { + if(n < bufsize) buffer[n++] = '\\'; + if(n < bufsize) buffer[n++] = '0' + ((ch >> 6) & 7); + if(n < bufsize) buffer[n++] = '0' + ((ch >> 3) & 7); + if(n < bufsize) buffer[n++] = '0' + ((ch >> 0) & 7); + } + } + if(n >= bufsize) + n = bufsize - 1; + buffer[n] = 0; +} + +/* log to a file */ +static void logfp(int pri, const char *msg, void *user) { + struct timeval tv; + FILE *fp = user ? user : stderr; + /* ...because stderr is not a constant so we can't initialize log_stderr + * sanely */ + const char *p; + + if(progname) + fprintf(fp, "%s: ", progname); + if(pri <= LOG_ERR) + fputs("ERROR: ", fp); + else if(pri < LOG_DEBUG) + fputs("INFO: ", fp); + else { + if(!debug_only) { + if(!(debug_only = getenv("DISORDER_DEBUG_ONLY"))) + debug_only = ""; + } + gettimeofday(&tv, 0); + p = debug_filename; + while(!strncmp(p, "../", 3)) p += 3; + if(*debug_only && strcmp(p, debug_only)) + return; + fprintf(fp, "%llu.%06lu: %s:%d: ", + (unsigned long long)tv.tv_sec, (unsigned long)tv.tv_usec, + p, debug_lineno); + } + fputs(msg, fp); + fputc('\n', fp); +} + +/* log to syslog */ +static void logsyslog(int pri, const char *msg, + void attribute((unused)) *user) { + if(pri < LOG_DEBUG) + syslog(pri, "%s", msg); + else + syslog(pri, "%s:%d: %s", debug_filename, debug_lineno, msg); +} + +struct log_output log_stderr = { logfp, 0 }; +struct log_output log_syslog = { logsyslog, 0 }; + +/* log to all log outputs */ +static void vlogger(int pri, const char *fmt, va_list ap) { + char buffer[1024]; + + format(buffer, sizeof buffer, fmt, ap); + log_default->fn(pri, buffer, log_default->user); +} + +/* wrapper for vlogger */ +static void logger(int pri, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vlogger(pri, fmt, ap); + va_end(ap); +} + +/* internals of fatal/error/info */ +void elog(int pri, int errno_value, const char *fmt, va_list ap) { + char buffer[1024]; + + if(errno_value == 0) + vlogger(pri, fmt, ap); + else { + memset(buffer, 0, sizeof buffer); + byte_vsnprintf(buffer, sizeof buffer, fmt, ap); + buffer[sizeof buffer - 1] = 0; + logger(pri, "%s: %s", buffer, strerror(errno_value)); + } +} + +#define disorder_fatal fatal +#define disorder_error error +#define disorder_info info + +/* shared implementation of vararg functions */ +#include "log-impl.h" + +void debug(const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + vlogger(LOG_DEBUG, msg, ap); + va_end(ap); +} + +void set_progname(char **argv) { + if((progname = strrchr(argv[0], '/'))) + ++progname; + else + progname = argv[0]; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:78385d5240eab4439cb7eca7dad5154d */ diff --git a/lib/log.h b/lib/log.h new file mode 100644 index 0000000..e251a0e --- /dev/null +++ b/lib/log.h @@ -0,0 +1,89 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef LOG_H +#define LOG_H + +/* All messages are initially emitted by one of the four functions below. + * debug() is generally invoked via D() so that mostly you just do a test + * rather than a complete subroutine call. + * + * Messages are dispatched via log_default. This defaults to log_stderr. + * daemonize() will turn off log_stderr and use log_syslog instead. + * + * fatal() will call exitfn() with a nonzero status. The default value is + * exit(), but it should be set to _exit() anywhere but the 'main line' of the + * program, to guarantee that exit() gets called at most once. + */ + +#include <stdarg.h> + +struct log_output; + +void set_progname(char **argv); +/* set progname from argv[0] */ + +void elog(int pri, int errno_value, const char *fmt, va_list ap); +/* internals of fatal/error/info/debug */ + +void fatal(int errno_value, const char *msg, ...) attribute((noreturn)) + attribute((format (printf, 2, 3))); +void error(int errno_value, const char *msg, ...) + attribute((format (printf, 2, 3))); +void info(const char *msg, ...) + attribute((format (printf, 1, 2))); +void debug(const char *msg, ...) + attribute((format (printf, 1, 2))); +/* report a message of the given class. @errno_value@ if present an + * non-zero is included. @fatal@ terminates the process. */ + +extern int debugging; +/* set when debugging enabled */ + +extern void (*exitfn)(int) attribute((noreturn)); +/* how to exit the program (for fatal) */ + +extern const char *progname; +/* program name */ + +extern struct log_output log_stderr, log_syslog, *log_default; +/* some typical outputs */ + +extern const char *debug_filename; +extern int debug_lineno; + +#define D(x) do { \ + if(debugging) { \ + debug_filename=__FILE__; \ + debug_lineno=__LINE__; \ + debug x; \ + } \ +} while(0) + +#endif /* LOG_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:6350679c7069ec3b2709aa51004a804a */ diff --git a/lib/logfd.c b/lib/logfd.c new file mode 100644 index 0000000..e644e6c --- /dev/null +++ b/lib/logfd.c @@ -0,0 +1,96 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include "syscalls.h" +#include "logfd.h" +#include "event.h" +#include "log.h" + +struct logfd_state { + const char *tag; +}; + +/* called when bytes are available and at eof */ +static int logfd_readable(ev_source attribute((unused)) *ev, + ev_reader *reader, + int fd, + void *ptr, + size_t bytes, + int eof, + void *u) { + char *nl; + const char *tag = u; + int len; + + while((nl = memchr(ptr, '\n', bytes))) { + len = nl - (char *)ptr; + ev_reader_consume(reader, len + 1); + info("%s: %.*s", tag, len, (char *)ptr); + ptr = nl + 1; + bytes -= len + 1; + } + if(eof && bytes) { + info("%s: %.*s", tag, (int)bytes, (char *)ptr); + ev_reader_consume(reader, bytes); + } + if(eof) + xclose(fd); + return 0; +} + +/* called when a read error occurs */ +static int logfd_error(ev_source attribute((unused)) *ev, + int fd, + int errno_value, + void *u) { + const char *tag = u; + + error(errno_value, "error reading log pipe from %s", tag); + xclose(fd); + return 0; +} + +int logfd(ev_source *ev, const char *tag) { + int p[2]; + + xpipe(p); + cloexec(p[0]); + nonblock(p[0]); + if(!ev_reader_new(ev, p[0], logfd_readable, logfd_error, (void *)tag)) + fatal(errno, "error calling ev_reader_new"); + return p[1]; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:zVrHlzk2xWMLo9nDM803pA */ diff --git a/lib/logfd.h b/lib/logfd.h new file mode 100644 index 0000000..4815fa4 --- /dev/null +++ b/lib/logfd.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef LOGFD_H +#define LOGFD_H + +struct ev_source; + +int logfd(struct ev_source *ev, const char *tag); +/* return an FD to write log messages to */ + +#endif /* LOGFD_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:NBzZxO2J269xJpf8eiBw+Q */ diff --git a/lib/mem-impl.h b/lib/mem-impl.h new file mode 100644 index 0000000..a1ad0d1 --- /dev/null +++ b/lib/mem-impl.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +int disorder_asprintf(char **rp, const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = byte_vasprintf(rp, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:89630239839ffe383cc381e9d2e2fb19 */ diff --git a/lib/mem.c b/lib/mem.c new file mode 100644 index 0000000..07c3495 --- /dev/null +++ b/lib/mem.c @@ -0,0 +1,124 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <gc.h> +#include <errno.h> +#include <string.h> +#include <stdio.h> +#include <stdlib.h> + +#include "mem.h" +#include "log.h" +#include "printf.h" + +#include "disorder.h" + +static void *(*do_malloc)(size_t) = GC_malloc; +static void *(*do_realloc)(void *, size_t) = GC_realloc; +static void *(*do_malloc_atomic)(size_t) = GC_malloc_atomic; +static void (*do_free)(void *) = GC_free; + +static void *malloc_and_zero(size_t n) { + void *ptr = malloc(n); + + if(ptr) memset(ptr, 0, n); + return ptr; +} + +void mem_init(int gc) { + const char *e; + + if(!gc || ((e = getenv("DISORDER_GC")) && !strcmp(e, "no"))) { + do_malloc = malloc_and_zero; + do_malloc_atomic = malloc; + do_realloc = realloc; + do_free = free; + } else + GC_init(); +} + +void *xmalloc(size_t n) { + void *ptr; + + if(!(ptr = do_malloc(n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xrealloc(void *ptr, size_t n) { + if(!(ptr = do_realloc(ptr, n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xcalloc(size_t count, size_t size) { + if(count > SIZE_MAX / size) + fatal(0, "excessively large calloc"); + return xmalloc(count * size); +} + +void *xmalloc_noptr(size_t n) { + void *ptr; + + if(!(ptr = do_malloc_atomic(n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xrealloc_noptr(void *ptr, size_t n) { + if(ptr == 0) + return xmalloc_noptr(n); + if(!(ptr = do_realloc(ptr, n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +char *xstrdup(const char *s) { + char *t; + + if(!(t = do_malloc_atomic(strlen(s) + 1))) + fatal(errno, "error allocating memory"); + return strcpy(t, s); +} + +char *xstrndup(const char *s, size_t n) { + char *t; + + if(!(t = do_malloc_atomic(n + 1))) + fatal(errno, "error allocating memory"); + memcpy(t, s, n); + t[n] = 0; + return t; +} + +void xfree(void *ptr) { + do_free(ptr); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:fceaf81fe79b2f4f87c9541774a2b8f2 */ diff --git a/lib/mem.h b/lib/mem.h new file mode 100644 index 0000000..ab1529f --- /dev/null +++ b/lib/mem.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MEM_H +#define MEM_H + +#ifdef NO_MEMORY_ALLOCATION +# error including source file not allowed to perform memory allocation +#endif + +#include <stdarg.h> + +void mem_init(int gc); +/* initialize memory management. Set GC to 1 if garbage collection is + * desired. */ + +void *xmalloc(size_t); +void *xrealloc(void *, size_t); +void *xcalloc(size_t count, size_t size); +/* As malloc/realloc/calloc, but + * 1) succeed or call fatal + * 2) always clear (the unused part of) the new allocation + * 3) are garbage-collected + */ + +void *xmalloc_noptr(size_t); +void *xrealloc_noptr(void *, size_t); +char *xstrdup(const char *); +char *xstrndup(const char *, size_t); +/* As malloc/realloc/strdup, but + * 1) succeed or call fatal + * 2) are garbage-collected + * 3) allocated space must not contain any pointers + * + * {xmalloc,xrealloc}_noptr don't promise to clear the new space + */ + +void xfree(void *ptr); +/* As free, but calls GC_free instead if gc is enabled */ + +#endif /* MEM_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:333db053cab9e5ef91a151040d3256fb */ diff --git a/lib/mime.c b/lib/mime.c new file mode 100644 index 0000000..d80ae5d --- /dev/null +++ b/lib/mime.c @@ -0,0 +1,374 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <ctype.h> + +#include "mem.h" +#include "mime.h" +#include "vector.h" +#include "hex.h" + +static int whitespace(int c) { + switch(c) { + case ' ': + case '\t': + case '\r': + case '\n': + return 1; + default: + return 0; + } +} + +static int tspecial(int c) { + switch(c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + return 1; + default: + return 0; + } +} + +static const char *skipwhite(const char *s) { + int c, depth; + + for(;;) { + switch(c = *s) { + case ' ': + case '\t': + case '\r': + case '\n': + ++s; + break; + case '(': + ++s; + depth = 1; + while(*s && depth) { + c = *s++; + switch(c) { + case '(': ++depth; break; + case ')': --depth; break; + case '\\': + if(!*s) return 0; + ++s; + break; + } + } + if(depth) return 0; + break; + default: + return s; + } + } +} + +static const char *parsestring(const char *s, char **valuep) { + struct dynstr value; + int c; + + dynstr_init(&value); + ++s; + while((c = *s++) != '"') { + switch(c) { + case '\\': + if(!(c = *s++)) return 0; + default: + dynstr_append(&value, c); + break; + } + } + if(!c) return 0; + dynstr_terminate(&value); + *valuep = value.vec; + return s; +} + +int mime_content_type(const char *s, + char **typep, + char **parameternamep, + char **parametervaluep) { + struct dynstr type, parametername, parametervalue; + + dynstr_init(&type); + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&type, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '/') return -1; + dynstr_append(&type, '/'); + if(!(s = skipwhite(s))) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&type, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + + if(*s == ';') { + dynstr_init(¶metername); + ++s; + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metername, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '=') return -1; + if(!(s = skipwhite(s))) return -1; + if(*s == '"') { + if(!(s = parsestring(s, parametervaluep))) return -1; + } else { + dynstr_init(¶metervalue); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metervalue, *s++); + dynstr_terminate(¶metervalue); + *parametervaluep = parametervalue.vec; + } + if(!(s = skipwhite(s))) return -1; + dynstr_terminate(¶metername); + *parameternamep = parametername.vec; + } else + *parametervaluep = *parameternamep = 0; + dynstr_terminate(&type); + *typep = type.vec; + return 0; +} + +static int iscrlf(const char *ptr) { + return ptr[0] == '\r' && ptr[1] == '\n'; +} + +const char *mime_parse(const char *s, + int (*callback)(const char *name, const char *value, + void *u), + void *u) { + struct dynstr name, value; + char *cte = 0, *p; + + while(*s && !iscrlf(s)) { + dynstr_init(&name); + dynstr_init(&value); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&name, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return 0; + if(*s != ':') return 0; + ++s; + while(*s && !(*s == '\n' && !(s[1] == ' ' || s[1] == '\t'))) + dynstr_append(&value, *s++); + if(*s) ++s; + dynstr_terminate(&name); + dynstr_terminate(&value); + if(!strcmp(name.vec, "content-transfer-encoding")) { + cte = xstrdup(value.vec); + for(p = cte; *p; p++) + *p = tolower((unsigned char)*p); + } + if(callback(name.vec, value.vec, u)) return 0; + } + if(*s) s += 2; + if(cte) { + if(!strcmp(cte, "base64")) return mime_base64(s); + if(!strcmp(cte, "quoted-printable")) return mime_qp(s); + } + return s; +} + +static int isboundary(const char *ptr, const char *boundary, size_t bl) { + return (ptr[0] == '-' + && ptr[1] == '-' + && !strncmp(ptr + 2, boundary, bl) + && (iscrlf(ptr + bl + 2) + || (ptr[bl + 2] == '-' + && ptr[bl + 3] == '-' + && iscrlf(ptr + bl + 4)))); +} + +static int isfinal(const char *ptr, const char *boundary, size_t bl) { + return (ptr[0] == '-' + && ptr[1] == '-' + && !strncmp(ptr + 2, boundary, bl) + && ptr[bl + 2] == '-' + && ptr[bl + 3] == '-' + && iscrlf(ptr + bl + 4)); +} + +int mime_multipart(const char *s, + int (*callback)(const char *s, void *u), + const char *boundary, + void *u) { + size_t bl = strlen(boundary); + const char *start, *e; + int ret; + + if(!isboundary(s, boundary, bl)) return -1; + while(!isfinal(s, boundary, bl)) { + s = strstr(s, "\r\n") + 2; + start = s; + while(!isboundary(s, boundary, bl)) { + if(!(e = strstr(s, "\r\n"))) return -1; + s = e + 2; + } + if((ret = callback(xstrndup(start, + s == start ? 0 : s - start - 2), + u))) + return ret; + } + return 0; +} + +int mime_rfc2388_content_disposition(const char *s, + char **dispositionp, + char **parameternamep, + char **parametervaluep) { + struct dynstr disposition, parametername, parametervalue; + + dynstr_init(&disposition); + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&disposition, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + + if(*s == ';') { + dynstr_init(¶metername); + ++s; + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metername, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '=') return -1; + if(!(s = skipwhite(s))) return -1; + if(*s == '"') { + if(!(s = parsestring(s, parametervaluep))) return -1; + } else { + dynstr_init(¶metervalue); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metervalue, *s++); + dynstr_terminate(¶metervalue); + *parametervaluep = parametervalue.vec; + } + if(!(s = skipwhite(s))) return -1; + dynstr_terminate(¶metername); + *parameternamep = parametername.vec; + } else + *parametervaluep = *parameternamep = 0; + dynstr_terminate(&disposition); + *dispositionp = disposition.vec; + return 0; +} + +char *mime_qp(const char *s) { + struct dynstr d; + int c, a, b; + const char *t; + + dynstr_init(&d); + while((c = *s++)) { + switch(c) { + case '=': + if((a = unhexdigitq(s[0])) != -1 + && (b = unhexdigitq(s[1])) != -1) { + dynstr_append(&d, a * 16 + b); + s += 2; + } else { + t = s; + while(*t == ' ' || *t == '\t') ++t; + if(iscrlf(t)) { + /* soft line break */ + s = t + 2; + } else + return 0; + } + break; + case ' ': + case '\t': + t = s; + while(*t == ' ' || *t == '\t') ++t; + if(iscrlf(t)) + /* trailing space is always eliminated */ + s = t; + else + dynstr_append(&d, c); + break; + default: + dynstr_append(&d, c); + break; + } + } + dynstr_terminate(&d); + return d.vec; +} + +char *mime_base64(const char *s) { + struct dynstr d; + const char *t; + int b[4], n, c; + static const char table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + dynstr_init(&d); + n = 0; + while((c = (unsigned char)*s++)) { + if((t = strchr(table, c))) { + b[n++] = t - table; + if(n == 4) { + dynstr_append(&d, (b[0] << 2) + (b[1] >> 4)); + dynstr_append(&d, (b[1] << 4) + (b[2] >> 2)); + dynstr_append(&d, (b[2] << 6) + b[3]); + n = 0; + } + } else if(c == '=') { + if(n >= 2) { + dynstr_append(&d, (b[0] << 2) + (b[1] >> 4)); + if(n == 3) + dynstr_append(&d, (b[1] << 4) + (b[2] >> 2)); + } + break; + } + } + dynstr_terminate(&d); + return d.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:vz7S54dWl7x/NNx5kvg5cg */ diff --git a/lib/mime.h b/lib/mime.h new file mode 100644 index 0000000..14cb809 --- /dev/null +++ b/lib/mime.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MIME_H +#define MIME_H + +int mime_content_type(const char *s, + char **typep, + char **parameternamep, + char **parametervaluep); +/* Parse a content-type value. returns 0 on success, -1 on error. + * paramaternamep and parametervaluep are only set if present. + * type and parametername are forced to lower case. + */ + +const char *mime_parse(const char *s, + int (*callback)(const char *name, const char *value, + void *u), + void *u); +/* Parse a MIME message. Calls CALLBACK for each header field, then returns a + * pointer to the decoded body (might or might not point back into the original + * string). */ + +int mime_multipart(const char *s, + int (*callback)(const char *s, void *u), + const char *boundary, + void *u); +/* call CALLBACK with each part of multipart document [s,s+n) */ + +int mime_rfc2388_content_disposition(const char *s, + char **dispositionp, + char **parameternamep, + char **parametervaluep); +/* Parse an RFC2388-style content-disposition field */ + +char *mime_qp(const char *s); +char *mime_base64(const char *s); +/* convert quoted-printable or base64 data */ + +#endif /* MIME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:JMq56pGj+/kWY7mZDnHpWg */ diff --git a/lib/mixer.c b/lib/mixer.c new file mode 100644 index 0000000..5293ffd --- /dev/null +++ b/lib/mixer.c @@ -0,0 +1,114 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <stdio.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <stddef.h> +#include <sys/ioctl.h> + +#include "configuration.h" +#include "mixer.h" +#include "log.h" +#include "syscalls.h" + +#if HAVE_SYS_SOUNDCARD_H +#include <sys/soundcard.h> + +/* documentation does not match implementation! */ +#ifndef SOUND_MIXER_READ +# define SOUND_MIXER_READ(x) MIXER_READ(x) +#endif +#ifndef SOUND_MIXER_WRITE +# define SOUND_MIXER_WRITE(x) MIXER_WRITE(x) +#endif + +static const char *channels[] = SOUND_DEVICE_NAMES; + +int mixer_channel(const char *c) { + unsigned n; + + if(!c[strspn(c, "0123456789")]) + return atoi(c); + else { + for(n = 0; n < sizeof channels / sizeof *channels; ++n) + if(!strcmp(channels[n], c)) + return n; + return -1; + } +} + +int mixer_control(int *left, int *right, int set) { + int fd, ch, r; + + if(config->mixer + && config->channel + && (ch = mixer_channel(config->channel)) != -1) { + if((fd = open(config->mixer, O_RDWR, 0)) < 0) { + error(errno, "error opening %s", config->mixer); + return -1; + } + if(set) { + r = (*left & 0xff) + (*right & 0xff) * 256; + if(ioctl(fd, SOUND_MIXER_WRITE(ch), &r) == -1) { + error(errno, "error changing %s channel %s", + config->mixer, config->channel); + xclose(fd); + return -1; + } + } + if(ioctl(fd, SOUND_MIXER_READ(ch), &r) == -1) { + error(errno, "error reading %s channel %s", + config->mixer, config->channel); + xclose(fd); + return -1; + } + *left = r & 0xff; + *right = (r >> 8) & 0xff; + xclose(fd); + return 0; + } else + return -1; +} +#else +int mixer_channel(const char attribute((unused)) *c) { + return 0; +} + +int mixer_control(int attribute((unused)) *left, + int attribute((unused)) *right, + int attribute((unused)) set) { + error(0, "don't know how to set volume on this platform"); + return -1; +} +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2c047b85dbe68f45f2ebfc8c051ba967 */ diff --git a/lib/mixer.h b/lib/mixer.h new file mode 100644 index 0000000..99fbef2 --- /dev/null +++ b/lib/mixer.h @@ -0,0 +1,43 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MIXER_H +#define MIXER_H + +int mixer_channel(const char *c); +/* convert @c@ to a channel number, or return -1 if it does not match */ + +int mixer_control(int *left, int *right, int set); +/* get/set the current level. If @set@ is true then the level is set + * according to the pointers. In either case the eventual level is + * returned via the pointers. + * + * Returns 0 on success and -1 on error. + */ + +#endif /* MIXER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:d70a8b1bc1e79efcad02d20259246454 */ diff --git a/lib/plugin.c b/lib/plugin.c new file mode 100644 index 0000000..bf41797 --- /dev/null +++ b/lib/plugin.c @@ -0,0 +1,304 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <dlfcn.h> +#include <unistd.h> +#include <string.h> +#include <stdio.h> + +#include "plugin.h" +#include "configuration.h" +#include "log.h" +#include "mem.h" +#include "defs.h" +#include "disorder.h" +#include "printf.h" + +/* generic plugin support *****************************************************/ + +#ifndef SOSUFFIX +# define SOSUFFIX ".so" +#endif + +struct plugin { + struct plugin *next; + void *dlhandle; + const char *name; +}; + +static struct plugin *plugins; + +const struct plugin *open_plugin(const char *name, + unsigned flags) { + void *h = 0; + char *p; + int n; + struct plugin *pl; + + for(pl = plugins; pl && strcmp(pl->name, name); pl = pl->next) + ; + if(pl) return pl; + for(n = 0; n <= config->plugins.n; ++n) { + byte_xasprintf(&p, "%s/%s" SOSUFFIX, + n == config->plugins.n ? pkglibdir : config->plugins.s[n], + name); + if(access(p, R_OK) == 0) { + h = dlopen(p, RTLD_NOW); + if(!h) { + error(0, "error opening %s: %s", p, dlerror()); + continue; + } + pl = xmalloc(sizeof *pl); + pl->dlhandle = h; + pl->name = xstrdup(name); + pl->next = plugins; + plugins = pl; + return pl; + } + } + (flags & PLUGIN_FATAL ? fatal : error)(0, "cannot find plugin '%s'", name); + return 0; +} + +function_t *get_plugin_function(const struct plugin *pl, + const char *symbol) { + function_t *f; + const char *e; + + f = (function_t *)dlsym(pl->dlhandle, symbol); + if((e = dlerror())) + fatal(0, "error looking up function '%s' in '%s': %s",symbol, pl->name, e); + return f; +} + +const void *get_plugin_object(const struct plugin *pl, + const char *symbol) { + void *o; + const char *e; + + o = dlsym(pl->dlhandle, symbol); + if((e = dlerror())) + fatal(0, "error looking up object '%s' in '%s': %s", symbol, pl->name, e); + return o; +} + +/* specific plugin interfaces *************************************************/ + +typedef long tracklength_fn(const char *track, const char *path); + +long tracklength(const char *track, const char *path) { + static tracklength_fn *f = 0; + + if(!f) + f = (tracklength_fn *)get_plugin_function(open_plugin("tracklength", + PLUGIN_FATAL), + "disorder_tracklength"); + return (*f)(track, path); +} + +typedef void scan_fn(const char *root); + +void scan(const char *module, const char *root) { + ((scan_fn *)get_plugin_function(open_plugin(module, PLUGIN_FATAL), + "disorder_scan"))(root); +} + +typedef int check_fn(const char *root, const char *path); + + +int check(const char *module, const char *root, const char *path) { + return ((check_fn *)get_plugin_function(open_plugin(module, PLUGIN_FATAL), + "disorder_check"))(root, path); +} + +typedef void notify_play_fn(const char *track, const char *submitter); + +void notify_play(const char *track, + const char *submitter) { + static notify_play_fn *f; + + if(!f) + f = (notify_play_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_play"); + (*f)(track, submitter); +} + +typedef void notify_scratch_fn(const char *track, + const char *submitter, + const char *scratcher, + int seconds); + +void notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds) { + static notify_scratch_fn *f; + + if(!f) + f = (notify_scratch_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_scratch"); + (*f)(track, submitter, scratcher, seconds); +} + +typedef void notify_not_scratched_fn(const char *track, + const char *submitter); + +void notify_not_scratched(const char *track, + const char *submitter) { + static notify_not_scratched_fn *f; + + if(!f) + f = (notify_not_scratched_fn *)get_plugin_function + (open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_not_scratched"); + (*f)(track, submitter); +} + +typedef void notify_queue_fn(const char *track, + const char *submitter); + +void notify_queue(const char *track, + const char *submitter) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue"); + (*f)(track, submitter); +} + +void notify_queue_remove(const char *track, + const char *remover) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue_remove"); + (*f)(track, remover); +} + +void notify_queue_move(const char *track, + const char *mover) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue_move"); + (*f)(track, mover); +} + +void notify_pause(const char *track, const char *who) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_pause"); + (*f)(track, who); +} + +void notify_resume(const char *track, const char *who) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_resume"); + (*f)(track, who); +} + +/* player plugin interfaces ***************************************************/ + +/* get type */ + +unsigned long play_get_type(const struct plugin *pl) { + return *(const unsigned long *)get_plugin_object(pl, "disorder_player_type"); +} + +/* prefork */ + +typedef void *prefork_fn(const char *track); + +void *play_prefork(const struct plugin *pl, + const char *track) { + return ((prefork_fn *)get_plugin_function(pl, + "disorder_play_prefork"))(track); +} + +/* play */ + +typedef void play_track_fn(const char *const *parameters, + int nparameters, + const char *path, + const char *track); + +void play_track(const struct plugin *pl, + const char *const *parameters, + int nparameters, + const char *path, + const char *track) { + ((play_track_fn *)get_plugin_function(pl, + "disorder_play_track"))(parameters, + nparameters, + path, + track); +} + +/* cleanup */ + +typedef void cleanup_fn(void *data); + +void play_cleanup(const struct plugin *pl, void *data) { + ((cleanup_fn *)get_plugin_function(pl, "disorder_play_cleanup"))(data); +} + +/* pause */ + +typedef int pause_fn(long *playedp, void *data); + +int play_pause(const struct plugin *pl, long *playedp, void *data) { + return (((pause_fn *)get_plugin_function(pl, "disorder_pause_track")) + (playedp, data)); +} + +/* resume */ + +typedef void resume_fn(void *data); + +void play_resume(const struct plugin *pl, void *data) { + (((resume_fn *)get_plugin_function(pl, "disorder_resume_track")) + (data)); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:069494ccad9bf04cf6ca9505d9528f0a */ diff --git a/lib/plugin.h b/lib/plugin.h new file mode 100644 index 0000000..59cc936 --- /dev/null +++ b/lib/plugin.h @@ -0,0 +1,132 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef PLUGIN_H +#define PLUGIN_H + +/* general ********************************************************************/ + +struct plugin; + +typedef void *plugin_handle; +typedef void function_t(void); + +const struct plugin *open_plugin(const char *name, + unsigned flags); +#define PLUGIN_FATAL 0x0001 /* fatal() on error */ +/* Open a plugin. Returns a null pointer on error or a handle to it + * on success. */ + +function_t *get_plugin_function(const struct plugin *handle, + const char *symbol); +const void *get_plugin_object(const struct plugin *handle, + const char *symbol); +/* Look up a function or an object in a plugin */ + +/* track length computation ***************************************************/ + +long tracklength(const char *track, const char *path); +/* compute the length of the track. @track@ is the UTF-8 name of the + * track, @path@ is the file system name (or 0 for tracks that don't + * exist in the filesystem). The return value should be a positive + * number of seconds, 0 for unknown or -1 if an error occurred. */ + +/* collection interface *******************************************************/ + +void scan(const char *module, const char *root); +/* write a list of path names below @root@ to standard output. */ + +int check(const char *module, const char *root, const char *path); +/* Recheck a track, given its root and path name. Return 1 if it + * exists, 0 if it does not exist and -1 if an error occurred. */ + +/* notification interface *****************************************************/ + +void notify_play(const char *track, + const char *submitter); +/* we're going to play @track@. It was submitted by @submitter@ + * (might be a null pointer) */ + +void notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +/* @scratcher@ scratched @track@ after @seconds@. It was submitted by + * @submitter@ (might be a null pointer) */ + +void notify_not_scratched(const char *track, + const char *submitter); +/* @track@ (submitted by @submitter@, which might be a null pointer) + * was not scratched. */ + +void notify_queue(const char *track, + const char *submitter); +/* @track@ was queued by @submitter@ */ + +void notify_queue_remove(const char *track, + const char *remover); +/* @track@ removed from the queue by @remover@ (never a null pointer) */ + +void notify_queue_move(const char *track, + const char *mover); +/* @track@ moved in the queue by @mover@ (never a null pointer) */ + +void notify_pause(const char *track, + const char *pauser); +/* TRACK was paused by PAUSER (might be a null pointer) */ + +void notify_resume(const char *track, + const char *resumer); +/* TRACK was resumed by PAUSER (might be a null pointer) */ + +/* track playing **************************************************************/ + +unsigned long play_get_type(const struct plugin *pl); +/* Get the type word for this plugin */ + +void *play_prefork(const struct plugin *pl, + const char *track); +/* Call the prefork function for PL and return the user data */ + +void play_track(const struct plugin *pl, + const char *const *parameters, + int nparameters, + const char *path, + const char *track); +/* play a track. Called inside a fork. */ + +void play_cleanup(const struct plugin *pl, void *data); +/* Call the cleanup function for PL if necessary */ + +int play_pause(const struct plugin *pl, long *playedp, void *data); +/* Pause track. */ + +void play_resume(const struct plugin *pl, void *data); +/* Resume track. */ + +#endif /* PLUGIN_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4dbf7d07d493c66a58f6522f253287b6 */ diff --git a/lib/printf.c b/lib/printf.c new file mode 100644 index 0000000..aafe7ce --- /dev/null +++ b/lib/printf.c @@ -0,0 +1,494 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because byte_snprintf used from log.c */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> +#include <stdlib.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +enum flags { + f_thousands = 1, + f_left = 2, + f_sign = 4, + f_space = 8, + f_hash = 16, + f_zero = 32, + f_width = 256, + f_precision = 512 +}; + +enum lengths { + l_char = 1, + l_short, + l_long, + l_longlong, + l_size_t, + l_intmax_t, + l_ptrdiff_t, + l_longdouble +}; + +struct conversion; + +struct state { + struct sink *output; + int bytes; + va_list *ap; +}; + +struct specifier { + int ch; + int (*check)(const struct conversion *c); + int (*output)(struct state *s, struct conversion *c); + int base; + const char *digits; + const char *xform; +}; + +struct conversion { + unsigned flags; + int width; + int precision; + int length; + const struct specifier *specifier; +}; + +static const char flags[] = "'-+ #0"; + +/* write @nbytes@ to the output. Return -1 on error, 0 on success. + * Keeps track of the number of bytes written. */ +static int do_write(struct state *s, + const void *buffer, + int nbytes) { + if(s->bytes > INT_MAX - nbytes) { +#ifdef EOVERFLOW + errno = EOVERFLOW; +#endif + return -1; + } + if(s->output->write(s->output, buffer, nbytes) < 0) return -1; + s->bytes += nbytes; + return 0; +} + +/* write character @ch@ @n@ times, reasonably efficiently */ +static int do_pad(struct state *s, int ch, unsigned n) { + unsigned t; + const char *padding; + + switch(ch) { + case ' ': padding = " "; break; + case '0': padding = "00000000000000000000000000000000"; break; + default: abort(); + } + t = n / 32; + n %= 32; + while(t-- > 0) + if(do_write(s, padding, 32) < 0) return -1; + if(n > 0) + if(do_write(s, padding, n) < 0) return -1; + return 0; +} + +/* pick up the integer at @ptr@, returning it via @intp@. Return the + * number of characters consumed. Return 0 if there is no integer + * there and -1 if an error occurred (e.g. too big) */ +static int get_integer(int *intp, const char *ptr) { + long n; + char *e; + + errno = 0; + n = strtol(ptr, &e, 10); + if(errno || n > INT_MAX || n < INT_MIN || e == ptr) return -1; + *intp = n; + return e - ptr; +} + +/* consistency checks for various conversion specifications */ + +static int check_integer(const struct conversion *c) { + switch(c->length) { + case 0: + case l_char: + case l_short: + case l_long: + case l_longlong: + case l_intmax_t: + case l_size_t: + case l_longdouble: + return 0; + default: + return -1; + } +} + +static int check_string(const struct conversion *c) { + switch(c->length) { + case 0: + /* XXX don't support %ls, %lc */ + return 0; + default: + return -1; + } +} + +static int check_pointer(const struct conversion *c) { + if(c->length) return -1; + return 0; +} + +static int check_percent(const struct conversion *c) { + if(c->flags || c->width || c->precision || c->length) return -1; + return 0; +} + +/* output functions for various conversion specifications */ + +static int output_percent(struct state *s, + struct conversion attribute((unused)) *c) { + return do_write(s, "%", 1); +} + +static int output_integer(struct state *s, struct conversion *c) { + uintmax_t u; + intmax_t l; + char sign; + int base, dp, iszero, ndigits, prec, xform, sign_bytes, pad; + char digits[CHAR_BIT * sizeof (uintmax_t)]; /* overestimate */ + + switch(c->specifier->ch) { + default: + if(c->specifier->base < 0) { + switch(c->length) { + case 0: l = va_arg(*s->ap, int); break; + case l_char: l = (signed char)va_arg(*s->ap, int); break; + case l_short: l = (short)va_arg(*s->ap, int); break; + case l_long: l = va_arg(*s->ap, long); break; + case l_longlong: l = va_arg(*s->ap, long_long); break; + case l_intmax_t: l = va_arg(*s->ap, intmax_t); break; + case l_size_t: l = va_arg(*s->ap, ssize_t); break; + case l_ptrdiff_t: l = va_arg(*s->ap, ptrdiff_t); break; + default: abort(); + } + base = -c->specifier->base; + if(l < 0) { + u = -l; + sign = '-'; + } else { + u = l; + sign = 0; + } + } else { + switch(c->length) { + case 0: u = va_arg(*s->ap, unsigned int); break; + case l_char: u = (unsigned char)va_arg(*s->ap, unsigned int); break; + case l_short: u = (unsigned short)va_arg(*s->ap, unsigned int); break; + case l_long: u = va_arg(*s->ap, unsigned long); break; + case l_longlong: u = va_arg(*s->ap, u_long_long); break; + case l_intmax_t: u = va_arg(*s->ap, uintmax_t); break; + case l_size_t: u = va_arg(*s->ap, size_t); break; + case l_ptrdiff_t: u = va_arg(*s->ap, ptrdiff_t); break; + default: abort(); + } + base = c->specifier->base; + sign = 0; + } + break; + case 'p': + u = (uintptr_t)va_arg(*s->ap, void *); + c->flags |= f_hash; + base = c->specifier->base; + sign = 0; + break; + } + /* default precision */ + if(!(c->flags & f_precision)) + c->precision = 1; + /* enforce sign */ + if((c->flags & f_sign) && !sign) sign = '+'; + /* compute the digits */ + iszero = (u == 0); + dp = sizeof digits; + while(u) { + digits[--dp] = c->specifier->digits[u % base]; + u /= base; + } + ndigits = sizeof digits - dp; + /* alternative form */ + if(c->flags & f_hash) { + switch(base) { + case 8: + if((dp == sizeof digits || digits[dp] != '0') + && c->precision <= ndigits) + c->precision = ndigits + 1; + break; + } + if(!iszero && c->specifier->xform) + xform = strlen(c->specifier->xform); + else + xform = 0; + } else + xform = 0; + /* calculate number of 0s to add for precision */ + if(ndigits < c->precision) + prec = c->precision - ndigits; + else + prec = 0; + /* bytes occupied by the sign */ + if(sign) + sign_bytes = 1; + else + sign_bytes = 0; + /* XXX implement the ' ' flag */ + /* calculate number of bytes of padding */ + if(c->flags & f_width) { + if((pad = c->width - (ndigits + prec + xform + sign_bytes)) < 0) + pad = 0; + } else + pad = 0; + /* now we are ready to output. Possibilities are: + * [space pad][sign][xform][0 prec]digits + * [sign][xform][0 pad][0 prec]digits + * [sign][xform][0 prec]digits[space pad] + * + * '-' beats '0'. + */ + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + } else if(c->flags & f_zero) { + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(pad && do_pad(s, '0', pad) < 0) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + } else { + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; +} + +static int output_string(struct state *s, struct conversion *c) { + const char *str, *n; + int pad, len; + + str = va_arg(*s->ap, const char *); + if(c->flags & f_precision) { + if((n = memchr(str, 0, c->precision))) + len = n - str; + else + len = c->precision; + } else + len = strlen(str); + if(c->flags & f_width) { + if((pad = c->width - len) < 0) + pad = 0; + } else + pad = 0; + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(do_write(s, str, len) < 0) return -1; + } else { + if(do_write(s, str, len) < 0) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; + +} + +static int output_char(struct state *s, struct conversion *c) { + int pad; + char ch; + + ch = va_arg(*s->ap, int); + if(c->flags & f_width) { + if((pad = c->width - 1) < 0) + pad = 0; + } else + pad = 0; + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(do_write(s, &ch, 1) < 0) return -1; + } else { + if(do_write(s, &ch, 1) < 0) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; +} + +static int output_count(struct state *s, struct conversion *c) { + switch(c->length) { + case 0: *va_arg(*s->ap, int *) = s->bytes; break; + case l_char: *va_arg(*s->ap, signed char *) = s->bytes; break; + case l_short: *va_arg(*s->ap, short *) = s->bytes; break; + case l_long: *va_arg(*s->ap, long *) = s->bytes; break; + case l_longlong: *va_arg(*s->ap, long_long *) = s->bytes; break; + case l_intmax_t: *va_arg(*s->ap, intmax_t *) = s->bytes; break; + case l_size_t: *va_arg(*s->ap, ssize_t *) = s->bytes; break; + case l_ptrdiff_t: *va_arg(*s->ap, ptrdiff_t *) = s->bytes; break; + default: abort(); + } + return 0; +} + +/* table of conversion specifiers */ +static const struct specifier specifiers[] = { + /* XXX don't support floating point conversions */ + { '%', check_percent, output_percent, 0, 0, 0 }, + { 'X', check_integer, output_integer, 16, "0123456789ABCDEF", "0X" }, + { 'c', check_string, output_char, 0, 0, 0 }, + { 'd', check_integer, output_integer, -10, "0123456789", 0 }, + { 'i', check_integer, output_integer, -10, "0123456789", 0 }, + { 'n', check_integer, output_count, 0, 0, 0 }, + { 'o', check_integer, output_integer, 8, "01234567", 0 }, + { 'p', check_pointer, output_integer, 16, "0123456789abcdef", "0x" }, + { 's', check_string, output_string, 0, 0, 0 }, + { 'u', check_integer, output_integer, 10, "0123456789", 0 }, + { 'x', check_integer, output_integer, 16, "0123456789abcdef", "0x" }, +}; + +/* collect and check information about a conversion specification */ +static int parse_conversion(struct conversion *c, const char *ptr) { + int n, ch, l, r, m; + const char *q, *start = ptr; + + memset(c, 0, sizeof *c); + /* flags */ + while(*ptr && (q = strchr(flags, *ptr))) { + c->flags |= (1 << (q - flags)); + ++ptr; + } + /* minimum field width */ + if(*ptr >= '0' && *ptr <= '9') { + if((n = get_integer(&c->width, ptr)) < 0) return -1; + ptr += n; + c->flags |= f_width; + } else if(*ptr == '*') { + ++ptr; + c->width = -1; + c->flags |= f_width; + } + /* precision */ + if(*ptr == '.') { + ++ptr; + if(*ptr >= '0' && *ptr <= '9') { + if((n = get_integer(&c->precision, ptr)) < 0) return -1; + ptr += n; + } else if(*ptr == '*') { + ++ptr; + c->precision = -1; + } else + return -1; + c->flags |= f_precision; + } + /* length modifier */ + switch(ch = *ptr++) { + case 'h': + if((ch = *ptr++) == 'h') { c->length = l_char; ch = *ptr++; } + else c->length = l_short; + break; + case 'l': + if((ch = *ptr++) == 'l') { c->length = l_longlong; ch = *ptr++; } + else c->length = l_long; + break; + case 'q': c->length = l_longlong; ch = *ptr++; break; + case 'j': c->length = l_intmax_t; ch = *ptr++; break; + case 'z': c->length = l_size_t; ch = *ptr++; break; + case 't': c->length = l_ptrdiff_t; ch = *ptr++; break; + case 'L': c->length = l_longdouble; ch = *ptr++; break; + } + /* conversion specifier */ + l = 0; + r = sizeof specifiers / sizeof *specifiers; + while(l <= r && (specifiers[m = (l + r) / 2].ch != ch)) + if(ch < specifiers[m].ch) r = m - 1; + else l = m + 1; + if(specifiers[m].ch != ch) return -1; + if(specifiers[m].check(c)) return -1; + c->specifier = &specifiers[m]; + return ptr - start; +} + +/* ISO/IEC 9899:1999 7.19.6.1 */ +/* http://www.opengroup.org/onlinepubs/009695399/functions/fprintf.html */ + +int byte_vsinkprintf(struct sink *output, + const char *fmt, + va_list ap) { + int n; + const char *ptr; + struct state s; + struct conversion c; + + memset(&s, 0, sizeof s); + s.output = output; + s.ap = ≈ + while(*fmt) { + /* output text up to next conversion specification */ + for(ptr = fmt; *fmt && *fmt != '%'; ++fmt) + ; + if((n = fmt - ptr)) + if(do_write(&s, ptr, n) < 0) return -1; + if(!*fmt) + break; + ++fmt; + /* parse conversion */ + if((n = parse_conversion(&c, fmt)) < 0) return -1; + fmt += n; + /* fill in width and precision */ + if((c.flags & f_width) && c.width == -1) + if((c.width = va_arg(*s.ap, int)) < 0) { + c.width = -c.width; + c.flags |= f_left; + } + if((c.flags & f_precision) && c.precision == -1) + if((c.precision = va_arg(*s.ap, int)) < 0) + c.flags ^= f_precision; + /* generate the output */ + if(c.specifier->output(&s, &c) < 0) return -1; + } + return s.bytes; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e6ce806ce060f1d992eed14d9e5f0a6f */ diff --git a/lib/printf.h b/lib/printf.h new file mode 100644 index 0000000..209615d --- /dev/null +++ b/lib/printf.h @@ -0,0 +1,74 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef PRINTF_H +#define PRINTF_H + +struct sink; + +int byte_vsinkprintf(struct sink *output, + const char *fmt, + va_list ap); +/* partial printf implementation that takes ASCII format strings but + * arbitrary byte strings as args to %s and friends. Lots of bits are + * missing! */ + +int byte_vsnprintf(char buffer[], + size_t bufsize, + const char *fmt, + va_list ap); +int byte_snprintf(char buffer[], + size_t bufsize, + const char *fmt, + ...) + attribute((format (printf, 3, 4))); +/* analogues of [v]snprintf */ + +int byte_vasprintf(char **ptrp, + const char *fmt, + va_list ap); +int byte_asprintf(char **ptrp, + const char *fmt, + ...) + attribute((format (printf, 2, 3))); +/* analogues of [v]asprintf (uses xmalloc/xrealloc) */ + +int byte_xvasprintf(char **ptrp, + const char *fmt, + va_list ap); +int byte_xasprintf(char **ptrp, + const char *fmt, + ...) + attribute((format (printf, 2, 3))); +/* same but terminate on error */ + +int byte_vfprintf(FILE *fp, const char *fmt, va_list ap); +int byte_fprintf(FILE *fp, const char *fmt, ...) + attribute((format (printf, 2, 3))); +/* analogues of [v]fprintf */ + +#endif /* PRINTF_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a3e98b59150e46c340b0ce5f87274458 */ diff --git a/lib/queue.c b/lib/queue.c new file mode 100644 index 0000000..c7d32a4 --- /dev/null +++ b/lib/queue.c @@ -0,0 +1,512 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <time.h> +#include <stddef.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "mem.h" +#include "queue.h" +#include "log.h" +#include "configuration.h" +#include "split.h" +#include "syscalls.h" +#include "charset.h" +#include "table.h" +#include "inputline.h" +#include "printf.h" +#include "plugin.h" +#include "basen.h" +#include "eventlog.h" +#include "disorder.h" + +const char *playing_states[] = { + "failed", + "isscratch", + "no_player", + "ok", + "paused", + "quitting", + "random", + "scratched", + "started", + "unplayed" +}; + +/* the head of the queue is played next, so normally we add to the tail */ +struct queue_entry qhead = { &qhead, &qhead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +/* the head of the recent list is the oldest thing, the tail the most recently + * played */ +struct queue_entry phead = { &phead, &phead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +static long pcount; + +/* add new entry @n@ to a doubly linked list just after @b@ */ +static void l_add(struct queue_entry *b, struct queue_entry *n) { + n->prev = b; + n->next = b->next; + n->next->prev = n; + n->prev->next = n; +} + +/* remove an entry from a doubly-linked list */ +static void l_remove(struct queue_entry *node) { + node->next->prev = node->prev; + node->prev->next = node->next; +} + +#define VALUE(q, offset, type) *(type *)((char *)q + offset) + +static int unmarshall_long(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + if(xstrtol(&VALUE(q, offset, long), data, 0, 0)) { + error_handler(strerror(errno), u); + return -1; + } + return 0; +} + +static const char *marshall_long(const struct queue_entry *q, size_t offset) { + char buffer[256]; + int n; + + n = byte_snprintf(buffer, sizeof buffer, "%ld", VALUE(q, offset, long)); + if(n < 0) + fatal(errno, "error converting int"); + else if((size_t)n >= sizeof buffer) + fatal(0, "long converted to decimal is too long"); + return xstrdup(buffer); +} + +static int unmarshall_string(char *data, struct queue_entry *q, + size_t offset, + void attribute((unused)) (*error_handler)(const char *, void *), + void attribute((unused)) *u) { + VALUE(q, offset, char *) = data; + return 0; +} + +static const char *marshall_string(const struct queue_entry *q, size_t offset) { + return VALUE(q, offset, char *); +} + +static int unmarshall_time_t(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + long_long ul; + + if(xstrtoll(&ul, data, 0, 0)) { + error_handler(strerror(errno), u); + return -1; + } + VALUE(q, offset, time_t) = ul; + return 0; +} + +static const char *marshall_time_t(const struct queue_entry *q, size_t offset) { + char buffer[256]; + int n; + + n = byte_snprintf(buffer, sizeof buffer, + "%"PRIdMAX, (intmax_t)VALUE(q, offset, time_t)); + if(n < 0) + fatal(errno, "error converting time"); + else if((size_t)n >= sizeof buffer) + fatal(0, "time converted to decimal is too long"); + return xstrdup(buffer); +} + +static int unmarshall_state(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + int n; + + if((n = table_find(playing_states, 0, sizeof (char *), + sizeof playing_states / sizeof *playing_states, + data)) < 0) { + D(("state=[%s] n=%d", data, n)); + error_handler("invalid state", u); + return -1; + } + VALUE(q, offset, enum playing_state) = n; + return 0; +} + +static const char *marshall_state(const struct queue_entry *q, size_t offset) { + return playing_states[VALUE(q, offset, enum playing_state)]; +} + +#define F(n, h) { #n, offsetof(struct queue_entry, n), marshall_##h, unmarshall_##h } + +static const struct field { + const char *name; + size_t offset; + const char *(*marshall)(const struct queue_entry *q, size_t offset); + int (*unmarshall)(char *data, struct queue_entry *q, size_t offset, + void (*error_handler)(const char *, void *), + void *u); +} fields[] = { + /* Keep this table sorted. */ + F(expected, time_t), + F(id, string), + F(played, time_t), + F(scratched, string), + F(sofar, long), + F(state, state), + F(submitter, string), + F(track, string), + F(when, time_t), + F(wstat, long) +}; + +int queue_unmarshall(struct queue_entry *q, const char *s, + void (*error_handler)(const char *, void *), + void *u) { + char **vec; + int nvec; + + if(!(vec = split(s, &nvec, SPLIT_QUOTES, error_handler, u))) + return -1; + return queue_unmarshall_vec(q, nvec, vec, error_handler, u); +} + +int queue_unmarshall_vec(struct queue_entry *q, int nvec, char **vec, + void (*error_handler)(const char *, void *), + void *u) { + int n; + + if(nvec % 2 != 0) { + error_handler("invalid marshalled queue format", u); + return -1; + } + while(*vec) { + D(("key %s value %s", vec[0], vec[1])); + if((n = TABLE_FIND(fields, struct field, name, *vec)) < 0) { + error_handler("unknown key in queue data", u); + return -1; + } else { + if(fields[n].unmarshall(vec[1], q, fields[n].offset, error_handler, u)) + return -1; + } + vec += 2; + } + return 0; +} + +void queue_fix_sofar(struct queue_entry *q) { + long sofar; + + /* Fake up SOFAR field for currently-playing tracks that don't have it filled + * in by the speaker process. XXX this horrible bodge should go away when we + * have a more general implementation of pausing as that field will always + * have to be right for the playing track. */ + if((q->state == playing_started + || q->state == playing_paused) + && q->type & DISORDER_PLAYER_PAUSES + && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW) { + if(q->lastpaused) { + if(q->uptopause == -1) /* Don't know how far thru. */ + sofar = -1; + else if(q->lastresumed) /* Has been paused and resumed. */ + sofar = q->uptopause + time(0) - q->lastresumed; + else /* Currently paused. */ + sofar = q->uptopause; + } else /* Never been paused. */ + sofar = time(0) - q->played; + q->sofar = sofar; + } +} + +char *queue_marshall(const struct queue_entry *q) { + unsigned n; + const char *vec[sizeof fields / sizeof *fields], *v; + char *r, *s; + size_t len = 1; + + for(n = 0; n < sizeof fields / sizeof *fields; ++n) + if((v = fields[n].marshall(q, fields[n].offset))) { + vec[n] = quoteutf8(v); + len += strlen(vec[n]) + strlen(fields[n].name) + 2; + } else + vec[n] = 0; + s = r = xmalloc_noptr(len); + for(n = 0; n < sizeof fields / sizeof *fields; ++n) + if(vec[n]) { + *s++ = ' '; + s += strlen(strcpy(s, fields[n].name)); + *s++ = ' '; + s += strlen(strcpy(s, vec[n])); + } + return r; +} + +static void queue_read_error(const char *msg, + void *u) { + fatal(0, "error parsing queue %s: %s", (const char *)u, msg); +} + +static void queue_do_read(struct queue_entry *head, const char *path) { + char *buffer; + FILE *fp; + struct queue_entry *q; + + if(!(fp = fopen(path, "r"))) { + if(errno == ENOENT) + return; /* no queue */ + fatal(errno, "error opening %s", path); + } + head->next = head->prev = head; + while(!inputline(path, fp, &buffer, '\n')) { + q = xmalloc(sizeof *q); + queue_unmarshall(q, buffer, queue_read_error, (void *)path); + if(head == &qhead + && (!q->track + || !q->when)) + fatal(0, "incomplete queue entry in %s", path); + l_add(head->prev, q); + } + if(ferror(fp)) fatal(errno, "error reading %s", path); + fclose(fp); +} + +void queue_read(void) { + queue_do_read(&qhead, config_get_file("queue")); +} + +void recent_read(void) { + struct queue_entry *q; + + queue_do_read(&phead, config_get_file("recent")); + /* reset pcount after loading */ + pcount = 0; + q = phead.next; + while(q != &phead) { + ++pcount; + q = q->next; + } +} + +static void queue_do_write(const struct queue_entry *head, const char *path) { + char *tmp; + FILE *fp; + struct queue_entry *q; + + byte_xasprintf(&tmp, "%s.new", path); + if(!(fp = fopen(tmp, "w"))) fatal(errno, "error opening %s", tmp); + for(q = head->next; q != head; q = q->next) + if(fprintf(fp, "%s\n", queue_marshall(q)) < 0) + fatal(errno, "error writing %s", tmp); + if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp); + if(rename(tmp, path) < 0) fatal(errno, "error replacing %s", path); +} + +void queue_write(void) { + queue_do_write(&qhead, config_get_file("queue")); +} + +void recent_write(void) { + queue_do_write(&phead, config_get_file("recent")); +} + +void queue_id(struct queue_entry *q) { + static unsigned long serial; + unsigned long a[3]; + char buffer[128]; + + a[0] = serial++ & 0xFFFFFFFFUL; + a[1] = time(0) & 0xFFFFFFFFUL; + a[2] = getpid() & 0xFFFFFFFFUL; + basen(a, 3, buffer, sizeof buffer, 62); + q->id = xstrdup(buffer); +} + +struct queue_entry *queue_add(const char *track, const char *submitter, + int where) { + struct queue_entry *q; + + q = xmalloc(sizeof *q); + q->track = xstrdup(track); + q->submitter = submitter ? xstrdup(submitter) : 0; + q->state = playing_unplayed; + queue_id(q); + time(&q->when); + switch(where) { + case WHERE_START: + l_add(&qhead, q); + break; + case WHERE_END: + l_add(qhead.prev, q); + break; + case WHERE_BEFORE_RANDOM: + if(qhead.prev == &qhead /* Empty queue. */ + || qhead.prev->state != playing_random) /* No random track */ + l_add(qhead.prev, q); + else + l_add(qhead.prev->prev, q); /* Before random track. */ + break; + } + /* submitter will be a null pointer for a scratch */ + if(submitter) + notify_queue(track, submitter); + eventlog_raw("queue", queue_marshall(q), (const char *)0); + return q; +} + +int queue_move(struct queue_entry *q, int delta, const char *who) { + int moved = 0; + char buffer[20]; + + /* not the most efficient approach but hopefuly relatively comprehensible: + * the idea is that for each step we determine which nodes are affected, and + * fill in all the links starting at the 'prev' end and moving towards the + * 'next' end. */ + + while(delta > 0 && q->prev != &qhead) { + struct queue_entry *n, *p, *pp; + + n = q->next; + p = q->prev; + pp = p->prev; + pp->next = q; + q->prev = pp; + q->next = p; + p->prev = q; + p->next = n; + n->prev = p; + --delta; + ++moved; + } + + while(delta < 0 && q->next != &qhead) { + struct queue_entry *n, *p, *nn; + + p = q->prev; + n = q->next; + nn = n->next; + p->next = n; + n->prev = p; + n->next = q; + q->prev = n; + q->next = nn; + nn->prev = q; + ++delta; + --moved; + } + + if(moved) { + info("user %s moved %s", who, q->id); + notify_queue_move(q->track, who); + sprintf(buffer, "%d", moved); + eventlog("moved", who, (char *)0); + } + + return delta; +} + +static int find_in_list(struct queue_entry *needle, + int nqs, struct queue_entry **qs) { + int n; + + for(n = 0; n < nqs; ++n) + if(qs[n] == needle) + return 1; + return 0; +} + +void queue_moveafter(struct queue_entry *target, + int nqs, struct queue_entry **qs, + const char *who) { + struct queue_entry *q; + int n; + + /* Normalize */ + if(!target) + target = &qhead; + else + while(find_in_list(target, nqs, qs)) + target = target->prev; + /* Do the move */ + for(n = 0; n < nqs; ++n) { + q = qs[n]; + l_remove(q); + l_add(target, q); + target = q; + /* Log the individual tracks */ + info("user %s moved %s", who, q->id); + notify_queue_move(q->track, who); + } + /* Report that the queue changed to the event log */ + eventlog("moved", who, (char *)0); +} + +void queue_remove(struct queue_entry *which, const char *who) { + if(who) { + info("user %s removed %s", who, which->id); + notify_queue_move(which->track, who); + } + eventlog("removed", which->id, who, (const char *)0); + l_remove(which); +} + +struct queue_entry *queue_find(const char *key) { + struct queue_entry *q; + + for(q = qhead.next; + q != &qhead && strcmp(q->track, key) && strcmp(q->id, key); + q = q->next) + ; + return q != &qhead ? q : 0; +} + +void queue_played(struct queue_entry *q) { + while(pcount && pcount >= config->history) { + eventlog("recent_removed", phead.next->id, (char *)0); + l_remove(phead.next); + pcount--; + } + if(config->history) { + eventlog_raw("recent_added", queue_marshall(q), (char *)0); + l_add(phead.prev, q); + ++pcount; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:62474dd3e75e5483b61b6befe2c523cc */ diff --git a/lib/queue.h b/lib/queue.h new file mode 100644 index 0000000..5bf1587 --- /dev/null +++ b/lib/queue.h @@ -0,0 +1,137 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef QUEUE_H +#define QUEUE_H + +enum playing_state { + playing_failed, /* failed to play */ + playing_isscratch, /* this is a scratch track */ + playing_no_player, /* couldn't find a player */ + playing_ok, /* played OK */ + playing_paused, /* started but paused */ + playing_quitting, /* interrupt because server quit */ + playing_random, /* unplayed randomly chosen track */ + playing_scratched, /* was scratched */ + playing_started, /* started to play */ + playing_unplayed /* haven't played this track yet */ +}; + +extern const char *playing_states[]; + +/* queue entries form a circular doubly-linked list */ +struct queue_entry { + struct queue_entry *next; /* next entry */ + struct queue_entry *prev; /* previous entry */ + const char *track; /* path to track */ + const char *submitter; /* name of submitter */ + time_t when; /* time submitted */ + time_t played; /* when played */ + enum playing_state state; /* state */ + long wstat; /* wait status */ + const char *scratched; /* scratched by */ + const char *id; /* queue entry ID */ + time_t expected; /* expected started time */ + /* for playing or soon-to-be-played tracks only: */ + unsigned long type; /* type word from plugin */ + const struct plugin *pl; /* plugin that's playing this track */ + void *data; /* player data */ + long sofar; /* how much played so far */ + /* For DISORDER_PLAYER_PAUSES only: */ + time_t lastpaused, lastresumed; /* when last paused/resumed, or 0 */ + long uptopause; /* how much played up to last pause */ + /* For Disobedience */ + struct queuelike *ql; /* owning queue */ +}; + +extern struct queue_entry qhead; +/* queue of things yet to be played. the head will be played + * soonest. */ + +extern struct queue_entry phead; +/* things that have been played in the past. the head is the oldest. */ + +void queue_read(void); +/* read the queue in. Calls @fatal@ on error. */ + +void queue_write(void); +/* write the queue out. Calls @fatal@ on error. */ + +void recent_read(void); +/* read the recently played list in. Calls @fatal@ on error. */ + +void recent_write(void); +/* write the recently played list out. Calls @fatal@ on error. */ + +struct queue_entry *queue_add(const char *track, const char *submitter, + int where); +#define WHERE_START 0 /* Add to head of queue */ +#define WHERE_END 1 /* Add to end of queue */ +#define WHERE_BEFORE_RANDOM 2 /* End, or before random track */ +/* add an entry to the queue. Return a pointer to the new entry. */ + +void queue_remove(struct queue_entry *q, const char *who); +/* remove an from the queue */ + +struct queue_entry *queue_find(const char *key); +/* find a track in the queue by name or ID */ + +void queue_played(struct queue_entry *q); +/* add @q@ to the played list */ + +int queue_unmarshall(struct queue_entry *q, const char *s, + void (*error_handler)(const char *, void *), + void *u); +/* unmarshall UTF-8 string @s@ into @q@ */ + +int queue_unmarshall_vec(struct queue_entry *q, int nvec, char **vec, + void (*error_handler)(const char *, void *), + void *u); +/* unmarshall pre-split string @vec@ into @q@ */ + +char *queue_marshall(const struct queue_entry *q); +/* marshall @q@ into a UTF-8 string */ + +void queue_id(struct queue_entry *q); +/* give @q@ an ID */ + +int queue_move(struct queue_entry *q, int delta, const char *who); +/* move element @q@ in the queue towards the front (@delta@ > 0) or towards the + * back (@delta@ < 0). The return value is the leftover delta once we've hit + * the end in whichever direction we were going. */ + +void queue_moveafter(struct queue_entry *target, + int nqs, struct queue_entry **qs, const char *who); +/* Move all the elements QS to just after TARGET, or to the head if + * TARGET=0. */ + +void queue_fix_sofar(struct queue_entry *q); +/* Fix up the sofar field for standalone players */ + +#endif /* QUEUE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:23ec4c111fdd6573a0adc8c366b87e7b */ diff --git a/lib/regsub.c b/lib/regsub.c new file mode 100644 index 0000000..bad6034 --- /dev/null +++ b/lib/regsub.c @@ -0,0 +1,166 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <pcre.h> + +#include "regsub.h" +#include "mem.h" +#include "vector.h" +#include "log.h" + +#define PREMATCH (-1) /* fictitious pre-match substring */ +#define POSTMATCH (-2) /* fictitious post-match substring */ + +static inline int substring_start(const char attribute((unused)) *subject, + const int *ovector, + int n) { + switch(n) { + case PREMATCH: return 0; + case POSTMATCH: return ovector[1]; + default: return ovector[2 * n]; + } +} + +static inline int substring_end(const char *subject, + const int *ovector, + int n) { + switch(n) { + case PREMATCH: return ovector[0]; + case POSTMATCH: return strlen(subject); + default: return ovector[2 * n + 1]; + } +} + +static void transform_append(struct dynstr *d, + const char *subject, + const int *ovector, + int n) { + int start = substring_start(subject, ovector, n); + int end = substring_end(subject, ovector, n); + + if(start != -1) + dynstr_append_bytes(d, subject + start, end - start); +} + +static void replace_core(struct dynstr *d, + const char *subject, + const char *replace, + int rc, + const int *ovector) { + int substr; + + while(*replace) { + if(*replace == '$') + switch(replace[1]) { + case '&': + transform_append(d, subject, ovector, 0); + replace += 2; + break; + case '1': case '2': case '3': + case '4': case '5': case '6': + case '7': case '8': case '9': + substr = replace[1] - '0'; + if(substr < rc) + transform_append(d, subject, ovector, substr); + replace += 2; + break; + case '$': + dynstr_append(d, '$'); + replace += 2; + break; + default: + dynstr_append(d, *replace++); + break; + } + else + dynstr_append(d, *replace++); + } +} + +unsigned regsub_flags(const char *flags) { + unsigned f = 0; + + while(*flags) { + switch(*flags++) { + case 'g': f |= REGSUB_GLOBAL; break; + case 'i': f |= REGSUB_CASE_INDEPENDENT; break; + default: break; + } + } + return f; +} + +int regsub_compile_options(unsigned flags) { + int options = 0; + + if(flags & REGSUB_CASE_INDEPENDENT) + options |= PCRE_CASELESS; + return options; +} + +const char *regsub(const pcre *re, const char *subject, const char *replace, + unsigned flags) { + int rc, ovector[99], matches; + struct dynstr d; + + dynstr_init(&d); + matches = 0; + /* find the next match */ + while((rc = pcre_exec(re, 0, subject, strlen(subject), 0, + 0, ovector, sizeof ovector / sizeof (int))) > 0) { + /* text just before the match */ + if(!(flags & REGSUB_REPLACE)) + transform_append(&d, subject, ovector, PREMATCH); + /* the replacement text */ + replace_core(&d, subject, replace, rc, ovector); + ++matches; + if(!*subject) /* end of subject */ + break; + if(flags & REGSUB_REPLACE) /* replace subject entirely */ + break; + /* step over the matched substring */ + subject += substring_start(subject, ovector, POSTMATCH); + if(!(flags & REGSUB_GLOBAL)) + break; + } + if(rc <= 0 && rc != PCRE_ERROR_NOMATCH) { + error(0, "pcre_exec returned %d, subject '%s'", rc, subject); + return 0; + } + if((flags & REGSUB_MUST_MATCH) && matches == 0) + return 0; + /* append the remainder of the subject */ + if(!(flags & REGSUB_REPLACE)) + dynstr_append_string(&d, subject); + dynstr_terminate(&d); + return d.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:69b3b3dafcbaa25619a77a33c44ad699 */ diff --git a/lib/regsub.h b/lib/regsub.h new file mode 100644 index 0000000..683a85b --- /dev/null +++ b/lib/regsub.h @@ -0,0 +1,46 @@ +/* + * This file is part of DisOrder +* Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef REGSUB_H +#define REGSUB_H + +#define REGSUB_GLOBAL 0x0001 /* global replace */ +#define REGSUB_MUST_MATCH 0x0002 /* return 0 if no match */ +#define REGSUB_CASE_INDEPENDENT 0x0004 /* case independent */ +#define REGSUB_REPLACE 0x0008 /* replace whole, not match */ + +unsigned regsub_flags(const char *flags); +/* parse a flag string */ + +int regsub_compile_options(unsigned flags); +/* convert compile-time options */ + +const char *regsub(const pcre *re, const char *subject, const char *replace, + unsigned flags); + +#endif /* REGSUB_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:cc77c5fd9a947d224b59167ed9eaa360 */ diff --git a/lib/selection.c b/lib/selection.c new file mode 100644 index 0000000..ad46cb3 --- /dev/null +++ b/lib/selection.c @@ -0,0 +1,87 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "mem.h" +#include "hash.h" +#include "selection.h" + +hash *selection_new(void) { + return hash_new(sizeof (int)); +} + +void selection_set(hash *h, const char *key, int selected) { + if(selected) + hash_add(h, key, xmalloc_noptr(sizeof (int)), HASH_INSERT_OR_REPLACE); + else + hash_remove(h, key); +} + +int selection_selected(hash *h, const char *key) { + return hash_find(h, key) != 0; +} + +void selection_flip(hash *h, const char *key) { + selection_set(h, key, !selection_selected(h, key)); +} + +void selection_live(hash *h, const char *key) { + int *ptr = hash_find(h, key); + + if(ptr) + *ptr = 1; +} + +static int selection_cleanup_callback(const char *key, + void *value, + void *v) { + if(*(int *)value) + *(int *)value = 0; + else + hash_remove((hash *)v, key); + return 0; +} + +void selection_cleanup(hash *h) { + hash_foreach(h, selection_cleanup_callback, h); +} + +static int selection_empty_callback(const char *key, + void attribute((unused)) *value, + void *v) { + hash_remove((hash *)v, key); + return 0; +} + +void selection_empty(hash *h) { + hash_foreach(h, selection_empty_callback, h); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:ZUNwKZppAUsZ2KQ9yLedIg */ diff --git a/lib/selection.h b/lib/selection.h new file mode 100644 index 0000000..b358eb3 --- /dev/null +++ b/lib/selection.h @@ -0,0 +1,56 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SELECTION_H +#define SELECTION_H + +/* Represent a selection using a hash */ + +hash *selection_new(void); + +void selection_set(hash *h, const char *key, int selected); +/* Set the selection status of KEY */ + +void selection_flip(hash *h, const char *key); +/* Flip the selection status of KEY */ + +int selection_selected(hash *h, const char *key); +/* Test whether KEY is selected. Always returns 0 or 1. */ + +void selection_live(hash *h, const char *key); +/* Mark KEY as live. Ignored if KEY is not selected. */ + +void selection_cleanup(hash *h); +/* Discard dead items (and mark everything left as dead) */ + +void selection_empty(hash *h); +/* Empty the selection */ + +#endif /* SELECTION_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:es7QFEo6xhzYHZ6+Ejos/A */ diff --git a/lib/signame.c b/lib/signame.c new file mode 100644 index 0000000..19e0662 --- /dev/null +++ b/lib/signame.c @@ -0,0 +1,154 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <signal.h> +#include <stddef.h> + +#include "table.h" +#include "signame.h" + +static const struct sigtable { + int signal; + const char *name; +} signals[] = { +#define S(sig) { sig, #sig } + /* table must be kept in lexical order */ +#ifdef SIGABRT + S(SIGABRT), +#endif +#ifdef SIGALRM + S(SIGALRM), +#endif +#ifdef SIGBUS + S(SIGBUS), +#endif +#ifdef SIGCHLD + S(SIGCHLD), +#endif +#ifdef SIGCONT + S(SIGCONT), +#endif +#ifdef SIGFPE + S(SIGFPE), +#endif +#ifdef SIGHUP + S(SIGHUP), +#endif +#ifdef SIGILL + S(SIGILL), +#endif +#ifdef SIGINT + S(SIGINT), +#endif +#ifdef SIGIO + S(SIGIO), +#endif +#ifdef SIGIOT + S(SIGIOT), +#endif +#ifdef SIGKILL + S(SIGKILL), +#endif +#ifdef SIGPIPE + S(SIGPIPE), +#endif +#ifdef SIGPOLL + S(SIGPOLL), +#endif +#ifdef SIGPROF + S(SIGPROF), +#endif +#ifdef SIGPWR + S(SIGPWR), +#endif +#ifdef SIGQUIT + S(SIGQUIT), +#endif +#ifdef SIGSEGV + S(SIGSEGV), +#endif +#ifdef SIGSTKFLT + S(SIGSTKFLT), +#endif +#ifdef SIGSTOP + S(SIGSTOP), +#endif +#ifdef SIGSYS + S(SIGSYS), +#endif +#ifdef SIGTERM + S(SIGTERM), +#endif +#ifdef SIGTRAP + S(SIGTRAP), +#endif +#ifdef SIGTSTP + S(SIGTSTP), +#endif +#ifdef SIGTTIN + S(SIGTTIN), +#endif +#ifdef SIGTTOU + S(SIGTTOU), +#endif +#ifdef SIGURG + S(SIGURG), +#endif +#ifdef SIGUSR1 + S(SIGUSR1), +#endif +#ifdef SIGUSR2 + S(SIGUSR2), +#endif +#ifdef SIGVTALRM + S(SIGVTALRM), +#endif +#ifdef SIGWINCH + S(SIGWINCH), +#endif +#ifdef SIGXCPU + S(SIGXCPU), +#endif +#ifdef SIGXFSZ + S(SIGXFSZ), +#endif +#undef S +}; + +int find_signal(const char *s) { + int n; + + if((n = TABLE_FIND(signals, struct sigtable, name, s)) < 0) + return -1; + return signals[n].signal; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:6D5fEI6CzWvDXNVeX/1qDg */ diff --git a/lib/signame.h b/lib/signame.h new file mode 100644 index 0000000..a4d74d4 --- /dev/null +++ b/lib/signame.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SIGNAME_H +#define SIGNAME_H + +int find_signal(const char *s); +/* Map S to a signal number */ + +#endif /* SIGNAME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:/w7s28ztfuoDjF8iE/rT3A */ diff --git a/lib/sink.c b/lib/sink.c new file mode 100644 index 0000000..e54533a --- /dev/null +++ b/lib/sink.c @@ -0,0 +1,105 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> + +#include "mem.h" +#include "vector.h" +#include "sink.h" +#include "log.h" +#include "printf.h" + +int sink_vprintf(struct sink *s, const char *fmt, va_list ap) { + return byte_vsinkprintf(s, fmt, ap); +} + +int sink_printf(struct sink *s, const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = byte_vsinkprintf(s, fmt, ap); + va_end(ap); + return n; +} + +/* stdio sink *****************************************************************/ + +struct stdio_sink { + struct sink s; + const char *name; + FILE *fp; +}; + +#define S(s) ((struct stdio_sink *)s) + +static int sink_stdio_write(struct sink *s, const void *buffer, int nbytes) { + int n = fwrite(buffer, 1, nbytes, S(s)->fp); + if(n < nbytes) { + if(S(s)->name) + fatal(errno, "error writing to %s", S(s)->name); + else + return -1; + } + return n; +} + +struct sink *sink_stdio(const char *name, FILE *fp) { + struct stdio_sink *s = xmalloc(sizeof *s); + + s->s.write = sink_stdio_write; + s->name = name; + s->fp = fp; + return (struct sink *)s; +} + +/* dynstr sink ****************************************************************/ + +struct dynstr_sink { + struct sink s; + struct dynstr *d; +}; + +static int sink_dynstr_write(struct sink *s, const void *buffer, int nbytes) { + dynstr_append_bytes(((struct dynstr_sink *)s)->d, buffer, nbytes); + return nbytes; +} + +struct sink *sink_dynstr(struct dynstr *output) { + struct dynstr_sink *s = xmalloc(sizeof *s); + + s->s.write = sink_dynstr_write; + s->d = output; + return (struct sink *)s; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:10f08a9ea316137ef7259088403a636e */ diff --git a/lib/sink.h b/lib/sink.h new file mode 100644 index 0000000..f19a0c6 --- /dev/null +++ b/lib/sink.h @@ -0,0 +1,67 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SINK_H +#define SINK_H + +/* a sink is something you write to (the opposite would be a source) */ + +struct dynstr; + +struct sink { + int (*write)(struct sink *s, const void *buffer, int nbytes); + /* return >= 0 on success, -1 on error */ +}; + +struct sink *sink_stdio(const char *name, FILE *fp); +/* return a sink which writes to @fp@. If @name@ is not a null + * pointer, it will be used in (fatal) error messages; if it is a null + * pointer then errors will be signalled by returning -1. */ + +struct sink *sink_dynstr(struct dynstr *output); +/* return a sink which appends to @output@. */ + +int sink_vprintf(struct sink *s, const char *fmt, va_list ap); +int sink_printf(struct sink *s, const char *fmt, ...) + attribute((format (printf, 2, 3))); +/* equivalent of vfprintf/fprintf for sink @s@ */ + +static inline int sink_write(struct sink *s, const void *buffer, int nbytes) { + return s->write(s, buffer, nbytes); +} + +static inline int sink_writes(struct sink *s, const char *str) { + return s->write(s, str, strlen(str)); +} + +static inline int sink_writec(struct sink *s, char c) { + return s->write(s, &c, 1); +} + +#endif /* SINK_H */ + + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2f4f2c2a4e65aef8f7c59b4785121083 */ diff --git a/lib/snprintf.c b/lib/snprintf.c new file mode 100644 index 0000000..f16b047 --- /dev/null +++ b/lib/snprintf.c @@ -0,0 +1,97 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because used from log.c */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +struct fixedstr_sink { + struct sink s; + char *buffer; + int nbytes; + size_t size; +}; + +static int fixedstr_write(struct sink *f, const void *buffer, int nbytes) { + struct fixedstr_sink *s = (struct fixedstr_sink *)f; + int count; + + if((size_t)s->nbytes < s->size) { + if((size_t)nbytes > s->size - s->nbytes) + count = s->size - s->nbytes; + else + count = nbytes; + memcpy(s->buffer + s->nbytes, buffer, count); + } + s->nbytes += nbytes; + return 0; +} + +int byte_vsnprintf(char buffer[], + size_t bufsize, + const char *fmt, + va_list ap) { + struct fixedstr_sink s; + int n, m; + + /* We have to make a sink directly here, since we can't safely do memory + * allocation here (we might be formatting the error message from a failed + * memory allocation) */ + s.s.write = fixedstr_write; + s.buffer = buffer; + s.nbytes = 0; + s.size = bufsize; + n = byte_vsinkprintf(&s.s, fmt, ap); + if(bufsize) { + /* add the null terminator (even if the printf failed) */ + m = s.nbytes; + if((size_t)m >= bufsize) m = bufsize - 1; + buffer[m] = 0; + } + return n; +} + +int byte_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vsnprintf(buffer, bufsize, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4f73357e08ed449f25b5b2c5a6b9ada8 */ diff --git a/lib/speaker.c b/lib/speaker.c new file mode 100644 index 0000000..401bbe5 --- /dev/null +++ b/lib/speaker.c @@ -0,0 +1,109 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/socket.h> +#include <string.h> +#include <errno.h> +#include <sys/uio.h> + +#include "speaker.h" +#include "log.h" + +void speaker_send(int fd, const struct speaker_message *sm, int datafd) { + struct msghdr m; + struct iovec iov; + union { + struct cmsghdr cmsg; + char size[CMSG_SPACE(sizeof (int))]; + } u; + int ret; + + memset(&m, 0, sizeof m); + m.msg_iov = &iov; + m.msg_iovlen = 1; + iov.iov_base = (void *)sm; + iov.iov_len = sizeof *sm; + if(datafd != -1) { + m.msg_control = (void *)&u.cmsg; + m.msg_controllen = sizeof u; + memset(&u, 0, sizeof u); + u.cmsg.cmsg_len = CMSG_LEN(sizeof (int)); + u.cmsg.cmsg_level = SOL_SOCKET; + u.cmsg.cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(&u.cmsg) = datafd; + } + do { + ret = sendmsg(fd, &m, 0); + } while(ret < 0 && errno == EINTR); + if(ret < 0) + fatal(errno, "sendmsg"); +} + +int speaker_recv(int fd, struct speaker_message *sm, int *datafd) { + struct msghdr m; + struct iovec iov; + union { + struct cmsghdr cmsg; + char size[CMSG_SPACE(sizeof (int))]; + } u; + int ret; + + memset(&m, 0, sizeof m); + m.msg_iov = &iov; + m.msg_iovlen = 1; + iov.iov_base = (void *)sm; + iov.iov_len = sizeof *sm; + if(datafd) { + m.msg_control = (void *)&u.cmsg; + m.msg_controllen = sizeof u; + memset(&u, 0, sizeof u); + u.cmsg.cmsg_len = CMSG_LEN(sizeof (int)); + u.cmsg.cmsg_level = SOL_SOCKET; + u.cmsg.cmsg_type = SCM_RIGHTS; + *datafd = -1; + } + do { + ret = recvmsg(fd, &m, MSG_DONTWAIT); + } while(ret < 0 && errno == EINTR); + if(ret < 0) { + if(errno != EAGAIN) fatal(errno, "recvmsg"); + return -1; + } + if((size_t)m.msg_controllen >= CMSG_LEN(sizeof (int))) { + if(!datafd) + fatal(0, "got an unexpected file descriptor from recvmsg"); + else + *datafd = *(int *)CMSG_DATA(&u.cmsg); + } + return ret; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:VMDANBsSj5JSGuDZIL/zUw */ diff --git a/lib/speaker.h b/lib/speaker.h new file mode 100644 index 0000000..ef1e849 --- /dev/null +++ b/lib/speaker.h @@ -0,0 +1,61 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SPEAKER_H +#define SPEAKER_H + +struct speaker_message { + int type; /* message type */ + long data; /* whatever */ + char id[24]; /* ID including terminator */ +}; + +/* messages from the main DisOrder server */ +#define SM_PREPARE 0 /* prepare ID */ +#define SM_PLAY 1 /* play ID */ +#define SM_PAUSE 2 /* pause current track */ +#define SM_RESUME 3 /* resume current track */ +#define SM_CANCEL 4 /* cancel ID */ +#define SM_RELOAD 5 /* reload configuration */ + +/* messages from the speaker */ +#define SM_PAUSED 128 /* paused ID, DATA seconds in */ +#define SM_FINISHED 129 /* finished ID */ +#define SM_PLAYING 131 /* playing ID, DATA seconds in */ + +void speaker_send(int fd, const struct speaker_message *sm, int datafd); +/* Send a message. DATAFD is passed too if not -1. Does not close DATAFD. */ + +int speaker_recv(int fd, struct speaker_message *sm, int *datafd); +/* Receive a message. If DATAFD is not null then can receive an FD. Return 0 + * on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other + * error. */ + +#endif /* SPEAKER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:QpuCTaKRDXEwz+Df/Jt+Wg */ diff --git a/lib/split.c b/lib/split.c new file mode 100644 index 0000000..dbf7a48 --- /dev/null +++ b/lib/split.c @@ -0,0 +1,169 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <ctype.h> +#include <string.h> +#include <errno.h> + +#include "mem.h" +#include "split.h" +#include "log.h" +#include "charset.h" +#include "vector.h" + +static inline int space(int c) { + return (c == ' ' + || c == '\t' + || c == '\n' + || c == '\r'); +} + +static void no_error_handler(const char attribute((unused)) *msg, + void attribute((unused)) *u) { +} + +char **split(const char *p, + int *np, + unsigned flags, + void (*error_handler)(const char *msg, void *u), + void *u) { + char *f, *g; + const char *q; + struct vector v; + size_t l; + int qc; + + if(!error_handler) error_handler = no_error_handler; + vector_init(&v); + while(*p && !(*p == '#' && (flags & SPLIT_COMMENTS))) { + if(space(*p)) { + ++p; + continue; + } + if((flags & SPLIT_QUOTES) && (*p == '"' || *p == '\'')) { + qc = *p++; + l = 0; + for(q = p; *q && *q != qc; ++q) { + if(*q == '\\' && q[1]) + ++q; + ++l; + } + if(!*q) { + error_handler("unterminated quoted string", u); + return 0; + } + f = g = xmalloc_noptr(l + 1); + for(q = p; *q != qc;) { + if(*q == '\\') { + ++q; + switch(*q) { + case '\\': + case '"': + case '\'': + *g++ = *q++; + break; + case 'n': + ++q; + *g++ = '\n'; + break; + default: + error_handler("illegal escape sequence", u); + return 0; + } + } else + *g++ = *q++; + } + *g = 0; + p = q + 1; + } else { + for(q = p; *q && !space(*q); ++q) + ; + l = q - p; + f = xstrndup(p, l); + p = q; + } + vector_append(&v, f); + } + vector_terminate(&v); + if(np) + *np = v.nvec; + return v.vec; +} + +const char *quoteutf8(const char *s) { + size_t len = 3 + strlen(s); + const char *t; + char *r, *q; + + /* see if we need to quote */ + if(*s) { + for(t = s; *t; t++) + if((unsigned char)*t <= ' ' + || *t == '"' + || *t == '\\' + || *t == '\'' + || *t == '#') + break; + if(!*t) + return s; + } + + /* we rely on ASCII characters only ever representing themselves in UTF-8. */ + for(t = s; *t; t++) { + switch(*t) { + case '"': + case '\\': + case '\n': + ++len; + break; + } + } + q = r = xmalloc_noptr(len); + *q++ = '"'; + for(t = s; *t; t++) { + switch(*t) { + case '"': + case '\\': + *q++ = '\\'; + /* fall through */ + default: + *q++ = *t; + break; + case '\n': + *q++ = '\\'; + *q++ = 'n'; + break; + } + } + *q++ = '"'; + *q = 0; + return r; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:768e4e1bc91d9f45d6beecc9b433992f */ diff --git a/lib/split.h b/lib/split.h new file mode 100644 index 0000000..2344f87 --- /dev/null +++ b/lib/split.h @@ -0,0 +1,31 @@ +#ifndef SPLIT_H +#define SPLIT_H + +#define SPLIT_COMMENTS 0001 /* # starts a comment */ +#define SPLIT_QUOTES 0002 /* " and ' quote strings */ + +char **split(const char *s, + int *np, + unsigned flags, + void (*error_handler)(const char *msg, void *u), + void *u); +/* split @s@ up into fields. Return a null-pointer-terminated array + * of pointers to the fields. If @np@ is not a null pointer store the + * number of fields there. Calls @error_handler@ to report any + * errors. + * + * split() operates on UTF-8 strings. + */ + +const char *quoteutf8(const char *s); +/* quote a UTF-8 string. Might return @s@ if no quoting is required. */ + +#endif /* SPLIT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:b1e012fb5925c578672b122a3fc685cb */ diff --git a/lib/syscalls.c b/lib/syscalls.c new file mode 100644 index 0000000..ddfcb46 --- /dev/null +++ b/lib/syscalls.c @@ -0,0 +1,151 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> + +#include "syscalls.h" +#include "log.h" +#include "printf.h" + +int mustnotbeminus1(const char *what, int ret) { + if(ret == -1) + fatal(errno, "error calling %s", what); + return ret; +} + +pid_t xfork(void) { + pid_t pid; + + if((pid = fork()) < 0) fatal(errno, "error calling fork"); + return pid; +} + +void xclose(int fd) { + mustnotbeminus1("close", close(fd)); +} + +void xdup2(int fd1, int fd2) { + mustnotbeminus1("dup2", dup2(fd1, fd2)); +} + +void xpipe(int *fdp) { + mustnotbeminus1("pipe", pipe(fdp)); +} + +void nonblock(int fd) { + mustnotbeminus1("fcntl F_SETFL", + fcntl(fd, F_SETFL, + mustnotbeminus1("fcntl F_GETFL", + fcntl(fd, F_GETFL)) | O_NONBLOCK)); +} + +void cloexec(int fd) { + mustnotbeminus1("fcntl F_SETFD", + fcntl(fd, F_SETFD, + mustnotbeminus1("fcntl F_GETFD", + fcntl(fd, F_GETFD)) | FD_CLOEXEC)); +} + +void xlisten(int fd, int q) { + mustnotbeminus1("listen", listen(fd, q)); +} + +void xshutdown(int fd, int how) { + mustnotbeminus1("shutdown", shutdown(fd, how)); +} + +void xsetsockopt(int fd, int l, int o, const void *v, socklen_t vl) { + mustnotbeminus1("setsockopt", setsockopt(fd, l, o, v, vl)); +} + +int xsocket(int d, int t, int p) { + return mustnotbeminus1("socket", socket(d, t, p)); +} + +void xconnect(int fd, const struct sockaddr *sa, socklen_t sl) { + mustnotbeminus1("connect", connect(fd, sa, sl)); +} + +void xsigprocmask(int how, const sigset_t *set, sigset_t *oldset) { + mustnotbeminus1("sigprocmask", sigprocmask(how, set, oldset)); +} + +void xsigaction(int sig, const struct sigaction *sa, struct sigaction *oldsa) { + mustnotbeminus1("sigaction", sigaction(sig, sa, oldsa)); +} + +int xprintf(const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = mustnotbeminus1("byte_vfprintf", byte_vfprintf(stdout, fmt, ap)); + va_end(ap); + return n; +} + +void xfclose(FILE *fp) { + mustnotbeminus1("fclose", fclose(fp)); +} + +int xstrtol(long *n, const char *s, char **ep, int base) { + errno = 0; + *n = strtol(s, ep, base); + return errno; +} + +int xstrtoll(long_long *n, const char *s, char **ep, int base) { + errno = 0; + *n = strtoll(s, ep, base); + return errno; +} + +int xnice(int inc) { + int ret; + + /* some versions of nice() return the new nice value which in principle could + * be -1 */ + errno = 0; + ret = nice(inc); + if(errno) fatal(errno, "error calling nice"); + return ret; +} + +void xgettimeofday(struct timeval *tv, struct timezone *tz) { + mustnotbeminus1("gettimeofday", gettimeofday(tv, tz)); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0be4384b4081d464d1a2fad746469d3d */ diff --git a/lib/syscalls.h b/lib/syscalls.h new file mode 100644 index 0000000..2ebffd9 --- /dev/null +++ b/lib/syscalls.h @@ -0,0 +1,74 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SYSCALLS_H +#define SYSCALLS_H + +/* various error-handling wrappers. Not actually all system calls! */ + +struct sockaddr; +struct sigaction; +struct timezone; + +#include <sys/socket.h> +#include <signal.h> +#include <stdio.h> + +#include "types.h" + +pid_t xfork(void); +void xclose(int); +void xdup2(int, int); +void xpipe(int *); +int xfcntl(int, int, long); +void xlisten(int, int); +void xshutdown(int, int); +void xsetsockopt(int, int, int, const void *, socklen_t); +int xsocket(int, int, int); +void xconnect(int, const struct sockaddr *, socklen_t); +void xsigprocmask(int how, const sigset_t *set, sigset_t *oldset); +void xsigaction(int sig, const struct sigaction *sa, struct sigaction *oldsa); +int xprintf(const char *, ...) + attribute((format (printf, 1, 2))); +void xfclose(FILE *); +int xnice(int); +void xgettimeofday(struct timeval *, struct timezone *); +/* the above all call @fatal@ if the system call fails */ + +void nonblock(int fd); +void cloexec(int fd); +/* make @fd@ non-blocking/close-on-exec; call @fatal@ on error. */ + +int mustnotbeminus1(const char *what, int value); +/* If @value@ is -1, report an error including @what@. */ + +int xstrtol(long *n, const char *s, char **ep, int base); +int xstrtoll(long_long *n, const char *s, char **ep, int base); +/* like strtol() but returns errno on error, 0 on success */ + +#endif /* SYSCALLS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:87545481469f8a85f73c5216c6788c0e */ diff --git a/lib/table.c b/lib/table.c new file mode 100644 index 0000000..1f30980 --- /dev/null +++ b/lib/table.c @@ -0,0 +1,50 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> + +#include "table.h" + +int table_find(const void *table, size_t offset, size_t eltsize, size_t nelts, + const char *name) { + int l = 0, r = (int)nelts - 1, c, m; + const char *k; + + while(l <= r) { + k = *(const char **)((char *)table + offset + eltsize * (m = (l + r) / 2)); + if(!(c = strcmp(name, k))) + return m; + if(c < 0) + r = m - 1; + else + l = m + 1; + } + return -1; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0cc7ee1d25ae5e35f8dbd2460c9f4afa */ diff --git a/lib/table.h b/lib/table.h new file mode 100644 index 0000000..476399d --- /dev/null +++ b/lib/table.h @@ -0,0 +1,46 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TABLE_H +#define TABLE_H + +#define TABLE_FIND(TABLE, TYPE, FIELD, NAME) \ + table_find((void *)TABLE, \ + offsetof(TYPE, FIELD), \ + sizeof (TYPE), \ + sizeof TABLE / sizeof (TYPE), \ + NAME) +/* Search TYPE TABLE[] for an element where TABLE[N].FIELD matches NAME + * Returns the index N on success or -1 if not found + * The table must be lexically sorted on FIELD + */ + +int table_find(const void *table, size_t offset, size_t eltsize, size_t nelts, + const char *name); + +#endif /* TABLE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:15b07f98a592f80e4e22dd3f213f2580 */ diff --git a/lib/test.c b/lib/test.c new file mode 100644 index 0000000..6eee1c9 --- /dev/null +++ b/lib/test.c @@ -0,0 +1,418 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <ctype.h> + +#include "utf8.h" +#include "mem.h" +#include "log.h" +#include "vector.h" +#include "charset.h" +#include "mime.h" +#include "hex.h" +#include "words.h" + +static int tests, errors; + +#define insist(expr) do { \ + if(!expr) { \ + ++errors; \ + fprintf(stderr, "%s:%d: error checking %s\n", \ + __FILE__, __LINE__, #expr); \ + } \ + ++tests; \ +} while(0) + +static const char *format(const char *s) { + struct dynstr d; + int c; + char buf[10]; + + dynstr_init(&d); + while((c = (unsigned char)*s++)) { + if(c >= ' ' && c <= '~') + dynstr_append(&d, c); + else { + sprintf(buf, "\\x%02X", (unsigned)c); + dynstr_append_string(&d, buf); + } + } + dynstr_terminate(&d); + return d.vec; +} + +#define check_string(GOT, WANT) do { \ + const char *g = GOT; \ + const char *w = WANT; \ + \ + if(w == 0) { \ + fprintf(stderr, "%s:%d: %s returned 0\n", \ + __FILE__, __LINE__, #GOT); \ + ++errors; \ + } else if(strcmp(w, g)) { \ + fprintf(stderr, "%s:%d: %s returned:\n%s\nexpected:\n%s\n", \ + __FILE__, __LINE__, #GOT, format(g), format(w)); \ + ++errors; \ + } \ + ++tests; \ + } while(0) + +static uint32_t *ucs4parse(const char *s) { + struct dynstr_ucs4 d; + char *e; + + dynstr_ucs4_init(&d); + while(*s) { + errno = 0; + dynstr_ucs4_append(&d, strtoul(s, &e, 0)); + if(errno) fatal(errno, "strtoul (%s)", s); + s = e; + } + dynstr_ucs4_terminate(&d); + return d.vec; +} + +static void test_utf8(void) { + /* Test validutf8, convert to UCS-4, check the answer is right, + * convert back to UTF-8, check we got to where we started */ +#define U8(CHARS, WORDS) do { \ + uint32_t *w = ucs4parse(WORDS); \ + uint32_t *ucs; \ + char *u8; \ + \ + insist(validutf8(CHARS)); \ + ucs = utf82ucs4(CHARS); \ + insist(ucs != 0); \ + insist(!ucs4cmp(w, ucs)); \ + u8 = ucs42utf8(ucs); \ + insist(u8 != 0); \ + insist(!strcmp(u8, CHARS)); \ +} while(0) + + /* empty string */ + + U8("", ""); + + /* ASCII characters */ + + U8(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d " + "0x2e 0x2f 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a " + "0x3b 0x3c 0x3d 0x3e 0x3f 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 " + "0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f 0x50 0x51 0x52 0x53 0x54 " + "0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f 0x60 0x61 " + "0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e " + "0x6f 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b " + "0x7c 0x7d 0x7e"); + U8("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177", + "0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa 0xb 0xc 0xd 0xe 0xf 0x10 " + "0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d " + "0x1e 0x1f 0x7f"); + + /* from RFC3629 */ + + /* UTF8-2 = %xC2-DF UTF8-tail */ + insist(!validutf8("\xC0\x80")); + insist(!validutf8("\xC1\x80")); + insist(!validutf8("\xC2\x7F")); + U8("\xC2\x80", "0x80"); + U8("\xDF\xBF", "0x7FF"); + insist(!validutf8("\xDF\xC0")); + + /* UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / + * %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) + */ + insist(!validutf8("\xE0\x9F\x80")); + U8("\xE0\xA0\x80", "0x800"); + U8("\xE0\xBF\xBF", "0xFFF"); + insist(!validutf8("\xE0\xC0\xBF")); + + insist(!validutf8("\xE1\x80\x7F")); + U8("\xE1\x80\x80", "0x1000"); + U8("\xEC\xBF\xBF", "0xCFFF"); + insist(!validutf8("\xEC\xC0\xBF")); + + U8("\xED\x80\x80", "0xD000"); + U8("\xED\x9F\xBF", "0xD7FF"); + insist(!validutf8("\xED\xA0\xBF")); + + insist(!validutf8("\xEE\x7f\x80")); + U8("\xEE\x80\x80", "0xE000"); + U8("\xEF\xBF\xBF", "0xFFFF"); + insist(!validutf8("\xEF\xC0\xBF")); + + /* UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / + * %xF4 %x80-8F 2( UTF8-tail ) + */ + insist(!validutf8("\xF0\x8F\x80\x80")); + U8("\xF0\x90\x80\x80", "0x10000"); + U8("\xF0\xBF\xBF\xBF", "0x3FFFF"); + insist(!validutf8("\xF0\xC0\x80\x80")); + + insist(!validutf8("\xF1\x80\x80\x7F")); + U8("\xF1\x80\x80\x80", "0x40000"); + U8("\xF3\xBF\xBF\xBF", "0xFFFFF"); + insist(!validutf8("\xF3\xC0\x80\x80")); + + insist(!validutf8("\xF4\x80\x80\x7F")); + U8("\xF4\x80\x80\x80", "0x100000"); + U8("\xF4\x8F\xBF\xBF", "0x10FFFF"); + insist(!validutf8("\xF4\x90\x80\x80")); + + /* miscellaneous non-UTF-8 rubbish */ + insist(!validutf8("\x80")); + insist(!validutf8("\xBF")); + insist(!validutf8("\xC0")); + insist(!validutf8("\xC0\x7F")); + insist(!validutf8("\xC0\xC0")); + insist(!validutf8("\xE0")); + insist(!validutf8("\xE0\x7F")); + insist(!validutf8("\xE0\xC0")); + insist(!validutf8("\xE0\x80")); + insist(!validutf8("\xE0\x80\x7f")); + insist(!validutf8("\xE0\x80\xC0")); + insist(!validutf8("\xF0")); + insist(!validutf8("\xF0\x7F")); + insist(!validutf8("\xF0\xC0")); + insist(!validutf8("\xF0\x80")); + insist(!validutf8("\xF0\x80\x7f")); + insist(!validutf8("\xF0\x80\xC0")); + insist(!validutf8("\xF0\x80\x80\x7f")); + insist(!validutf8("\xF0\x80\x80\xC0")); + insist(!validutf8("\xF5\x80\x80\x80")); + insist(!validutf8("\xF8")); +} + +static void test_mime(void) { + char *t, *n, *v; + + t = n = v = 0; + insist(!mime_content_type("text/plain", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(n == 0); + insist(v == 0); + + t = n = v = 0; + insist(!mime_content_type("TEXT ((nested) comment) /plain", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(n == 0); + insist(v == 0); + + t = n = v = 0; + insist(!mime_content_type(" text/plain ; Charset=utf-8", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(!strcmp(n, "charset")); + insist(!strcmp(v, "utf-8")); + + t = n = v = 0; + insist(!mime_content_type("text/plain;charset = ISO-8859-1 ", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(!strcmp(n, "charset")); + insist(!strcmp(v, "ISO-8859-1")); + + /* XXX mime_parse */ + /* XXX mime_multipart */ + /* XXX mime_rfc2388_content_disposition */ + + check_string(mime_qp(""), ""); + check_string(mime_qp("foobar"), "foobar"); + check_string(mime_qp("foo=20bar"), "foo bar"); + check_string(mime_qp("x \r\ny"), "x\r\ny"); + check_string(mime_qp("x=\r\ny"), "xy"); + check_string(mime_qp("x= \r\ny"), "xy"); + check_string(mime_qp("x =\r\ny"), "x y"); + check_string(mime_qp("x = \r\ny"), "x y"); + + /* from RFC2045 */ + check_string(mime_qp("Now's the time =\r\n" +"for all folk to come=\r\n" +" to the aid of their country."), + "Now's the time for all folk to come to the aid of their country."); + + check_string(mime_base64(""), ""); + check_string(mime_base64("BBBB"), "\x04\x10\x41"); + check_string(mime_base64("////"), "\xFF\xFF\xFF"); + check_string(mime_base64("//BB"), "\xFF\xF0\x41"); + check_string(mime_base64("BBBB//BB////"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("B B B B / / B B / / / /"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("B\r\nBBB.// B-B//~//"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("BBBB="), + "\x04\x10\x41"); + check_string(mime_base64("BBBBx="), /* not actually valid base64 */ + "\x04\x10\x41"); + check_string(mime_base64("BBBB BB=="), + "\x04\x10\x41" "\x04"); + check_string(mime_base64("BBBB BBB="), + "\x04\x10\x41" "\x04\x10"); +} + +static void test_hex(void) { + unsigned n; + static const unsigned char h[] = { 0x00, 0xFF, 0x80, 0x7F }; + uint8_t *u; + size_t ul; + + for(n = 0; n <= UCHAR_MAX; ++n) { + if(!isxdigit(n)) + insist(unhexdigitq(n) == -1); + } + insist(unhexdigitq('0') == 0); + insist(unhexdigitq('1') == 1); + insist(unhexdigitq('2') == 2); + insist(unhexdigitq('3') == 3); + insist(unhexdigitq('4') == 4); + insist(unhexdigitq('5') == 5); + insist(unhexdigitq('6') == 6); + insist(unhexdigitq('7') == 7); + insist(unhexdigitq('8') == 8); + insist(unhexdigitq('9') == 9); + insist(unhexdigitq('a') == 10); + insist(unhexdigitq('b') == 11); + insist(unhexdigitq('c') == 12); + insist(unhexdigitq('d') == 13); + insist(unhexdigitq('e') == 14); + insist(unhexdigitq('f') == 15); + insist(unhexdigitq('A') == 10); + insist(unhexdigitq('B') == 11); + insist(unhexdigitq('C') == 12); + insist(unhexdigitq('D') == 13); + insist(unhexdigitq('E') == 14); + insist(unhexdigitq('F') == 15); + check_string(hex(h, sizeof h), "00ff807f"); + check_string(hex(0, 0), ""); + u = unhex("00ff807f", &ul); + insist(ul == 4); + insist(memcmp(u, h, 4) == 0); + u = unhex("00FF807F", &ul); + insist(ul == 4); + insist(memcmp(u, h, 4) == 0); + u = unhex("", &ul); + insist(ul == 0); + fprintf(stderr, "2 ERROR reports expected:\n"); + insist(unhex("F", 0) == 0); + insist(unhex("az", 0) == 0); +} + +static void test_casefold(void) { + uint32_t c, l, u[2]; + const char *s, *ls; + + for(c = 1; c < 256; ++c) { + u[0] = c; + u[1] = 0; + s = ucs42utf8(u); + ls = casefold(s); + switch(c) { + default: + if((c >= 'A' && c <= 'Z') + || (c >= 0xC0 && c <= 0xDE && c != 0xD7)) + l = c ^ 0x20; + else + l = c; + break; + case 0xB5: /* MICRO SIGN */ + l = 0x3BC; /* GREEK SMALL LETTER MU */ + break; + case 0xDF: /* LATIN SMALL LETTER SHARP S */ + insist(!strcmp(ls, "ss")); + l = 0; + break; + } + if(l) { + u[0] = l; + u[1] = 0; + s = ucs42utf8(u); + if(strcmp(s, ls)) { + fprintf(stderr, "%s:%d: casefolding %#lx got '%s', expected '%s'\n", + __FILE__, __LINE__, (unsigned long)c, + format(ls), format(s)); + ++errors; + } + ++tests; + } + } + check_string(casefold(""), ""); +} + +int main(void) { + insist('\n' == 0x0A); + insist('\r' == 0x0D); + insist(' ' == 0x20); + insist('0' == 0x30); + insist('9' == 0x39); + insist('A' == 0x41); + insist('Z' == 0x5A); + insist('a' == 0x61); + insist('z' == 0x7A); + /* addr.c */ + /* asprintf.c */ + /* authhash.c */ + /* basen.c */ + /* charset.c */ + /* client.c */ + /* configuration.c */ + /* event.c */ + /* fprintf.c */ + /* hex.c */ + test_hex(); + /* inputline.c */ + /* kvp.c */ + /* log.c */ + /* mem.c */ + /* mime.c */ + test_mime(); + /* mixer.c */ + /* plugin.c */ + /* printf.c */ + /* queue.c */ + /* sink.c */ + /* snprintf.c */ + /* split.c */ + /* syscalls.c */ + /* table.c */ + /* utf8.c */ + test_utf8(); + /* vector.c */ + /* words.c */ + test_casefold(); + /* XXX words() */ + /* wstat.c */ + fprintf(stderr, "%d errors out of %d tests\n", errors, tests); + return !!errors; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ + +/* arch-tag:pJuXi+p6I8qcGldDkx/yXA */ diff --git a/lib/trackname.c b/lib/trackname.c new file mode 100644 index 0000000..c439fa4 --- /dev/null +++ b/lib/trackname.c @@ -0,0 +1,144 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <pcre.h> +#include <fnmatch.h> +#include <string.h> +#include <assert.h> + +#include "trackname.h" +#include "configuration.h" +#include "regsub.h" +#include "log.h" +#include "filepart.h" +#include "words.h" + +const char *find_track_root(const char *track) { + int n; + size_t l, tl = strlen(track); + + for(n = 0; n < config->collection.n; ++n) { + l = strlen(config->collection.s[n].root); + if(tl > l + && !strncmp(track, config->collection.s[n].root, l) + && track[l] == '/') + break; + } + if(n >= config->collection.n) return 0; + return config->collection.s[n].root; +} + +const char *track_rootless(const char *track) { + const char *root; + + if(!(root = find_track_root(track))) return 0; + return track + strlen(root); +} + +const char *trackname_part(const char *track, + const char *context, + const char *part) { + int n; + const char *replaced, *rootless; + + assert(track != 0); + if(!strcmp(part, "path")) return track; + if(!strcmp(part, "ext")) return extension(track); + if((rootless = track_rootless(track))) track = rootless; + for(n = 0; n < config->namepart.n; ++n) { + if(!strcmp(config->namepart.s[n].part, part) + && fnmatch(config->namepart.s[n].context, context, 0) == 0) { + if((replaced = regsub(config->namepart.s[n].re, + track, + config->namepart.s[n].replace, + config->namepart.s[n].reflags + |REGSUB_MUST_MATCH + |REGSUB_REPLACE))) + return replaced; + } + } + return ""; +} + +const char *trackname_transform(const char *type, + const char *subject, + const char *context) { + const char *replaced; + int n; + const struct transform *k; + + for(n = 0; n < config->transform.n; ++n) { + k = &config->transform.t[n]; + if(strcmp(k->type, type)) + continue; + if(fnmatch(k->context, context, 0) != 0) + continue; + if((replaced = regsub(k->re, subject, k->replace, k->flags))) + subject = replaced; + } + return subject; +} + +int compare_tracks(const char *sa, const char *sb, + const char *da, const char *db, + const char *ta, const char *tb) { + int c; + + if((c = strcmp(casefold(sa), casefold(sb)))) return c; + if((c = strcmp(sa, sb))) return c; + if((c = strcmp(casefold(da), casefold(db)))) return c; + if((c = strcmp(da, db))) return c; + return compare_path(ta, tb); +} + +int compare_path_raw(const unsigned char *ap, size_t an, + const unsigned char *bp, size_t bn) { + while(an > 0 && bn > 0) { + if(*ap == *bp) { + ap++; + bp++; + an--; + bn--; + } else if(*ap == '/') { + return -1; /* /a/b < /aa/ */ + } else if(*bp == '/') { + return 1; /* /aa > /a/b */ + } else + return *ap - *bp; + } + if(an > 0) + return 1; /* /a/b > /a and /ab > /a */ + else if(bn > 0) + return -1; /* /a < /ab and /a < /a/b */ + else + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:xMbRRluU86PaVSSnyIR77A */ diff --git a/lib/trackname.h b/lib/trackname.h new file mode 100644 index 0000000..cb227e0 --- /dev/null +++ b/lib/trackname.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TRACKNAME_H +#define TRACKNAME_H + +const char *find_track_root(const char *track); +/* find the collection root for @track@ */ + +const char *track_rootless(const char *track); +/* return the rootless part of @track@ (typically starting /) */ + +const char *trackname_part(const char *track, + const char *context, + const char *part); +/* compute PART (artist/album/title) for TRACK in CONTEXT (display/sort) */ + +const char *trackname_transform(const char *type, + const char *subject, + const char *context); +/* convert SUBJECT (usually 'track' or 'dir' according to TYPE) for CONTEXT + * (display/sort) */ + +int compare_tracks(const char *sa, const char *sb, + const char *da, const char *db, + const char *ta, const char *tb); +/* Compare tracks A and B, with sort/display/track names S?, D? and T? */ + +int compare_path_raw(const unsigned char *ap, size_t an, + const unsigned char *bp, size_t bn); +/* Comparison function for path names that groups all entries in a directory + * together */ + +/* Convenient wrapper for compare_path_raw */ +static inline int compare_path(const char *ap, const char *bp) { + return compare_path_raw((const unsigned char *)ap, strlen(ap), + (const unsigned char *)bp, strlen(bp)); +} + +#endif /* TRACKNAME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:JwJ4jz+OgN1Th4UTvpqQ5Q */ diff --git a/lib/types.h b/lib/types.h new file mode 100644 index 0000000..e683268 --- /dev/null +++ b/lib/types.h @@ -0,0 +1,115 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TYPES_H +#define TYPES_H + +#if HAVE_INTTYPES_H +# include <inttypes.h> +#endif +#include <limits.h> +#include <sys/types.h> + +/* had better be before atol/atoll redefinition */ +#include <stdlib.h> + +#if HAVE_LONG_LONG +typedef long long long_long; +typedef unsigned long long u_long_long; +# if ! DECLARES_STRTOLL +long long strtoll(const char *, char **, int); +# endif +# if ! DECLARES_ATOLL +long long atoll(const char *); +# endif +#else +typedef long long_long; +typedef unsigned long u_long_long; +# define atoll atol +# define strtoll strtol +#endif + +#if __APPLE__ +/* apple define these to j[dxu], which gcc -std=c99 -pedantic then rejects */ +# undef PRIdMAX +# undef PRIxMAX +# undef PRIuMAX +#endif + +#if HAVE_INTMAX_T +# ifndef PRIdMAX +# define PRIdMAX "jd" +# endif +#elif HAVE_LONG_LONG +typedef long long intmax_t; +# define PRIdMAX "lld" +#else +typedef long intmax_t; +# define PRIdMAX "ld" +#endif + +#if HAVE_UINTMAX_T +# ifndef PRIuMAX +# define PRIuMAX "ju" +# endif +# ifndef PRIxMAX +# define PRIxMAX "jx" +# endif +#elif HAVE_LONG_LONG +typedef unsigned long long uintmax_t; +# define PRIuMAX "llu" +# define PRIxMAX "llx" +#else +typedef unsigned long uintmax_t; +# define PRIuMAX "lu" +# define PRIxMAX "lx" +#endif + +#if ! HAVE_UINT8_T +# if CHAR_BIT == 8 +typedef unsigned char uint8_t; +# else +# error cannot determine uint8_t +# endif +#endif + +#if ! HAVE_UINT32_T +# if UINT_MAX == 4294967295 +typedef unsigned int uint32_t; +# elif ULONG_MAX == 4294967295 +typedef unsigned long uint32_t; +# elif USHRT_MAX == 4294967295 +typedef unsigned short uint32_t; +# elif UCHAR_MAX == 4294967295 +typedef unsigned char uint32_t; +# else +# error cannot determine uint32_t +# endif +#endif + +#endif /* TYPES_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:d42f46f53ccab924aabc3116b716a531 */ diff --git a/lib/unicodegc.h b/lib/unicodegc.h new file mode 100644 index 0000000..1f67f5d --- /dev/null +++ b/lib/unicodegc.h @@ -0,0 +1,1614 @@ +enum unicode_gc_cat { + unicode_gc_Cc, + unicode_gc_Cf, + unicode_gc_Co, + unicode_gc_Cs, + unicode_gc_Ll, + unicode_gc_Lm, + unicode_gc_Lo, + unicode_gc_Lt, + unicode_gc_Lu, + unicode_gc_Mc, + unicode_gc_Me, + unicode_gc_Mn, + unicode_gc_Nd, + unicode_gc_Nl, + unicode_gc_No, + unicode_gc_Pc, + unicode_gc_Pd, + unicode_gc_Pe, + unicode_gc_Pf, + unicode_gc_Pi, + unicode_gc_Po, + unicode_gc_Ps, + unicode_gc_Sc, + unicode_gc_Sk, + unicode_gc_Sm, + unicode_gc_So, + unicode_gc_Zl, + unicode_gc_Zp, + unicode_gc_Zs, + unicode_gc_none +}; +static const struct unicode_gc { + uint32_t l, h; + enum unicode_gc_cat cat; +} gcs[] = { + { 0, 31, unicode_gc_Cc }, + { 32, 32, unicode_gc_Zs }, + { 33, 35, unicode_gc_Po }, + { 36, 36, unicode_gc_Sc }, + { 37, 39, unicode_gc_Po }, + { 40, 40, unicode_gc_Ps }, + { 41, 41, unicode_gc_Pe }, + { 42, 42, unicode_gc_Po }, + { 43, 43, unicode_gc_Sm }, + { 44, 44, unicode_gc_Po }, + { 45, 45, unicode_gc_Pd }, + { 46, 47, unicode_gc_Po }, + { 48, 57, unicode_gc_Nd }, + { 58, 59, unicode_gc_Po }, + { 60, 62, unicode_gc_Sm }, + { 63, 64, unicode_gc_Po }, + { 65, 90, unicode_gc_Lu }, + { 91, 91, unicode_gc_Ps }, + { 92, 92, unicode_gc_Po }, + { 93, 93, unicode_gc_Pe }, + { 94, 94, unicode_gc_Sk }, + { 95, 95, unicode_gc_Pc }, + { 96, 96, unicode_gc_Sk }, + { 97, 122, unicode_gc_Ll }, + { 123, 123, unicode_gc_Ps }, + { 124, 124, unicode_gc_Sm }, + { 125, 125, unicode_gc_Pe }, + { 126, 126, unicode_gc_Sm }, + { 127, 159, unicode_gc_Cc }, + { 160, 160, unicode_gc_Zs }, + { 161, 161, unicode_gc_Po }, + { 162, 165, unicode_gc_Sc }, + { 166, 167, unicode_gc_So }, + { 168, 168, unicode_gc_Sk }, + { 169, 169, unicode_gc_So }, + { 170, 170, unicode_gc_Ll }, + { 171, 171, unicode_gc_Pi }, + { 172, 172, unicode_gc_Sm }, + { 173, 173, unicode_gc_Cf }, + { 174, 174, unicode_gc_So }, + { 175, 175, unicode_gc_Sk }, + { 176, 176, unicode_gc_So }, + { 177, 177, unicode_gc_Sm }, + { 178, 179, unicode_gc_No }, + { 180, 180, unicode_gc_Sk }, + { 181, 181, unicode_gc_Ll }, + { 182, 182, unicode_gc_So }, + { 183, 183, unicode_gc_Po }, + { 184, 184, unicode_gc_Sk }, + { 185, 185, unicode_gc_No }, + { 186, 186, unicode_gc_Ll }, + { 187, 187, unicode_gc_Pf }, + { 188, 190, unicode_gc_No }, + { 191, 191, unicode_gc_Po }, + { 192, 214, unicode_gc_Lu }, + { 215, 215, unicode_gc_Sm }, + { 216, 222, unicode_gc_Lu }, + { 223, 246, unicode_gc_Ll }, + { 247, 247, unicode_gc_Sm }, + { 248, 255, unicode_gc_Ll }, + { 256, 256, unicode_gc_Lu }, + { 257, 257, unicode_gc_Ll }, + { 258, 258, unicode_gc_Lu }, + { 259, 259, unicode_gc_Ll }, + { 260, 260, unicode_gc_Lu }, + { 261, 261, unicode_gc_Ll }, + { 262, 262, unicode_gc_Lu }, + { 263, 263, unicode_gc_Ll }, + { 264, 264, unicode_gc_Lu }, + { 265, 265, unicode_gc_Ll }, + { 266, 266, unicode_gc_Lu }, + { 267, 267, unicode_gc_Ll }, + { 268, 268, unicode_gc_Lu }, + { 269, 269, unicode_gc_Ll }, + { 270, 270, unicode_gc_Lu }, + { 271, 271, unicode_gc_Ll }, + { 272, 272, unicode_gc_Lu }, + { 273, 273, unicode_gc_Ll }, + { 274, 274, unicode_gc_Lu }, + { 275, 275, unicode_gc_Ll }, + { 276, 276, unicode_gc_Lu }, + { 277, 277, unicode_gc_Ll }, + { 278, 278, unicode_gc_Lu }, + { 279, 279, unicode_gc_Ll }, + { 280, 280, unicode_gc_Lu }, + { 281, 281, unicode_gc_Ll }, + { 282, 282, unicode_gc_Lu }, + { 283, 283, unicode_gc_Ll }, + { 284, 284, unicode_gc_Lu }, + { 285, 285, unicode_gc_Ll }, + { 286, 286, unicode_gc_Lu }, + { 287, 287, unicode_gc_Ll }, + { 288, 288, unicode_gc_Lu }, + { 289, 289, unicode_gc_Ll }, + { 290, 290, unicode_gc_Lu }, + { 291, 291, unicode_gc_Ll }, + { 292, 292, unicode_gc_Lu }, + { 293, 293, unicode_gc_Ll }, + { 294, 294, unicode_gc_Lu }, + { 295, 295, unicode_gc_Ll }, + { 296, 296, unicode_gc_Lu }, + { 297, 297, unicode_gc_Ll }, + { 298, 298, unicode_gc_Lu }, + { 299, 299, unicode_gc_Ll }, + { 300, 300, unicode_gc_Lu }, + { 301, 301, unicode_gc_Ll }, + { 302, 302, unicode_gc_Lu }, + { 303, 303, unicode_gc_Ll }, + { 304, 304, unicode_gc_Lu }, + { 305, 305, unicode_gc_Ll }, + { 306, 306, unicode_gc_Lu }, + { 307, 307, unicode_gc_Ll }, + { 308, 308, unicode_gc_Lu }, + { 309, 309, unicode_gc_Ll }, + { 310, 310, unicode_gc_Lu }, + { 311, 312, unicode_gc_Ll }, + { 313, 313, unicode_gc_Lu }, + { 314, 314, unicode_gc_Ll }, + { 315, 315, unicode_gc_Lu }, + { 316, 316, unicode_gc_Ll }, + { 317, 317, unicode_gc_Lu }, + { 318, 318, unicode_gc_Ll }, + { 319, 319, unicode_gc_Lu }, + { 320, 320, unicode_gc_Ll }, + { 321, 321, unicode_gc_Lu }, + { 322, 322, unicode_gc_Ll }, + { 323, 323, unicode_gc_Lu }, + { 324, 324, unicode_gc_Ll }, + { 325, 325, unicode_gc_Lu }, + { 326, 326, unicode_gc_Ll }, + { 327, 327, unicode_gc_Lu }, + { 328, 329, unicode_gc_Ll }, + { 330, 330, unicode_gc_Lu }, + { 331, 331, unicode_gc_Ll }, + { 332, 332, unicode_gc_Lu }, + { 333, 333, unicode_gc_Ll }, + { 334, 334, unicode_gc_Lu }, + { 335, 335, unicode_gc_Ll }, + { 336, 336, unicode_gc_Lu }, + { 337, 337, unicode_gc_Ll }, + { 338, 338, unicode_gc_Lu }, + { 339, 339, unicode_gc_Ll }, + { 340, 340, unicode_gc_Lu }, + { 341, 341, unicode_gc_Ll }, + { 342, 342, unicode_gc_Lu }, + { 343, 343, unicode_gc_Ll }, + { 344, 344, unicode_gc_Lu }, + { 345, 345, unicode_gc_Ll }, + { 346, 346, unicode_gc_Lu }, + { 347, 347, unicode_gc_Ll }, + { 348, 348, unicode_gc_Lu }, + { 349, 349, unicode_gc_Ll }, + { 350, 350, unicode_gc_Lu }, + { 351, 351, unicode_gc_Ll }, + { 352, 352, unicode_gc_Lu }, + { 353, 353, unicode_gc_Ll }, + { 354, 354, unicode_gc_Lu }, + { 355, 355, unicode_gc_Ll }, + { 356, 356, unicode_gc_Lu }, + { 357, 357, unicode_gc_Ll }, + { 358, 358, unicode_gc_Lu }, + { 359, 359, unicode_gc_Ll }, + { 360, 360, unicode_gc_Lu }, + { 361, 361, unicode_gc_Ll }, + { 362, 362, unicode_gc_Lu }, + { 363, 363, unicode_gc_Ll }, + { 364, 364, unicode_gc_Lu }, + { 365, 365, unicode_gc_Ll }, + { 366, 366, unicode_gc_Lu }, + { 367, 367, unicode_gc_Ll }, + { 368, 368, unicode_gc_Lu }, + { 369, 369, unicode_gc_Ll }, + { 370, 370, unicode_gc_Lu }, + { 371, 371, unicode_gc_Ll }, + { 372, 372, unicode_gc_Lu }, + { 373, 373, unicode_gc_Ll }, + { 374, 374, unicode_gc_Lu }, + { 375, 375, unicode_gc_Ll }, + { 376, 377, unicode_gc_Lu }, + { 378, 378, unicode_gc_Ll }, + { 379, 379, unicode_gc_Lu }, + { 380, 380, unicode_gc_Ll }, + { 381, 381, unicode_gc_Lu }, + { 382, 384, unicode_gc_Ll }, + { 385, 386, unicode_gc_Lu }, + { 387, 387, unicode_gc_Ll }, + { 388, 388, unicode_gc_Lu }, + { 389, 389, unicode_gc_Ll }, + { 390, 391, unicode_gc_Lu }, + { 392, 392, unicode_gc_Ll }, + { 393, 395, unicode_gc_Lu }, + { 396, 397, unicode_gc_Ll }, + { 398, 401, unicode_gc_Lu }, + { 402, 402, unicode_gc_Ll }, + { 403, 404, unicode_gc_Lu }, + { 405, 405, unicode_gc_Ll }, + { 406, 408, unicode_gc_Lu }, + { 409, 411, unicode_gc_Ll }, + { 412, 413, unicode_gc_Lu }, + { 414, 414, unicode_gc_Ll }, + { 415, 416, unicode_gc_Lu }, + { 417, 417, unicode_gc_Ll }, + { 418, 418, unicode_gc_Lu }, + { 419, 419, unicode_gc_Ll }, + { 420, 420, unicode_gc_Lu }, + { 421, 421, unicode_gc_Ll }, + { 422, 423, unicode_gc_Lu }, + { 424, 424, unicode_gc_Ll }, + { 425, 425, unicode_gc_Lu }, + { 426, 427, unicode_gc_Ll }, + { 428, 428, unicode_gc_Lu }, + { 429, 429, unicode_gc_Ll }, + { 430, 431, unicode_gc_Lu }, + { 432, 432, unicode_gc_Ll }, + { 433, 435, unicode_gc_Lu }, + { 436, 436, unicode_gc_Ll }, + { 437, 437, unicode_gc_Lu }, + { 438, 438, unicode_gc_Ll }, + { 439, 440, unicode_gc_Lu }, + { 441, 442, unicode_gc_Ll }, + { 443, 443, unicode_gc_Lo }, + { 444, 444, unicode_gc_Lu }, + { 445, 447, unicode_gc_Ll }, + { 448, 451, unicode_gc_Lo }, + { 452, 452, unicode_gc_Lu }, + { 453, 453, unicode_gc_Lt }, + { 454, 454, unicode_gc_Ll }, + { 455, 455, unicode_gc_Lu }, + { 456, 456, unicode_gc_Lt }, + { 457, 457, unicode_gc_Ll }, + { 458, 458, unicode_gc_Lu }, + { 459, 459, unicode_gc_Lt }, + { 460, 460, unicode_gc_Ll }, + { 461, 461, unicode_gc_Lu }, + { 462, 462, unicode_gc_Ll }, + { 463, 463, unicode_gc_Lu }, + { 464, 464, unicode_gc_Ll }, + { 465, 465, unicode_gc_Lu }, + { 466, 466, unicode_gc_Ll }, + { 467, 467, unicode_gc_Lu }, + { 468, 468, unicode_gc_Ll }, + { 469, 469, unicode_gc_Lu }, + { 470, 470, unicode_gc_Ll }, + { 471, 471, unicode_gc_Lu }, + { 472, 472, unicode_gc_Ll }, + { 473, 473, unicode_gc_Lu }, + { 474, 474, unicode_gc_Ll }, + { 475, 475, unicode_gc_Lu }, + { 476, 477, unicode_gc_Ll }, + { 478, 478, unicode_gc_Lu }, + { 479, 479, unicode_gc_Ll }, + { 480, 480, unicode_gc_Lu }, + { 481, 481, unicode_gc_Ll }, + { 482, 482, unicode_gc_Lu }, + { 483, 483, unicode_gc_Ll }, + { 484, 484, unicode_gc_Lu }, + { 485, 485, unicode_gc_Ll }, + { 486, 486, unicode_gc_Lu }, + { 487, 487, unicode_gc_Ll }, + { 488, 488, unicode_gc_Lu }, + { 489, 489, unicode_gc_Ll }, + { 490, 490, unicode_gc_Lu }, + { 491, 491, unicode_gc_Ll }, + { 492, 492, unicode_gc_Lu }, + { 493, 493, unicode_gc_Ll }, + { 494, 494, unicode_gc_Lu }, + { 495, 496, unicode_gc_Ll }, + { 497, 497, unicode_gc_Lu }, + { 498, 498, unicode_gc_Lt }, + { 499, 499, unicode_gc_Ll }, + { 500, 500, unicode_gc_Lu }, + { 501, 501, unicode_gc_Ll }, + { 502, 504, unicode_gc_Lu }, + { 505, 505, unicode_gc_Ll }, + { 506, 506, unicode_gc_Lu }, + { 507, 507, unicode_gc_Ll }, + { 508, 508, unicode_gc_Lu }, + { 509, 509, unicode_gc_Ll }, + { 510, 510, unicode_gc_Lu }, + { 511, 511, unicode_gc_Ll }, + { 512, 512, unicode_gc_Lu }, + { 513, 513, unicode_gc_Ll }, + { 514, 514, unicode_gc_Lu }, + { 515, 515, unicode_gc_Ll }, + { 516, 516, unicode_gc_Lu }, + { 517, 517, unicode_gc_Ll }, + { 518, 518, unicode_gc_Lu }, + { 519, 519, unicode_gc_Ll }, + { 520, 520, unicode_gc_Lu }, + { 521, 521, unicode_gc_Ll }, + { 522, 522, unicode_gc_Lu }, + { 523, 523, unicode_gc_Ll }, + { 524, 524, unicode_gc_Lu }, + { 525, 525, unicode_gc_Ll }, + { 526, 526, unicode_gc_Lu }, + { 527, 527, unicode_gc_Ll }, + { 528, 528, unicode_gc_Lu }, + { 529, 529, unicode_gc_Ll }, + { 530, 530, unicode_gc_Lu }, + { 531, 531, unicode_gc_Ll }, + { 532, 532, unicode_gc_Lu }, + { 533, 533, unicode_gc_Ll }, + { 534, 534, unicode_gc_Lu }, + { 535, 535, unicode_gc_Ll }, + { 536, 536, unicode_gc_Lu }, + { 537, 537, unicode_gc_Ll }, + { 538, 538, unicode_gc_Lu }, + { 539, 539, unicode_gc_Ll }, + { 540, 540, unicode_gc_Lu }, + { 541, 541, unicode_gc_Ll }, + { 542, 542, unicode_gc_Lu }, + { 543, 543, unicode_gc_Ll }, + { 544, 544, unicode_gc_Lu }, + { 545, 545, unicode_gc_Ll }, + { 546, 546, unicode_gc_Lu }, + { 547, 547, unicode_gc_Ll }, + { 548, 548, unicode_gc_Lu }, + { 549, 549, unicode_gc_Ll }, + { 550, 550, unicode_gc_Lu }, + { 551, 551, unicode_gc_Ll }, + { 552, 552, unicode_gc_Lu }, + { 553, 553, unicode_gc_Ll }, + { 554, 554, unicode_gc_Lu }, + { 555, 555, unicode_gc_Ll }, + { 556, 556, unicode_gc_Lu }, + { 557, 557, unicode_gc_Ll }, + { 558, 558, unicode_gc_Lu }, + { 559, 559, unicode_gc_Ll }, + { 560, 560, unicode_gc_Lu }, + { 561, 561, unicode_gc_Ll }, + { 562, 562, unicode_gc_Lu }, + { 563, 687, unicode_gc_Ll }, + { 688, 705, unicode_gc_Lm }, + { 706, 709, unicode_gc_Sk }, + { 710, 721, unicode_gc_Lm }, + { 722, 735, unicode_gc_Sk }, + { 736, 740, unicode_gc_Lm }, + { 741, 749, unicode_gc_Sk }, + { 750, 750, unicode_gc_Lm }, + { 751, 767, unicode_gc_Sk }, + { 768, 883, unicode_gc_Mn }, + { 884, 889, unicode_gc_Sk }, + { 890, 893, unicode_gc_Lm }, + { 894, 899, unicode_gc_Po }, + { 900, 901, unicode_gc_Sk }, + { 902, 902, unicode_gc_Lu }, + { 903, 903, unicode_gc_Po }, + { 904, 911, unicode_gc_Lu }, + { 912, 912, unicode_gc_Ll }, + { 913, 939, unicode_gc_Lu }, + { 940, 977, unicode_gc_Ll }, + { 978, 980, unicode_gc_Lu }, + { 981, 983, unicode_gc_Ll }, + { 984, 984, unicode_gc_Lu }, + { 985, 985, unicode_gc_Ll }, + { 986, 986, unicode_gc_Lu }, + { 987, 987, unicode_gc_Ll }, + { 988, 988, unicode_gc_Lu }, + { 989, 989, unicode_gc_Ll }, + { 990, 990, unicode_gc_Lu }, + { 991, 991, unicode_gc_Ll }, + { 992, 992, unicode_gc_Lu }, + { 993, 993, unicode_gc_Ll }, + { 994, 994, unicode_gc_Lu }, + { 995, 995, unicode_gc_Ll }, + { 996, 996, unicode_gc_Lu }, + { 997, 997, unicode_gc_Ll }, + { 998, 998, unicode_gc_Lu }, + { 999, 999, unicode_gc_Ll }, + { 1000, 1000, unicode_gc_Lu }, + { 1001, 1001, unicode_gc_Ll }, + { 1002, 1002, unicode_gc_Lu }, + { 1003, 1003, unicode_gc_Ll }, + { 1004, 1004, unicode_gc_Lu }, + { 1005, 1005, unicode_gc_Ll }, + { 1006, 1006, unicode_gc_Lu }, + { 1007, 1011, unicode_gc_Ll }, + { 1012, 1012, unicode_gc_Lu }, + { 1013, 1013, unicode_gc_Ll }, + { 1014, 1014, unicode_gc_Sm }, + { 1015, 1015, unicode_gc_Lu }, + { 1016, 1016, unicode_gc_Ll }, + { 1017, 1018, unicode_gc_Lu }, + { 1019, 1023, unicode_gc_Ll }, + { 1024, 1071, unicode_gc_Lu }, + { 1072, 1119, unicode_gc_Ll }, + { 1120, 1120, unicode_gc_Lu }, + { 1121, 1121, unicode_gc_Ll }, + { 1122, 1122, unicode_gc_Lu }, + { 1123, 1123, unicode_gc_Ll }, + { 1124, 1124, unicode_gc_Lu }, + { 1125, 1125, unicode_gc_Ll }, + { 1126, 1126, unicode_gc_Lu }, + { 1127, 1127, unicode_gc_Ll }, + { 1128, 1128, unicode_gc_Lu }, + { 1129, 1129, unicode_gc_Ll }, + { 1130, 1130, unicode_gc_Lu }, + { 1131, 1131, unicode_gc_Ll }, + { 1132, 1132, unicode_gc_Lu }, + { 1133, 1133, unicode_gc_Ll }, + { 1134, 1134, unicode_gc_Lu }, + { 1135, 1135, unicode_gc_Ll }, + { 1136, 1136, unicode_gc_Lu }, + { 1137, 1137, unicode_gc_Ll }, + { 1138, 1138, unicode_gc_Lu }, + { 1139, 1139, unicode_gc_Ll }, + { 1140, 1140, unicode_gc_Lu }, + { 1141, 1141, unicode_gc_Ll }, + { 1142, 1142, unicode_gc_Lu }, + { 1143, 1143, unicode_gc_Ll }, + { 1144, 1144, unicode_gc_Lu }, + { 1145, 1145, unicode_gc_Ll }, + { 1146, 1146, unicode_gc_Lu }, + { 1147, 1147, unicode_gc_Ll }, + { 1148, 1148, unicode_gc_Lu }, + { 1149, 1149, unicode_gc_Ll }, + { 1150, 1150, unicode_gc_Lu }, + { 1151, 1151, unicode_gc_Ll }, + { 1152, 1152, unicode_gc_Lu }, + { 1153, 1153, unicode_gc_Ll }, + { 1154, 1154, unicode_gc_So }, + { 1155, 1159, unicode_gc_Mn }, + { 1160, 1161, unicode_gc_Me }, + { 1162, 1162, unicode_gc_Lu }, + { 1163, 1163, unicode_gc_Ll }, + { 1164, 1164, unicode_gc_Lu }, + { 1165, 1165, unicode_gc_Ll }, + { 1166, 1166, unicode_gc_Lu }, + { 1167, 1167, unicode_gc_Ll }, + { 1168, 1168, unicode_gc_Lu }, + { 1169, 1169, unicode_gc_Ll }, + { 1170, 1170, unicode_gc_Lu }, + { 1171, 1171, unicode_gc_Ll }, + { 1172, 1172, unicode_gc_Lu }, + { 1173, 1173, unicode_gc_Ll }, + { 1174, 1174, unicode_gc_Lu }, + { 1175, 1175, unicode_gc_Ll }, + { 1176, 1176, unicode_gc_Lu }, + { 1177, 1177, unicode_gc_Ll }, + { 1178, 1178, unicode_gc_Lu }, + { 1179, 1179, unicode_gc_Ll }, + { 1180, 1180, unicode_gc_Lu }, + { 1181, 1181, unicode_gc_Ll }, + { 1182, 1182, unicode_gc_Lu }, + { 1183, 1183, unicode_gc_Ll }, + { 1184, 1184, unicode_gc_Lu }, + { 1185, 1185, unicode_gc_Ll }, + { 1186, 1186, unicode_gc_Lu }, + { 1187, 1187, unicode_gc_Ll }, + { 1188, 1188, unicode_gc_Lu }, + { 1189, 1189, unicode_gc_Ll }, + { 1190, 1190, unicode_gc_Lu }, + { 1191, 1191, unicode_gc_Ll }, + { 1192, 1192, unicode_gc_Lu }, + { 1193, 1193, unicode_gc_Ll }, + { 1194, 1194, unicode_gc_Lu }, + { 1195, 1195, unicode_gc_Ll }, + { 1196, 1196, unicode_gc_Lu }, + { 1197, 1197, unicode_gc_Ll }, + { 1198, 1198, unicode_gc_Lu }, + { 1199, 1199, unicode_gc_Ll }, + { 1200, 1200, unicode_gc_Lu }, + { 1201, 1201, unicode_gc_Ll }, + { 1202, 1202, unicode_gc_Lu }, + { 1203, 1203, unicode_gc_Ll }, + { 1204, 1204, unicode_gc_Lu }, + { 1205, 1205, unicode_gc_Ll }, + { 1206, 1206, unicode_gc_Lu }, + { 1207, 1207, unicode_gc_Ll }, + { 1208, 1208, unicode_gc_Lu }, + { 1209, 1209, unicode_gc_Ll }, + { 1210, 1210, unicode_gc_Lu }, + { 1211, 1211, unicode_gc_Ll }, + { 1212, 1212, unicode_gc_Lu }, + { 1213, 1213, unicode_gc_Ll }, + { 1214, 1214, unicode_gc_Lu }, + { 1215, 1215, unicode_gc_Ll }, + { 1216, 1217, unicode_gc_Lu }, + { 1218, 1218, unicode_gc_Ll }, + { 1219, 1219, unicode_gc_Lu }, + { 1220, 1220, unicode_gc_Ll }, + { 1221, 1221, unicode_gc_Lu }, + { 1222, 1222, unicode_gc_Ll }, + { 1223, 1223, unicode_gc_Lu }, + { 1224, 1224, unicode_gc_Ll }, + { 1225, 1225, unicode_gc_Lu }, + { 1226, 1226, unicode_gc_Ll }, + { 1227, 1227, unicode_gc_Lu }, + { 1228, 1228, unicode_gc_Ll }, + { 1229, 1229, unicode_gc_Lu }, + { 1230, 1231, unicode_gc_Ll }, + { 1232, 1232, unicode_gc_Lu }, + { 1233, 1233, unicode_gc_Ll }, + { 1234, 1234, unicode_gc_Lu }, + { 1235, 1235, unicode_gc_Ll }, + { 1236, 1236, unicode_gc_Lu }, + { 1237, 1237, unicode_gc_Ll }, + { 1238, 1238, unicode_gc_Lu }, + { 1239, 1239, unicode_gc_Ll }, + { 1240, 1240, unicode_gc_Lu }, + { 1241, 1241, unicode_gc_Ll }, + { 1242, 1242, unicode_gc_Lu }, + { 1243, 1243, unicode_gc_Ll }, + { 1244, 1244, unicode_gc_Lu }, + { 1245, 1245, unicode_gc_Ll }, + { 1246, 1246, unicode_gc_Lu }, + { 1247, 1247, unicode_gc_Ll }, + { 1248, 1248, unicode_gc_Lu }, + { 1249, 1249, unicode_gc_Ll }, + { 1250, 1250, unicode_gc_Lu }, + { 1251, 1251, unicode_gc_Ll }, + { 1252, 1252, unicode_gc_Lu }, + { 1253, 1253, unicode_gc_Ll }, + { 1254, 1254, unicode_gc_Lu }, + { 1255, 1255, unicode_gc_Ll }, + { 1256, 1256, unicode_gc_Lu }, + { 1257, 1257, unicode_gc_Ll }, + { 1258, 1258, unicode_gc_Lu }, + { 1259, 1259, unicode_gc_Ll }, + { 1260, 1260, unicode_gc_Lu }, + { 1261, 1261, unicode_gc_Ll }, + { 1262, 1262, unicode_gc_Lu }, + { 1263, 1263, unicode_gc_Ll }, + { 1264, 1264, unicode_gc_Lu }, + { 1265, 1265, unicode_gc_Ll }, + { 1266, 1266, unicode_gc_Lu }, + { 1267, 1267, unicode_gc_Ll }, + { 1268, 1268, unicode_gc_Lu }, + { 1269, 1271, unicode_gc_Ll }, + { 1272, 1272, unicode_gc_Lu }, + { 1273, 1279, unicode_gc_Ll }, + { 1280, 1280, unicode_gc_Lu }, + { 1281, 1281, unicode_gc_Ll }, + { 1282, 1282, unicode_gc_Lu }, + { 1283, 1283, unicode_gc_Ll }, + { 1284, 1284, unicode_gc_Lu }, + { 1285, 1285, unicode_gc_Ll }, + { 1286, 1286, unicode_gc_Lu }, + { 1287, 1287, unicode_gc_Ll }, + { 1288, 1288, unicode_gc_Lu }, + { 1289, 1289, unicode_gc_Ll }, + { 1290, 1290, unicode_gc_Lu }, + { 1291, 1291, unicode_gc_Ll }, + { 1292, 1292, unicode_gc_Lu }, + { 1293, 1293, unicode_gc_Ll }, + { 1294, 1294, unicode_gc_Lu }, + { 1295, 1328, unicode_gc_Ll }, + { 1329, 1368, unicode_gc_Lu }, + { 1369, 1369, unicode_gc_Lm }, + { 1370, 1376, unicode_gc_Po }, + { 1377, 1416, unicode_gc_Ll }, + { 1417, 1417, unicode_gc_Po }, + { 1418, 1424, unicode_gc_Pd }, + { 1425, 1469, unicode_gc_Mn }, + { 1470, 1470, unicode_gc_Po }, + { 1471, 1471, unicode_gc_Mn }, + { 1472, 1472, unicode_gc_Po }, + { 1473, 1474, unicode_gc_Mn }, + { 1475, 1475, unicode_gc_Po }, + { 1476, 1487, unicode_gc_Mn }, + { 1488, 1522, unicode_gc_Lo }, + { 1523, 1535, unicode_gc_Po }, + { 1536, 1547, unicode_gc_Cf }, + { 1548, 1549, unicode_gc_Po }, + { 1550, 1551, unicode_gc_So }, + { 1552, 1562, unicode_gc_Mn }, + { 1563, 1568, unicode_gc_Po }, + { 1569, 1599, unicode_gc_Lo }, + { 1600, 1600, unicode_gc_Lm }, + { 1601, 1610, unicode_gc_Lo }, + { 1611, 1631, unicode_gc_Mn }, + { 1632, 1641, unicode_gc_Nd }, + { 1642, 1645, unicode_gc_Po }, + { 1646, 1647, unicode_gc_Lo }, + { 1648, 1648, unicode_gc_Mn }, + { 1649, 1747, unicode_gc_Lo }, + { 1748, 1748, unicode_gc_Po }, + { 1749, 1749, unicode_gc_Lo }, + { 1750, 1756, unicode_gc_Mn }, + { 1757, 1757, unicode_gc_Cf }, + { 1758, 1758, unicode_gc_Me }, + { 1759, 1764, unicode_gc_Mn }, + { 1765, 1766, unicode_gc_Lm }, + { 1767, 1768, unicode_gc_Mn }, + { 1769, 1769, unicode_gc_So }, + { 1770, 1773, unicode_gc_Mn }, + { 1774, 1775, unicode_gc_Lo }, + { 1776, 1785, unicode_gc_Nd }, + { 1786, 1788, unicode_gc_Lo }, + { 1789, 1790, unicode_gc_So }, + { 1791, 1791, unicode_gc_Lo }, + { 1792, 1806, unicode_gc_Po }, + { 1807, 1807, unicode_gc_Cf }, + { 1808, 1808, unicode_gc_Lo }, + { 1809, 1809, unicode_gc_Mn }, + { 1810, 1839, unicode_gc_Lo }, + { 1840, 1868, unicode_gc_Mn }, + { 1869, 1957, unicode_gc_Lo }, + { 1958, 1968, unicode_gc_Mn }, + { 1969, 2304, unicode_gc_Lo }, + { 2305, 2306, unicode_gc_Mn }, + { 2307, 2307, unicode_gc_Mc }, + { 2308, 2363, unicode_gc_Lo }, + { 2364, 2364, unicode_gc_Mn }, + { 2365, 2365, unicode_gc_Lo }, + { 2366, 2368, unicode_gc_Mc }, + { 2369, 2376, unicode_gc_Mn }, + { 2377, 2380, unicode_gc_Mc }, + { 2381, 2383, unicode_gc_Mn }, + { 2384, 2384, unicode_gc_Lo }, + { 2385, 2391, unicode_gc_Mn }, + { 2392, 2401, unicode_gc_Lo }, + { 2402, 2403, unicode_gc_Mn }, + { 2404, 2405, unicode_gc_Po }, + { 2406, 2415, unicode_gc_Nd }, + { 2416, 2432, unicode_gc_Po }, + { 2433, 2433, unicode_gc_Mn }, + { 2434, 2436, unicode_gc_Mc }, + { 2437, 2491, unicode_gc_Lo }, + { 2492, 2492, unicode_gc_Mn }, + { 2493, 2493, unicode_gc_Lo }, + { 2494, 2496, unicode_gc_Mc }, + { 2497, 2502, unicode_gc_Mn }, + { 2503, 2508, unicode_gc_Mc }, + { 2509, 2518, unicode_gc_Mn }, + { 2519, 2523, unicode_gc_Mc }, + { 2524, 2529, unicode_gc_Lo }, + { 2530, 2533, unicode_gc_Mn }, + { 2534, 2543, unicode_gc_Nd }, + { 2544, 2545, unicode_gc_Lo }, + { 2546, 2547, unicode_gc_Sc }, + { 2548, 2553, unicode_gc_No }, + { 2554, 2560, unicode_gc_So }, + { 2561, 2562, unicode_gc_Mn }, + { 2563, 2564, unicode_gc_Mc }, + { 2565, 2619, unicode_gc_Lo }, + { 2620, 2621, unicode_gc_Mn }, + { 2622, 2624, unicode_gc_Mc }, + { 2625, 2648, unicode_gc_Mn }, + { 2649, 2661, unicode_gc_Lo }, + { 2662, 2671, unicode_gc_Nd }, + { 2672, 2673, unicode_gc_Mn }, + { 2674, 2688, unicode_gc_Lo }, + { 2689, 2690, unicode_gc_Mn }, + { 2691, 2692, unicode_gc_Mc }, + { 2693, 2747, unicode_gc_Lo }, + { 2748, 2748, unicode_gc_Mn }, + { 2749, 2749, unicode_gc_Lo }, + { 2750, 2752, unicode_gc_Mc }, + { 2753, 2760, unicode_gc_Mn }, + { 2761, 2764, unicode_gc_Mc }, + { 2765, 2767, unicode_gc_Mn }, + { 2768, 2785, unicode_gc_Lo }, + { 2786, 2789, unicode_gc_Mn }, + { 2790, 2800, unicode_gc_Nd }, + { 2801, 2816, unicode_gc_Sc }, + { 2817, 2817, unicode_gc_Mn }, + { 2818, 2820, unicode_gc_Mc }, + { 2821, 2875, unicode_gc_Lo }, + { 2876, 2876, unicode_gc_Mn }, + { 2877, 2877, unicode_gc_Lo }, + { 2878, 2878, unicode_gc_Mc }, + { 2879, 2879, unicode_gc_Mn }, + { 2880, 2880, unicode_gc_Mc }, + { 2881, 2886, unicode_gc_Mn }, + { 2887, 2892, unicode_gc_Mc }, + { 2893, 2902, unicode_gc_Mn }, + { 2903, 2907, unicode_gc_Mc }, + { 2908, 2917, unicode_gc_Lo }, + { 2918, 2927, unicode_gc_Nd }, + { 2928, 2928, unicode_gc_So }, + { 2929, 2945, unicode_gc_Lo }, + { 2946, 2946, unicode_gc_Mn }, + { 2947, 3005, unicode_gc_Lo }, + { 3006, 3007, unicode_gc_Mc }, + { 3008, 3008, unicode_gc_Mn }, + { 3009, 3020, unicode_gc_Mc }, + { 3021, 3030, unicode_gc_Mn }, + { 3031, 3046, unicode_gc_Mc }, + { 3047, 3055, unicode_gc_Nd }, + { 3056, 3058, unicode_gc_No }, + { 3059, 3064, unicode_gc_So }, + { 3065, 3065, unicode_gc_Sc }, + { 3066, 3072, unicode_gc_So }, + { 3073, 3076, unicode_gc_Mc }, + { 3077, 3133, unicode_gc_Lo }, + { 3134, 3136, unicode_gc_Mn }, + { 3137, 3141, unicode_gc_Mc }, + { 3142, 3167, unicode_gc_Mn }, + { 3168, 3173, unicode_gc_Lo }, + { 3174, 3201, unicode_gc_Nd }, + { 3202, 3204, unicode_gc_Mc }, + { 3205, 3259, unicode_gc_Lo }, + { 3260, 3260, unicode_gc_Mn }, + { 3261, 3261, unicode_gc_Lo }, + { 3262, 3262, unicode_gc_Mc }, + { 3263, 3263, unicode_gc_Mn }, + { 3264, 3269, unicode_gc_Mc }, + { 3270, 3270, unicode_gc_Mn }, + { 3271, 3275, unicode_gc_Mc }, + { 3276, 3284, unicode_gc_Mn }, + { 3285, 3293, unicode_gc_Mc }, + { 3294, 3301, unicode_gc_Lo }, + { 3302, 3329, unicode_gc_Nd }, + { 3330, 3332, unicode_gc_Mc }, + { 3333, 3389, unicode_gc_Lo }, + { 3390, 3392, unicode_gc_Mc }, + { 3393, 3397, unicode_gc_Mn }, + { 3398, 3404, unicode_gc_Mc }, + { 3405, 3414, unicode_gc_Mn }, + { 3415, 3423, unicode_gc_Mc }, + { 3424, 3429, unicode_gc_Lo }, + { 3430, 3457, unicode_gc_Nd }, + { 3458, 3460, unicode_gc_Mc }, + { 3461, 3529, unicode_gc_Lo }, + { 3530, 3534, unicode_gc_Mn }, + { 3535, 3537, unicode_gc_Mc }, + { 3538, 3543, unicode_gc_Mn }, + { 3544, 3571, unicode_gc_Mc }, + { 3572, 3584, unicode_gc_Po }, + { 3585, 3632, unicode_gc_Lo }, + { 3633, 3633, unicode_gc_Mn }, + { 3634, 3635, unicode_gc_Lo }, + { 3636, 3646, unicode_gc_Mn }, + { 3647, 3647, unicode_gc_Sc }, + { 3648, 3653, unicode_gc_Lo }, + { 3654, 3654, unicode_gc_Lm }, + { 3655, 3662, unicode_gc_Mn }, + { 3663, 3663, unicode_gc_Po }, + { 3664, 3673, unicode_gc_Nd }, + { 3674, 3712, unicode_gc_Po }, + { 3713, 3760, unicode_gc_Lo }, + { 3761, 3761, unicode_gc_Mn }, + { 3762, 3763, unicode_gc_Lo }, + { 3764, 3772, unicode_gc_Mn }, + { 3773, 3781, unicode_gc_Lo }, + { 3782, 3783, unicode_gc_Lm }, + { 3784, 3791, unicode_gc_Mn }, + { 3792, 3803, unicode_gc_Nd }, + { 3804, 3840, unicode_gc_Lo }, + { 3841, 3843, unicode_gc_So }, + { 3844, 3858, unicode_gc_Po }, + { 3859, 3863, unicode_gc_So }, + { 3864, 3865, unicode_gc_Mn }, + { 3866, 3871, unicode_gc_So }, + { 3872, 3881, unicode_gc_Nd }, + { 3882, 3891, unicode_gc_No }, + { 3892, 3892, unicode_gc_So }, + { 3893, 3893, unicode_gc_Mn }, + { 3894, 3894, unicode_gc_So }, + { 3895, 3895, unicode_gc_Mn }, + { 3896, 3896, unicode_gc_So }, + { 3897, 3897, unicode_gc_Mn }, + { 3898, 3898, unicode_gc_Ps }, + { 3899, 3899, unicode_gc_Pe }, + { 3900, 3900, unicode_gc_Ps }, + { 3901, 3901, unicode_gc_Pe }, + { 3902, 3903, unicode_gc_Mc }, + { 3904, 3952, unicode_gc_Lo }, + { 3953, 3966, unicode_gc_Mn }, + { 3967, 3967, unicode_gc_Mc }, + { 3968, 3972, unicode_gc_Mn }, + { 3973, 3973, unicode_gc_Po }, + { 3974, 3975, unicode_gc_Mn }, + { 3976, 3983, unicode_gc_Lo }, + { 3984, 4029, unicode_gc_Mn }, + { 4030, 4037, unicode_gc_So }, + { 4038, 4038, unicode_gc_Mn }, + { 4039, 4095, unicode_gc_So }, + { 4096, 4139, unicode_gc_Lo }, + { 4140, 4140, unicode_gc_Mc }, + { 4141, 4144, unicode_gc_Mn }, + { 4145, 4145, unicode_gc_Mc }, + { 4146, 4151, unicode_gc_Mn }, + { 4152, 4152, unicode_gc_Mc }, + { 4153, 4159, unicode_gc_Mn }, + { 4160, 4169, unicode_gc_Nd }, + { 4170, 4175, unicode_gc_Po }, + { 4176, 4181, unicode_gc_Lo }, + { 4182, 4183, unicode_gc_Mc }, + { 4184, 4255, unicode_gc_Mn }, + { 4256, 4303, unicode_gc_Lu }, + { 4304, 4346, unicode_gc_Lo }, + { 4347, 4351, unicode_gc_Po }, + { 4352, 4960, unicode_gc_Lo }, + { 4961, 4968, unicode_gc_Po }, + { 4969, 4977, unicode_gc_Nd }, + { 4978, 5023, unicode_gc_No }, + { 5024, 5740, unicode_gc_Lo }, + { 5741, 5742, unicode_gc_Po }, + { 5743, 5759, unicode_gc_Lo }, + { 5760, 5760, unicode_gc_Zs }, + { 5761, 5786, unicode_gc_Lo }, + { 5787, 5787, unicode_gc_Ps }, + { 5788, 5791, unicode_gc_Pe }, + { 5792, 5866, unicode_gc_Lo }, + { 5867, 5869, unicode_gc_Po }, + { 5870, 5887, unicode_gc_Nl }, + { 5888, 5905, unicode_gc_Lo }, + { 5906, 5919, unicode_gc_Mn }, + { 5920, 5937, unicode_gc_Lo }, + { 5938, 5940, unicode_gc_Mn }, + { 5941, 5951, unicode_gc_Po }, + { 5952, 5969, unicode_gc_Lo }, + { 5970, 5983, unicode_gc_Mn }, + { 5984, 6001, unicode_gc_Lo }, + { 6002, 6015, unicode_gc_Mn }, + { 6016, 6067, unicode_gc_Lo }, + { 6068, 6069, unicode_gc_Cf }, + { 6070, 6070, unicode_gc_Mc }, + { 6071, 6077, unicode_gc_Mn }, + { 6078, 6085, unicode_gc_Mc }, + { 6086, 6086, unicode_gc_Mn }, + { 6087, 6088, unicode_gc_Mc }, + { 6089, 6099, unicode_gc_Mn }, + { 6100, 6102, unicode_gc_Po }, + { 6103, 6103, unicode_gc_Lm }, + { 6104, 6106, unicode_gc_Po }, + { 6107, 6107, unicode_gc_Sc }, + { 6108, 6108, unicode_gc_Lo }, + { 6109, 6111, unicode_gc_Mn }, + { 6112, 6127, unicode_gc_Nd }, + { 6128, 6143, unicode_gc_No }, + { 6144, 6149, unicode_gc_Po }, + { 6150, 6150, unicode_gc_Pd }, + { 6151, 6154, unicode_gc_Po }, + { 6155, 6157, unicode_gc_Mn }, + { 6158, 6159, unicode_gc_Zs }, + { 6160, 6175, unicode_gc_Nd }, + { 6176, 6210, unicode_gc_Lo }, + { 6211, 6211, unicode_gc_Lm }, + { 6212, 6312, unicode_gc_Lo }, + { 6313, 6399, unicode_gc_Mn }, + { 6400, 6431, unicode_gc_Lo }, + { 6432, 6434, unicode_gc_Mn }, + { 6435, 6438, unicode_gc_Mc }, + { 6439, 6440, unicode_gc_Mn }, + { 6441, 6449, unicode_gc_Mc }, + { 6450, 6450, unicode_gc_Mn }, + { 6451, 6456, unicode_gc_Mc }, + { 6457, 6463, unicode_gc_Mn }, + { 6464, 6467, unicode_gc_So }, + { 6468, 6469, unicode_gc_Po }, + { 6470, 6479, unicode_gc_Nd }, + { 6480, 6623, unicode_gc_Lo }, + { 6624, 7423, unicode_gc_So }, + { 7424, 7467, unicode_gc_Ll }, + { 7468, 7521, unicode_gc_Lm }, + { 7522, 7679, unicode_gc_Ll }, + { 7680, 7680, unicode_gc_Lu }, + { 7681, 7681, unicode_gc_Ll }, + { 7682, 7682, unicode_gc_Lu }, + { 7683, 7683, unicode_gc_Ll }, + { 7684, 7684, unicode_gc_Lu }, + { 7685, 7685, unicode_gc_Ll }, + { 7686, 7686, unicode_gc_Lu }, + { 7687, 7687, unicode_gc_Ll }, + { 7688, 7688, unicode_gc_Lu }, + { 7689, 7689, unicode_gc_Ll }, + { 7690, 7690, unicode_gc_Lu }, + { 7691, 7691, unicode_gc_Ll }, + { 7692, 7692, unicode_gc_Lu }, + { 7693, 7693, unicode_gc_Ll }, + { 7694, 7694, unicode_gc_Lu }, + { 7695, 7695, unicode_gc_Ll }, + { 7696, 7696, unicode_gc_Lu }, + { 7697, 7697, unicode_gc_Ll }, + { 7698, 7698, unicode_gc_Lu }, + { 7699, 7699, unicode_gc_Ll }, + { 7700, 7700, unicode_gc_Lu }, + { 7701, 7701, unicode_gc_Ll }, + { 7702, 7702, unicode_gc_Lu }, + { 7703, 7703, unicode_gc_Ll }, + { 7704, 7704, unicode_gc_Lu }, + { 7705, 7705, unicode_gc_Ll }, + { 7706, 7706, unicode_gc_Lu }, + { 7707, 7707, unicode_gc_Ll }, + { 7708, 7708, unicode_gc_Lu }, + { 7709, 7709, unicode_gc_Ll }, + { 7710, 7710, unicode_gc_Lu }, + { 7711, 7711, unicode_gc_Ll }, + { 7712, 7712, unicode_gc_Lu }, + { 7713, 7713, unicode_gc_Ll }, + { 7714, 7714, unicode_gc_Lu }, + { 7715, 7715, unicode_gc_Ll }, + { 7716, 7716, unicode_gc_Lu }, + { 7717, 7717, unicode_gc_Ll }, + { 7718, 7718, unicode_gc_Lu }, + { 7719, 7719, unicode_gc_Ll }, + { 7720, 7720, unicode_gc_Lu }, + { 7721, 7721, unicode_gc_Ll }, + { 7722, 7722, unicode_gc_Lu }, + { 7723, 7723, unicode_gc_Ll }, + { 7724, 7724, unicode_gc_Lu }, + { 7725, 7725, unicode_gc_Ll }, + { 7726, 7726, unicode_gc_Lu }, + { 7727, 7727, unicode_gc_Ll }, + { 7728, 7728, unicode_gc_Lu }, + { 7729, 7729, unicode_gc_Ll }, + { 7730, 7730, unicode_gc_Lu }, + { 7731, 7731, unicode_gc_Ll }, + { 7732, 7732, unicode_gc_Lu }, + { 7733, 7733, unicode_gc_Ll }, + { 7734, 7734, unicode_gc_Lu }, + { 7735, 7735, unicode_gc_Ll }, + { 7736, 7736, unicode_gc_Lu }, + { 7737, 7737, unicode_gc_Ll }, + { 7738, 7738, unicode_gc_Lu }, + { 7739, 7739, unicode_gc_Ll }, + { 7740, 7740, unicode_gc_Lu }, + { 7741, 7741, unicode_gc_Ll }, + { 7742, 7742, unicode_gc_Lu }, + { 7743, 7743, unicode_gc_Ll }, + { 7744, 7744, unicode_gc_Lu }, + { 7745, 7745, unicode_gc_Ll }, + { 7746, 7746, unicode_gc_Lu }, + { 7747, 7747, unicode_gc_Ll }, + { 7748, 7748, unicode_gc_Lu }, + { 7749, 7749, unicode_gc_Ll }, + { 7750, 7750, unicode_gc_Lu }, + { 7751, 7751, unicode_gc_Ll }, + { 7752, 7752, unicode_gc_Lu }, + { 7753, 7753, unicode_gc_Ll }, + { 7754, 7754, unicode_gc_Lu }, + { 7755, 7755, unicode_gc_Ll }, + { 7756, 7756, unicode_gc_Lu }, + { 7757, 7757, unicode_gc_Ll }, + { 7758, 7758, unicode_gc_Lu }, + { 7759, 7759, unicode_gc_Ll }, + { 7760, 7760, unicode_gc_Lu }, + { 7761, 7761, unicode_gc_Ll }, + { 7762, 7762, unicode_gc_Lu }, + { 7763, 7763, unicode_gc_Ll }, + { 7764, 7764, unicode_gc_Lu }, + { 7765, 7765, unicode_gc_Ll }, + { 7766, 7766, unicode_gc_Lu }, + { 7767, 7767, unicode_gc_Ll }, + { 7768, 7768, unicode_gc_Lu }, + { 7769, 7769, unicode_gc_Ll }, + { 7770, 7770, unicode_gc_Lu }, + { 7771, 7771, unicode_gc_Ll }, + { 7772, 7772, unicode_gc_Lu }, + { 7773, 7773, unicode_gc_Ll }, + { 7774, 7774, unicode_gc_Lu }, + { 7775, 7775, unicode_gc_Ll }, + { 7776, 7776, unicode_gc_Lu }, + { 7777, 7777, unicode_gc_Ll }, + { 7778, 7778, unicode_gc_Lu }, + { 7779, 7779, unicode_gc_Ll }, + { 7780, 7780, unicode_gc_Lu }, + { 7781, 7781, unicode_gc_Ll }, + { 7782, 7782, unicode_gc_Lu }, + { 7783, 7783, unicode_gc_Ll }, + { 7784, 7784, unicode_gc_Lu }, + { 7785, 7785, unicode_gc_Ll }, + { 7786, 7786, unicode_gc_Lu }, + { 7787, 7787, unicode_gc_Ll }, + { 7788, 7788, unicode_gc_Lu }, + { 7789, 7789, unicode_gc_Ll }, + { 7790, 7790, unicode_gc_Lu }, + { 7791, 7791, unicode_gc_Ll }, + { 7792, 7792, unicode_gc_Lu }, + { 7793, 7793, unicode_gc_Ll }, + { 7794, 7794, unicode_gc_Lu }, + { 7795, 7795, unicode_gc_Ll }, + { 7796, 7796, unicode_gc_Lu }, + { 7797, 7797, unicode_gc_Ll }, + { 7798, 7798, unicode_gc_Lu }, + { 7799, 7799, unicode_gc_Ll }, + { 7800, 7800, unicode_gc_Lu }, + { 7801, 7801, unicode_gc_Ll }, + { 7802, 7802, unicode_gc_Lu }, + { 7803, 7803, unicode_gc_Ll }, + { 7804, 7804, unicode_gc_Lu }, + { 7805, 7805, unicode_gc_Ll }, + { 7806, 7806, unicode_gc_Lu }, + { 7807, 7807, unicode_gc_Ll }, + { 7808, 7808, unicode_gc_Lu }, + { 7809, 7809, unicode_gc_Ll }, + { 7810, 7810, unicode_gc_Lu }, + { 7811, 7811, unicode_gc_Ll }, + { 7812, 7812, unicode_gc_Lu }, + { 7813, 7813, unicode_gc_Ll }, + { 7814, 7814, unicode_gc_Lu }, + { 7815, 7815, unicode_gc_Ll }, + { 7816, 7816, unicode_gc_Lu }, + { 7817, 7817, unicode_gc_Ll }, + { 7818, 7818, unicode_gc_Lu }, + { 7819, 7819, unicode_gc_Ll }, + { 7820, 7820, unicode_gc_Lu }, + { 7821, 7821, unicode_gc_Ll }, + { 7822, 7822, unicode_gc_Lu }, + { 7823, 7823, unicode_gc_Ll }, + { 7824, 7824, unicode_gc_Lu }, + { 7825, 7825, unicode_gc_Ll }, + { 7826, 7826, unicode_gc_Lu }, + { 7827, 7827, unicode_gc_Ll }, + { 7828, 7828, unicode_gc_Lu }, + { 7829, 7839, unicode_gc_Ll }, + { 7840, 7840, unicode_gc_Lu }, + { 7841, 7841, unicode_gc_Ll }, + { 7842, 7842, unicode_gc_Lu }, + { 7843, 7843, unicode_gc_Ll }, + { 7844, 7844, unicode_gc_Lu }, + { 7845, 7845, unicode_gc_Ll }, + { 7846, 7846, unicode_gc_Lu }, + { 7847, 7847, unicode_gc_Ll }, + { 7848, 7848, unicode_gc_Lu }, + { 7849, 7849, unicode_gc_Ll }, + { 7850, 7850, unicode_gc_Lu }, + { 7851, 7851, unicode_gc_Ll }, + { 7852, 7852, unicode_gc_Lu }, + { 7853, 7853, unicode_gc_Ll }, + { 7854, 7854, unicode_gc_Lu }, + { 7855, 7855, unicode_gc_Ll }, + { 7856, 7856, unicode_gc_Lu }, + { 7857, 7857, unicode_gc_Ll }, + { 7858, 7858, unicode_gc_Lu }, + { 7859, 7859, unicode_gc_Ll }, + { 7860, 7860, unicode_gc_Lu }, + { 7861, 7861, unicode_gc_Ll }, + { 7862, 7862, unicode_gc_Lu }, + { 7863, 7863, unicode_gc_Ll }, + { 7864, 7864, unicode_gc_Lu }, + { 7865, 7865, unicode_gc_Ll }, + { 7866, 7866, unicode_gc_Lu }, + { 7867, 7867, unicode_gc_Ll }, + { 7868, 7868, unicode_gc_Lu }, + { 7869, 7869, unicode_gc_Ll }, + { 7870, 7870, unicode_gc_Lu }, + { 7871, 7871, unicode_gc_Ll }, + { 7872, 7872, unicode_gc_Lu }, + { 7873, 7873, unicode_gc_Ll }, + { 7874, 7874, unicode_gc_Lu }, + { 7875, 7875, unicode_gc_Ll }, + { 7876, 7876, unicode_gc_Lu }, + { 7877, 7877, unicode_gc_Ll }, + { 7878, 7878, unicode_gc_Lu }, + { 7879, 7879, unicode_gc_Ll }, + { 7880, 7880, unicode_gc_Lu }, + { 7881, 7881, unicode_gc_Ll }, + { 7882, 7882, unicode_gc_Lu }, + { 7883, 7883, unicode_gc_Ll }, + { 7884, 7884, unicode_gc_Lu }, + { 7885, 7885, unicode_gc_Ll }, + { 7886, 7886, unicode_gc_Lu }, + { 7887, 7887, unicode_gc_Ll }, + { 7888, 7888, unicode_gc_Lu }, + { 7889, 7889, unicode_gc_Ll }, + { 7890, 7890, unicode_gc_Lu }, + { 7891, 7891, unicode_gc_Ll }, + { 7892, 7892, unicode_gc_Lu }, + { 7893, 7893, unicode_gc_Ll }, + { 7894, 7894, unicode_gc_Lu }, + { 7895, 7895, unicode_gc_Ll }, + { 7896, 7896, unicode_gc_Lu }, + { 7897, 7897, unicode_gc_Ll }, + { 7898, 7898, unicode_gc_Lu }, + { 7899, 7899, unicode_gc_Ll }, + { 7900, 7900, unicode_gc_Lu }, + { 7901, 7901, unicode_gc_Ll }, + { 7902, 7902, unicode_gc_Lu }, + { 7903, 7903, unicode_gc_Ll }, + { 7904, 7904, unicode_gc_Lu }, + { 7905, 7905, unicode_gc_Ll }, + { 7906, 7906, unicode_gc_Lu }, + { 7907, 7907, unicode_gc_Ll }, + { 7908, 7908, unicode_gc_Lu }, + { 7909, 7909, unicode_gc_Ll }, + { 7910, 7910, unicode_gc_Lu }, + { 7911, 7911, unicode_gc_Ll }, + { 7912, 7912, unicode_gc_Lu }, + { 7913, 7913, unicode_gc_Ll }, + { 7914, 7914, unicode_gc_Lu }, + { 7915, 7915, unicode_gc_Ll }, + { 7916, 7916, unicode_gc_Lu }, + { 7917, 7917, unicode_gc_Ll }, + { 7918, 7918, unicode_gc_Lu }, + { 7919, 7919, unicode_gc_Ll }, + { 7920, 7920, unicode_gc_Lu }, + { 7921, 7921, unicode_gc_Ll }, + { 7922, 7922, unicode_gc_Lu }, + { 7923, 7923, unicode_gc_Ll }, + { 7924, 7924, unicode_gc_Lu }, + { 7925, 7925, unicode_gc_Ll }, + { 7926, 7926, unicode_gc_Lu }, + { 7927, 7927, unicode_gc_Ll }, + { 7928, 7928, unicode_gc_Lu }, + { 7929, 7943, unicode_gc_Ll }, + { 7944, 7951, unicode_gc_Lu }, + { 7952, 7959, unicode_gc_Ll }, + { 7960, 7967, unicode_gc_Lu }, + { 7968, 7975, unicode_gc_Ll }, + { 7976, 7983, unicode_gc_Lu }, + { 7984, 7991, unicode_gc_Ll }, + { 7992, 7999, unicode_gc_Lu }, + { 8000, 8007, unicode_gc_Ll }, + { 8008, 8015, unicode_gc_Lu }, + { 8016, 8024, unicode_gc_Ll }, + { 8025, 8031, unicode_gc_Lu }, + { 8032, 8039, unicode_gc_Ll }, + { 8040, 8047, unicode_gc_Lu }, + { 8048, 8071, unicode_gc_Ll }, + { 8072, 8079, unicode_gc_Lt }, + { 8080, 8087, unicode_gc_Ll }, + { 8088, 8095, unicode_gc_Lt }, + { 8096, 8103, unicode_gc_Ll }, + { 8104, 8111, unicode_gc_Lt }, + { 8112, 8119, unicode_gc_Ll }, + { 8120, 8123, unicode_gc_Lu }, + { 8124, 8124, unicode_gc_Lt }, + { 8125, 8125, unicode_gc_Sk }, + { 8126, 8126, unicode_gc_Ll }, + { 8127, 8129, unicode_gc_Sk }, + { 8130, 8135, unicode_gc_Ll }, + { 8136, 8139, unicode_gc_Lu }, + { 8140, 8140, unicode_gc_Lt }, + { 8141, 8143, unicode_gc_Sk }, + { 8144, 8151, unicode_gc_Ll }, + { 8152, 8156, unicode_gc_Lu }, + { 8157, 8159, unicode_gc_Sk }, + { 8160, 8167, unicode_gc_Ll }, + { 8168, 8172, unicode_gc_Lu }, + { 8173, 8177, unicode_gc_Sk }, + { 8178, 8183, unicode_gc_Ll }, + { 8184, 8187, unicode_gc_Lu }, + { 8188, 8188, unicode_gc_Lt }, + { 8189, 8191, unicode_gc_Sk }, + { 8192, 8203, unicode_gc_Zs }, + { 8204, 8207, unicode_gc_Cf }, + { 8208, 8213, unicode_gc_Pd }, + { 8214, 8215, unicode_gc_Po }, + { 8216, 8216, unicode_gc_Pi }, + { 8217, 8217, unicode_gc_Pf }, + { 8218, 8218, unicode_gc_Ps }, + { 8219, 8220, unicode_gc_Pi }, + { 8221, 8221, unicode_gc_Pf }, + { 8222, 8222, unicode_gc_Ps }, + { 8223, 8223, unicode_gc_Pi }, + { 8224, 8231, unicode_gc_Po }, + { 8232, 8232, unicode_gc_Zl }, + { 8233, 8233, unicode_gc_Zp }, + { 8234, 8238, unicode_gc_Cf }, + { 8239, 8239, unicode_gc_Zs }, + { 8240, 8248, unicode_gc_Po }, + { 8249, 8249, unicode_gc_Pi }, + { 8250, 8250, unicode_gc_Pf }, + { 8251, 8254, unicode_gc_Po }, + { 8255, 8256, unicode_gc_Pc }, + { 8257, 8259, unicode_gc_Po }, + { 8260, 8260, unicode_gc_Sm }, + { 8261, 8261, unicode_gc_Ps }, + { 8262, 8262, unicode_gc_Pe }, + { 8263, 8273, unicode_gc_Po }, + { 8274, 8274, unicode_gc_Sm }, + { 8275, 8275, unicode_gc_Po }, + { 8276, 8278, unicode_gc_Pc }, + { 8279, 8286, unicode_gc_Po }, + { 8287, 8287, unicode_gc_Zs }, + { 8288, 8303, unicode_gc_Cf }, + { 8304, 8304, unicode_gc_No }, + { 8305, 8307, unicode_gc_Ll }, + { 8308, 8313, unicode_gc_No }, + { 8314, 8316, unicode_gc_Sm }, + { 8317, 8317, unicode_gc_Ps }, + { 8318, 8318, unicode_gc_Pe }, + { 8319, 8319, unicode_gc_Ll }, + { 8320, 8329, unicode_gc_No }, + { 8330, 8332, unicode_gc_Sm }, + { 8333, 8333, unicode_gc_Ps }, + { 8334, 8351, unicode_gc_Pe }, + { 8352, 8399, unicode_gc_Sc }, + { 8400, 8412, unicode_gc_Mn }, + { 8413, 8416, unicode_gc_Me }, + { 8417, 8417, unicode_gc_Mn }, + { 8418, 8420, unicode_gc_Me }, + { 8421, 8447, unicode_gc_Mn }, + { 8448, 8449, unicode_gc_So }, + { 8450, 8450, unicode_gc_Lu }, + { 8451, 8454, unicode_gc_So }, + { 8455, 8455, unicode_gc_Lu }, + { 8456, 8457, unicode_gc_So }, + { 8458, 8458, unicode_gc_Ll }, + { 8459, 8461, unicode_gc_Lu }, + { 8462, 8463, unicode_gc_Ll }, + { 8464, 8466, unicode_gc_Lu }, + { 8467, 8467, unicode_gc_Ll }, + { 8468, 8468, unicode_gc_So }, + { 8469, 8469, unicode_gc_Lu }, + { 8470, 8472, unicode_gc_So }, + { 8473, 8477, unicode_gc_Lu }, + { 8478, 8483, unicode_gc_So }, + { 8484, 8484, unicode_gc_Lu }, + { 8485, 8485, unicode_gc_So }, + { 8486, 8486, unicode_gc_Lu }, + { 8487, 8487, unicode_gc_So }, + { 8488, 8488, unicode_gc_Lu }, + { 8489, 8489, unicode_gc_So }, + { 8490, 8493, unicode_gc_Lu }, + { 8494, 8494, unicode_gc_So }, + { 8495, 8495, unicode_gc_Ll }, + { 8496, 8497, unicode_gc_Lu }, + { 8498, 8498, unicode_gc_So }, + { 8499, 8499, unicode_gc_Lu }, + { 8500, 8500, unicode_gc_Ll }, + { 8501, 8504, unicode_gc_Lo }, + { 8505, 8505, unicode_gc_Ll }, + { 8506, 8508, unicode_gc_So }, + { 8509, 8509, unicode_gc_Ll }, + { 8510, 8511, unicode_gc_Lu }, + { 8512, 8516, unicode_gc_Sm }, + { 8517, 8517, unicode_gc_Lu }, + { 8518, 8521, unicode_gc_Ll }, + { 8522, 8522, unicode_gc_So }, + { 8523, 8530, unicode_gc_Sm }, + { 8531, 8543, unicode_gc_No }, + { 8544, 8591, unicode_gc_Nl }, + { 8592, 8596, unicode_gc_Sm }, + { 8597, 8601, unicode_gc_So }, + { 8602, 8603, unicode_gc_Sm }, + { 8604, 8607, unicode_gc_So }, + { 8608, 8608, unicode_gc_Sm }, + { 8609, 8610, unicode_gc_So }, + { 8611, 8611, unicode_gc_Sm }, + { 8612, 8613, unicode_gc_So }, + { 8614, 8614, unicode_gc_Sm }, + { 8615, 8621, unicode_gc_So }, + { 8622, 8622, unicode_gc_Sm }, + { 8623, 8653, unicode_gc_So }, + { 8654, 8655, unicode_gc_Sm }, + { 8656, 8657, unicode_gc_So }, + { 8658, 8658, unicode_gc_Sm }, + { 8659, 8659, unicode_gc_So }, + { 8660, 8660, unicode_gc_Sm }, + { 8661, 8691, unicode_gc_So }, + { 8692, 8959, unicode_gc_Sm }, + { 8960, 8967, unicode_gc_So }, + { 8968, 8971, unicode_gc_Sm }, + { 8972, 8991, unicode_gc_So }, + { 8992, 8993, unicode_gc_Sm }, + { 8994, 9000, unicode_gc_So }, + { 9001, 9001, unicode_gc_Ps }, + { 9002, 9002, unicode_gc_Pe }, + { 9003, 9083, unicode_gc_So }, + { 9084, 9084, unicode_gc_Sm }, + { 9085, 9114, unicode_gc_So }, + { 9115, 9139, unicode_gc_Sm }, + { 9140, 9140, unicode_gc_Ps }, + { 9141, 9141, unicode_gc_Pe }, + { 9142, 9142, unicode_gc_Po }, + { 9143, 9311, unicode_gc_So }, + { 9312, 9371, unicode_gc_No }, + { 9372, 9449, unicode_gc_So }, + { 9450, 9471, unicode_gc_No }, + { 9472, 9654, unicode_gc_So }, + { 9655, 9655, unicode_gc_Sm }, + { 9656, 9664, unicode_gc_So }, + { 9665, 9665, unicode_gc_Sm }, + { 9666, 9719, unicode_gc_So }, + { 9720, 9727, unicode_gc_Sm }, + { 9728, 9838, unicode_gc_So }, + { 9839, 9839, unicode_gc_Sm }, + { 9840, 10087, unicode_gc_So }, + { 10088, 10088, unicode_gc_Ps }, + { 10089, 10089, unicode_gc_Pe }, + { 10090, 10090, unicode_gc_Ps }, + { 10091, 10091, unicode_gc_Pe }, + { 10092, 10092, unicode_gc_Ps }, + { 10093, 10093, unicode_gc_Pe }, + { 10094, 10094, unicode_gc_Ps }, + { 10095, 10095, unicode_gc_Pe }, + { 10096, 10096, unicode_gc_Ps }, + { 10097, 10097, unicode_gc_Pe }, + { 10098, 10098, unicode_gc_Ps }, + { 10099, 10099, unicode_gc_Pe }, + { 10100, 10100, unicode_gc_Ps }, + { 10101, 10101, unicode_gc_Pe }, + { 10102, 10131, unicode_gc_No }, + { 10132, 10191, unicode_gc_So }, + { 10192, 10213, unicode_gc_Sm }, + { 10214, 10214, unicode_gc_Ps }, + { 10215, 10215, unicode_gc_Pe }, + { 10216, 10216, unicode_gc_Ps }, + { 10217, 10217, unicode_gc_Pe }, + { 10218, 10218, unicode_gc_Ps }, + { 10219, 10223, unicode_gc_Pe }, + { 10224, 10239, unicode_gc_Sm }, + { 10240, 10495, unicode_gc_So }, + { 10496, 10626, unicode_gc_Sm }, + { 10627, 10627, unicode_gc_Ps }, + { 10628, 10628, unicode_gc_Pe }, + { 10629, 10629, unicode_gc_Ps }, + { 10630, 10630, unicode_gc_Pe }, + { 10631, 10631, unicode_gc_Ps }, + { 10632, 10632, unicode_gc_Pe }, + { 10633, 10633, unicode_gc_Ps }, + { 10634, 10634, unicode_gc_Pe }, + { 10635, 10635, unicode_gc_Ps }, + { 10636, 10636, unicode_gc_Pe }, + { 10637, 10637, unicode_gc_Ps }, + { 10638, 10638, unicode_gc_Pe }, + { 10639, 10639, unicode_gc_Ps }, + { 10640, 10640, unicode_gc_Pe }, + { 10641, 10641, unicode_gc_Ps }, + { 10642, 10642, unicode_gc_Pe }, + { 10643, 10643, unicode_gc_Ps }, + { 10644, 10644, unicode_gc_Pe }, + { 10645, 10645, unicode_gc_Ps }, + { 10646, 10646, unicode_gc_Pe }, + { 10647, 10647, unicode_gc_Ps }, + { 10648, 10648, unicode_gc_Pe }, + { 10649, 10711, unicode_gc_Sm }, + { 10712, 10712, unicode_gc_Ps }, + { 10713, 10713, unicode_gc_Pe }, + { 10714, 10714, unicode_gc_Ps }, + { 10715, 10715, unicode_gc_Pe }, + { 10716, 10747, unicode_gc_Sm }, + { 10748, 10748, unicode_gc_Ps }, + { 10749, 10749, unicode_gc_Pe }, + { 10750, 11007, unicode_gc_Sm }, + { 11008, 12287, unicode_gc_So }, + { 12288, 12288, unicode_gc_Zs }, + { 12289, 12291, unicode_gc_Po }, + { 12292, 12292, unicode_gc_So }, + { 12293, 12293, unicode_gc_Lm }, + { 12294, 12294, unicode_gc_Lo }, + { 12295, 12295, unicode_gc_Nl }, + { 12296, 12296, unicode_gc_Ps }, + { 12297, 12297, unicode_gc_Pe }, + { 12298, 12298, unicode_gc_Ps }, + { 12299, 12299, unicode_gc_Pe }, + { 12300, 12300, unicode_gc_Ps }, + { 12301, 12301, unicode_gc_Pe }, + { 12302, 12302, unicode_gc_Ps }, + { 12303, 12303, unicode_gc_Pe }, + { 12304, 12304, unicode_gc_Ps }, + { 12305, 12305, unicode_gc_Pe }, + { 12306, 12307, unicode_gc_So }, + { 12308, 12308, unicode_gc_Ps }, + { 12309, 12309, unicode_gc_Pe }, + { 12310, 12310, unicode_gc_Ps }, + { 12311, 12311, unicode_gc_Pe }, + { 12312, 12312, unicode_gc_Ps }, + { 12313, 12313, unicode_gc_Pe }, + { 12314, 12314, unicode_gc_Ps }, + { 12315, 12315, unicode_gc_Pe }, + { 12316, 12316, unicode_gc_Pd }, + { 12317, 12317, unicode_gc_Ps }, + { 12318, 12319, unicode_gc_Pe }, + { 12320, 12320, unicode_gc_So }, + { 12321, 12329, unicode_gc_Nl }, + { 12330, 12335, unicode_gc_Mn }, + { 12336, 12336, unicode_gc_Pd }, + { 12337, 12341, unicode_gc_Lm }, + { 12342, 12343, unicode_gc_So }, + { 12344, 12346, unicode_gc_Nl }, + { 12347, 12347, unicode_gc_Lm }, + { 12348, 12348, unicode_gc_Lo }, + { 12349, 12349, unicode_gc_Po }, + { 12350, 12352, unicode_gc_So }, + { 12353, 12440, unicode_gc_Lo }, + { 12441, 12442, unicode_gc_Mn }, + { 12443, 12444, unicode_gc_Sk }, + { 12445, 12446, unicode_gc_Lm }, + { 12447, 12447, unicode_gc_Lo }, + { 12448, 12448, unicode_gc_Pd }, + { 12449, 12538, unicode_gc_Lo }, + { 12539, 12539, unicode_gc_Pc }, + { 12540, 12542, unicode_gc_Lm }, + { 12543, 12687, unicode_gc_Lo }, + { 12688, 12689, unicode_gc_So }, + { 12690, 12693, unicode_gc_No }, + { 12694, 12703, unicode_gc_So }, + { 12704, 12799, unicode_gc_Lo }, + { 12800, 12831, unicode_gc_So }, + { 12832, 12841, unicode_gc_No }, + { 12842, 12880, unicode_gc_So }, + { 12881, 12895, unicode_gc_No }, + { 12896, 12927, unicode_gc_So }, + { 12928, 12937, unicode_gc_No }, + { 12938, 12976, unicode_gc_So }, + { 12977, 12991, unicode_gc_No }, + { 12992, 13311, unicode_gc_So }, + { 13312, 19903, unicode_gc_Lo }, + { 19904, 19967, unicode_gc_So }, + { 19968, 42127, unicode_gc_Lo }, + { 42128, 44031, unicode_gc_So }, + { 44032, 55295, unicode_gc_Lo }, + { 55296, 57343, unicode_gc_Cs }, + { 57344, 63743, unicode_gc_Co }, + { 63744, 64255, unicode_gc_Lo }, + { 64256, 64284, unicode_gc_Ll }, + { 64285, 64285, unicode_gc_Lo }, + { 64286, 64286, unicode_gc_Mn }, + { 64287, 64296, unicode_gc_Lo }, + { 64297, 64297, unicode_gc_Sm }, + { 64298, 64829, unicode_gc_Lo }, + { 64830, 64830, unicode_gc_Ps }, + { 64831, 64847, unicode_gc_Pe }, + { 64848, 65019, unicode_gc_Lo }, + { 65020, 65020, unicode_gc_Sc }, + { 65021, 65023, unicode_gc_So }, + { 65024, 65071, unicode_gc_Mn }, + { 65072, 65072, unicode_gc_Po }, + { 65073, 65074, unicode_gc_Pd }, + { 65075, 65076, unicode_gc_Pc }, + { 65077, 65077, unicode_gc_Ps }, + { 65078, 65078, unicode_gc_Pe }, + { 65079, 65079, unicode_gc_Ps }, + { 65080, 65080, unicode_gc_Pe }, + { 65081, 65081, unicode_gc_Ps }, + { 65082, 65082, unicode_gc_Pe }, + { 65083, 65083, unicode_gc_Ps }, + { 65084, 65084, unicode_gc_Pe }, + { 65085, 65085, unicode_gc_Ps }, + { 65086, 65086, unicode_gc_Pe }, + { 65087, 65087, unicode_gc_Ps }, + { 65088, 65088, unicode_gc_Pe }, + { 65089, 65089, unicode_gc_Ps }, + { 65090, 65090, unicode_gc_Pe }, + { 65091, 65091, unicode_gc_Ps }, + { 65092, 65092, unicode_gc_Pe }, + { 65093, 65094, unicode_gc_Po }, + { 65095, 65095, unicode_gc_Ps }, + { 65096, 65096, unicode_gc_Pe }, + { 65097, 65100, unicode_gc_Po }, + { 65101, 65103, unicode_gc_Pc }, + { 65104, 65111, unicode_gc_Po }, + { 65112, 65112, unicode_gc_Pd }, + { 65113, 65113, unicode_gc_Ps }, + { 65114, 65114, unicode_gc_Pe }, + { 65115, 65115, unicode_gc_Ps }, + { 65116, 65116, unicode_gc_Pe }, + { 65117, 65117, unicode_gc_Ps }, + { 65118, 65118, unicode_gc_Pe }, + { 65119, 65121, unicode_gc_Po }, + { 65122, 65122, unicode_gc_Sm }, + { 65123, 65123, unicode_gc_Pd }, + { 65124, 65127, unicode_gc_Sm }, + { 65128, 65128, unicode_gc_Po }, + { 65129, 65129, unicode_gc_Sc }, + { 65130, 65135, unicode_gc_Po }, + { 65136, 65278, unicode_gc_Lo }, + { 65279, 65280, unicode_gc_Cf }, + { 65281, 65283, unicode_gc_Po }, + { 65284, 65284, unicode_gc_Sc }, + { 65285, 65287, unicode_gc_Po }, + { 65288, 65288, unicode_gc_Ps }, + { 65289, 65289, unicode_gc_Pe }, + { 65290, 65290, unicode_gc_Po }, + { 65291, 65291, unicode_gc_Sm }, + { 65292, 65292, unicode_gc_Po }, + { 65293, 65293, unicode_gc_Pd }, + { 65294, 65295, unicode_gc_Po }, + { 65296, 65305, unicode_gc_Nd }, + { 65306, 65307, unicode_gc_Po }, + { 65308, 65310, unicode_gc_Sm }, + { 65311, 65312, unicode_gc_Po }, + { 65313, 65338, unicode_gc_Lu }, + { 65339, 65339, unicode_gc_Ps }, + { 65340, 65340, unicode_gc_Po }, + { 65341, 65341, unicode_gc_Pe }, + { 65342, 65342, unicode_gc_Sk }, + { 65343, 65343, unicode_gc_Pc }, + { 65344, 65344, unicode_gc_Sk }, + { 65345, 65370, unicode_gc_Ll }, + { 65371, 65371, unicode_gc_Ps }, + { 65372, 65372, unicode_gc_Sm }, + { 65373, 65373, unicode_gc_Pe }, + { 65374, 65374, unicode_gc_Sm }, + { 65375, 65375, unicode_gc_Ps }, + { 65376, 65376, unicode_gc_Pe }, + { 65377, 65377, unicode_gc_Po }, + { 65378, 65378, unicode_gc_Ps }, + { 65379, 65379, unicode_gc_Pe }, + { 65380, 65380, unicode_gc_Po }, + { 65381, 65381, unicode_gc_Pc }, + { 65382, 65391, unicode_gc_Lo }, + { 65392, 65392, unicode_gc_Lm }, + { 65393, 65437, unicode_gc_Lo }, + { 65438, 65439, unicode_gc_Lm }, + { 65440, 65503, unicode_gc_Lo }, + { 65504, 65505, unicode_gc_Sc }, + { 65506, 65506, unicode_gc_Sm }, + { 65507, 65507, unicode_gc_Sk }, + { 65508, 65508, unicode_gc_So }, + { 65509, 65511, unicode_gc_Sc }, + { 65512, 65512, unicode_gc_So }, + { 65513, 65516, unicode_gc_Sm }, + { 65517, 65528, unicode_gc_So }, + { 65529, 65531, unicode_gc_Cf }, + { 65532, 65535, unicode_gc_So }, + { 65536, 65791, unicode_gc_Lo }, + { 65792, 65793, unicode_gc_Po }, + { 65794, 65798, unicode_gc_So }, + { 65799, 65846, unicode_gc_No }, + { 65847, 66303, unicode_gc_So }, + { 66304, 66335, unicode_gc_Lo }, + { 66336, 66351, unicode_gc_No }, + { 66352, 66377, unicode_gc_Lo }, + { 66378, 66431, unicode_gc_Nl }, + { 66432, 66462, unicode_gc_Lo }, + { 66463, 66559, unicode_gc_Po }, + { 66560, 66599, unicode_gc_Lu }, + { 66600, 66639, unicode_gc_Ll }, + { 66640, 66719, unicode_gc_Lo }, + { 66720, 67583, unicode_gc_Nd }, + { 67584, 118783, unicode_gc_Lo }, + { 118784, 119140, unicode_gc_So }, + { 119141, 119142, unicode_gc_Mc }, + { 119143, 119145, unicode_gc_Mn }, + { 119146, 119148, unicode_gc_So }, + { 119149, 119154, unicode_gc_Mc }, + { 119155, 119162, unicode_gc_Cf }, + { 119163, 119170, unicode_gc_Mn }, + { 119171, 119172, unicode_gc_So }, + { 119173, 119179, unicode_gc_Mn }, + { 119180, 119209, unicode_gc_So }, + { 119210, 119213, unicode_gc_Mn }, + { 119214, 119807, unicode_gc_So }, + { 119808, 119833, unicode_gc_Lu }, + { 119834, 119859, unicode_gc_Ll }, + { 119860, 119885, unicode_gc_Lu }, + { 119886, 119911, unicode_gc_Ll }, + { 119912, 119937, unicode_gc_Lu }, + { 119938, 119963, unicode_gc_Ll }, + { 119964, 119989, unicode_gc_Lu }, + { 119990, 120015, unicode_gc_Ll }, + { 120016, 120041, unicode_gc_Lu }, + { 120042, 120067, unicode_gc_Ll }, + { 120068, 120093, unicode_gc_Lu }, + { 120094, 120119, unicode_gc_Ll }, + { 120120, 120145, unicode_gc_Lu }, + { 120146, 120171, unicode_gc_Ll }, + { 120172, 120197, unicode_gc_Lu }, + { 120198, 120223, unicode_gc_Ll }, + { 120224, 120249, unicode_gc_Lu }, + { 120250, 120275, unicode_gc_Ll }, + { 120276, 120301, unicode_gc_Lu }, + { 120302, 120327, unicode_gc_Ll }, + { 120328, 120353, unicode_gc_Lu }, + { 120354, 120379, unicode_gc_Ll }, + { 120380, 120405, unicode_gc_Lu }, + { 120406, 120431, unicode_gc_Ll }, + { 120432, 120457, unicode_gc_Lu }, + { 120458, 120487, unicode_gc_Ll }, + { 120488, 120512, unicode_gc_Lu }, + { 120513, 120513, unicode_gc_Sm }, + { 120514, 120538, unicode_gc_Ll }, + { 120539, 120539, unicode_gc_Sm }, + { 120540, 120545, unicode_gc_Ll }, + { 120546, 120570, unicode_gc_Lu }, + { 120571, 120571, unicode_gc_Sm }, + { 120572, 120596, unicode_gc_Ll }, + { 120597, 120597, unicode_gc_Sm }, + { 120598, 120603, unicode_gc_Ll }, + { 120604, 120628, unicode_gc_Lu }, + { 120629, 120629, unicode_gc_Sm }, + { 120630, 120654, unicode_gc_Ll }, + { 120655, 120655, unicode_gc_Sm }, + { 120656, 120661, unicode_gc_Ll }, + { 120662, 120686, unicode_gc_Lu }, + { 120687, 120687, unicode_gc_Sm }, + { 120688, 120712, unicode_gc_Ll }, + { 120713, 120713, unicode_gc_Sm }, + { 120714, 120719, unicode_gc_Ll }, + { 120720, 120744, unicode_gc_Lu }, + { 120745, 120745, unicode_gc_Sm }, + { 120746, 120770, unicode_gc_Ll }, + { 120771, 120771, unicode_gc_Sm }, + { 120772, 120781, unicode_gc_Ll }, + { 120782, 131071, unicode_gc_Nd }, + { 131072, 917504, unicode_gc_Lo }, + { 917505, 917759, unicode_gc_Cf }, + { 917760, 983039, unicode_gc_Mn }, + { 983040, 1114109, unicode_gc_Co }, +}; +/* arch-tag:aba6847fbd64858c183a471a517838a9 */ diff --git a/lib/user.c b/lib/user.c new file mode 100644 index 0000000..7cbca98 --- /dev/null +++ b/lib/user.c @@ -0,0 +1,65 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <errno.h> +#include <pwd.h> +#include <grp.h> +#include <unistd.h> + +#include "user.h" +#include "log.h" +#include "configuration.h" + +void become_mortal(void) { + struct passwd *pw; + + if(config->user) { + if(!(pw = getpwnam(config->user))) + fatal(0, "cannot find user %s", config->user); + if(pw->pw_uid != getuid()) { + if(initgroups(config->user, pw->pw_gid)) + fatal(errno, "error calling initgroups"); + if(setgid(pw->pw_gid) < 0) fatal(errno, "error calling setgid"); + if(setuid(pw->pw_uid) < 0) fatal(errno, "error calling setgid"); + info("changed to user %s (uid %lu)", config->user, (unsigned long)getuid()); + } + /* sanity checks */ + if(getuid() != pw->pw_uid) fatal(0, "wrong real uid"); + if(geteuid() != pw->pw_uid) fatal(0, "wrong effective uid"); + if(getgid() != pw->pw_gid) fatal(0, "wrong real gid"); + if(getegid() != pw->pw_gid) fatal(0, "wrong effective gid"); + if(setuid(0) != -1) fatal(0, "setuid(0) unexpectedly succeeded"); + if(seteuid(0) != -1) fatal(0, "seteuid(0) unexpectedly succeeded"); + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:GTEl+AUapne19BB73yRZdw */ diff --git a/lib/user.h b/lib/user.h new file mode 100644 index 0000000..b895c5a --- /dev/null +++ b/lib/user.h @@ -0,0 +1,36 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef USER_H +#define USER_H + +void become_mortal(void); + +#endif /* USER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:pl++oRgq6iybMI28uVPCsA */ diff --git a/lib/utf8.c b/lib/utf8.c new file mode 100644 index 0000000..d4dc472 --- /dev/null +++ b/lib/utf8.c @@ -0,0 +1,41 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "utf8.h" + +int validutf8(const char *s) { + unsigned long c; + + while(*s) + PARSE_UTF8(s, c, return 0); + return 1; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:WGilqGFnXhhAeU5ZnbG8oQ */ diff --git a/lib/utf8.h b/lib/utf8.h new file mode 100644 index 0000000..a051019 --- /dev/null +++ b/lib/utf8.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef UTF8_H +#define UTF8_H + +#define PARSE_UTF8(S,C,E) do { \ + if((unsigned char)*S < 0x80) \ + C = *S++; \ + else if((unsigned char)*S <= 0xDF) { \ + C = (*S++ & 0x1F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x80) { E; } \ + } else if((unsigned char)*S <= 0xEF) { \ + C = (*S++ & 0x0F) << 12; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x800 \ + || (C >= 0xD800 && C <= 0xDFFF)) { \ + E; \ + } \ + } else if((unsigned char)*S <= 0xF7) { \ + C = (*S++ & 0x07) << 18; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 12; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x10000 || C > 0x10FFFF) { E; } \ + } else { \ + E; \ + } \ +} while(0) + +int validutf8(const char *s); +/* return nonzero if S is a valid UTF-8 sequence, else false */ + +#endif /* UTF8_h */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:456aedaec99ad19d321ac15b7765da4d */ diff --git a/lib/vacopy.h b/lib/vacopy.h new file mode 100644 index 0000000..844ec31 --- /dev/null +++ b/lib/vacopy.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef VACOPY_H +#define VACOPY_H + +#ifndef va_copy +# ifdef __va_copy +# define va_copy __va_copy +# else +# define va_copy(d, s) ((void)memcpy(&d, &s, sizeof s)) +# endif +#endif + +#endif /* VACOPY_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a57c12f1df1735ec855a1450117e7ccf */ diff --git a/lib/vector.c b/lib/vector.c new file mode 100644 index 0000000..f416104 --- /dev/null +++ b/lib/vector.c @@ -0,0 +1,49 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <string.h> + +#include "mem.h" +#include "log.h" +#include "vector.h" + +void vector_append_many(struct vector *v, char **vec, int nvec) { + while(nvec-- > 0) + vector_append(v, *vec++); +} + +void dynstr_append_bytes(struct dynstr *v, const char *ptr, size_t n) { + while(n > 0) { + dynstr_append(v, *ptr++); + n--; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4f207f71a87ed2f80b527e1fecddeaf4 */ diff --git a/lib/vector.h b/lib/vector.h new file mode 100644 index 0000000..8fbcc96 --- /dev/null +++ b/lib/vector.h @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef VECTOR_H +#define VECTOR_H + +#define VECTOR_TYPE(NAME,ETYPE,REALLOC) \ + \ +struct NAME { \ + ETYPE *vec; \ + int nvec, nslots; \ +}; \ + \ +static inline void NAME##_init(struct NAME *v) { \ + memset(v, 0, sizeof *v); \ +} \ + \ +static inline void NAME##_append(struct NAME *v, ETYPE val) { \ + if(v->nvec >= v->nslots) { \ + v->nslots = v->nslots ? 2 * v->nslots : 16; \ + v->vec = REALLOC(v->vec, v->nslots * sizeof(ETYPE)); \ + } \ + v->vec[v->nvec++] = val; \ +} \ + \ +static inline void NAME##_terminate(struct NAME *v) { \ + if(v->nvec >= v->nslots) \ + v->vec = REALLOC(v->vec, ++v->nslots * sizeof(ETYPE)); \ + memset(&v->vec[v->nvec], 0, sizeof (ETYPE)); \ +} \ + +VECTOR_TYPE(vector, char *, xrealloc) +VECTOR_TYPE(dynstr, char, xrealloc_noptr) +VECTOR_TYPE(dynstr_ucs4, uint32_t, xrealloc_noptr) + +void vector_append_many(struct vector *v, char **vec, int nvec); + +void dynstr_append_bytes(struct dynstr *v, const char *ptr, size_t n); + +static inline void dynstr_append_string(struct dynstr *v, const char *ptr) { + dynstr_append_bytes(v, ptr, strlen(ptr)); +} + +#endif /* VECTOR_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a57474a59d7ebd67cda96bf05bd89049 */ diff --git a/lib/words.c b/lib/words.c new file mode 100644 index 0000000..2e4001d --- /dev/null +++ b/lib/words.c @@ -0,0 +1,177 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stddef.h> + +#include "mem.h" +#include "vector.h" +#include "table.h" +#include "words.h" +#include "utf8.h" + +#include "casefold.h" +#include "unicodegc.h" + +const char *casefold(const char *ptr) { + struct dynstr d; + int l, r, m; + uint32_t c; + const struct cm *t; + const char *start, *s = ptr; + + dynstr_init(&d); + while(*s) { + start = s; + PARSE_UTF8(s, c, return ptr); + /* seek the folded equivalent */ + t = cm[c & CM_MASK]; + l = 0; + r = cmn[c & CM_MASK] - 1; + while(l <= r && c != t[m = (l + r) / 2].ch) + if(c < t[m].ch) + r = m - 1; + else + l = m + 1; + if(l <= r) + dynstr_append_string(&d, t[m].tr); + else + dynstr_append_bytes(&d, start, s - start); + } + dynstr_terminate(&d); + return d.vec; +} + +static enum unicode_gc_cat cat(uint32_t c) { + int l, r, m; + + l = 0; + r = sizeof gcs / sizeof *gcs; + while(l <= r) { + m = (l + r) / 2; + if(c < gcs[m].l) + r = m - 1; + else if(c > gcs[m].h) + l = m + 1; + else + return gcs[m].cat; + } + return unicode_gc_none; +} + +/* XXX this is a bit kludgy */ + +char **words(const char *s, int *nvecp) { + struct vector v; + struct dynstr d; + const char *start; + uint32_t c; + int in_word = 0; + + vector_init(&v); + while(*s) { + start = s; + PARSE_UTF8(s, c, return 0); + /* special cases first */ + switch(c) { + case '/': + case '.': + case '+': + case '&': + case ':': + case '_': + case '-': + goto separator; + } + /* do the rest on category */ + switch(cat(c)) { + case unicode_gc_Ll: + case unicode_gc_Lm: + case unicode_gc_Lo: + case unicode_gc_Lt: + case unicode_gc_Lu: + case unicode_gc_Nd: + case unicode_gc_Nl: + case unicode_gc_No: + case unicode_gc_Sc: + case unicode_gc_Sk: + case unicode_gc_Sm: + case unicode_gc_So: + /* letters, digits and symbols are considered to be part of + * words */ + if(!in_word) { + dynstr_init(&d); + in_word = 1; + } + dynstr_append_bytes(&d, start, s - start); + break; + + case unicode_gc_Cc: + case unicode_gc_Cf: + case unicode_gc_Co: + case unicode_gc_Cs: + case unicode_gc_Zl: + case unicode_gc_Zp: + case unicode_gc_Zs: + case unicode_gc_Pe: + case unicode_gc_Ps: + separator: + if(in_word) { + dynstr_terminate(&d); + vector_append(&v, d.vec); + in_word = 0; + } + break; + + case unicode_gc_Mc: + case unicode_gc_Me: + case unicode_gc_Mn: + case unicode_gc_Pc: + case unicode_gc_Pd: + case unicode_gc_Pf: + case unicode_gc_Pi: + case unicode_gc_Po: + case unicode_gc_none: + /* control and punctuation is completely ignored */ + break; + + } + } + if(in_word) { + /* pick up the final word */ + dynstr_terminate(&d); + vector_append(&v, d.vec); + } + vector_terminate(&v); + if(nvecp) + *nvecp = v.nvec; + return v.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0ea1f1700f14cd031b7f1fbbcca765fa */ diff --git a/lib/words.h b/lib/words.h new file mode 100644 index 0000000..52a6839 --- /dev/null +++ b/lib/words.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef WORDS_H +#define WORDS_H + +const char *casefold(const char *s); +/* return a case-folded version of UTF-8 string @s@, or the original + * string if malformed. */ + +char **words(const char *s, int *nvecp); +/* return the words found in UTF-8 string @s@, with punctuation + * stripped out. (Doesn't casefold.) */ + +#endif /* WORDS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e575b078ed351ae974d55ed5dfadfaa2 */ diff --git a/lib/wstat.c b/lib/wstat.c new file mode 100644 index 0000000..bd387ff --- /dev/null +++ b/lib/wstat.c @@ -0,0 +1,58 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <sys/wait.h> +#include <stdio.h> +#include <string.h> +#include <signal.h> + +#include "mem.h" +#include "log.h" +#include "wstat.h" +#include "printf.h" + +const char *wstat(int w) { + int n; + char *r; + + if(WIFEXITED(w)) + n = byte_xasprintf(&r, "exited with status %d", WEXITSTATUS(w)); + else if(WIFSIGNALED(w)) + n = byte_xasprintf(&r, "terminated by signal %d (%s)%s", + WTERMSIG(w), strsignal(WTERMSIG(w)), + WCOREDUMP(w) ? " - core dumped" : ""); + else if(WIFSTOPPED(w)) + n = byte_xasprintf(&r, "stopped by signal %d (%s)", + WSTOPSIG(w), strsignal(WSTOPSIG(w))); + else + n = byte_xasprintf(&r, "terminated with unknown wait status %#x", + (unsigned)w); + return n >= 0 ? r : "[could not convert wait status]"; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:266c61bd39bdfb327908ad3dd95412ae */ diff --git a/lib/wstat.h b/lib/wstat.h new file mode 100644 index 0000000..bf80e42 --- /dev/null +++ b/lib/wstat.h @@ -0,0 +1,36 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef WSTAT_H +#define WSTAT_H + +const char *wstat(int w); +/* Format wait status @w@. In extremis the return value might be a + * pointer to a string literal. The result should always be ASCII. */ + +#endif /* WSTAT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7bf1b99d0880deb7c52839aef20fa88d */ diff --git a/plugins/Makefile.am b/plugins/Makefile.am new file mode 100644 index 0000000..3c87817 --- /dev/null +++ b/plugins/Makefile.am @@ -0,0 +1,43 @@ +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +pkglib_LTLIBRARIES=tracklength.la fs.la notify.la exec.la shell.la \ + execraw.la +AM_CPPFLAGS=-I${top_srcdir}/lib + +notify_la_SOURCES=notify.c +notify_la_LDFLAGS=-module + +tracklength_la_SOURCES=tracklength.c mad.c madshim.h +tracklength_la_LDFLAGS=-module +tracklength_la_LIBADD=@LIBVORBISFILE@ @LIBMAD@ + +fs_la_SOURCES=fs.c +fs_la_LDFLAGS=-module + +exec_la_SOURCES=exec.c +exec_la_LDFLAGS=-module + +execraw_la_SOURCES=execraw.c +execraw_la_LDFLAGS=-module + +shell_la_SOURCES=shell.c +shell_la_LDFLAGS=-module +# arch-tag:45b01b1ebc885c96d6faad2d824a7eae diff --git a/plugins/exec.c b/plugins/exec.c new file mode 100644 index 0000000..283ebd3 --- /dev/null +++ b/plugins/exec.c @@ -0,0 +1,58 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> + +#include <disorder.h> + +#ifndef TYPE +# define TYPE DISORDER_PLAYER_STANDALONE +#endif +const unsigned long disorder_player_type = TYPE; + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char attribute((unused)) *track, + void attribute((unused)) *data) { + int i, j; + const char **vec; + + vec = disorder_malloc((nparameters + 2) * sizeof (char *)); + i = 0; + j = 0; + for(i = 0; i < nparameters; ++i) + vec[j++] = parameters[i]; + vec[j++] = path; + vec[j] = 0; + execvp(vec[0], (char **)vec); + disorder_fatal(errno, "error executing %s", vec[0]); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:39dab35f9dd51a2713a16234ee7b5030 */ diff --git a/plugins/execraw.c b/plugins/execraw.c new file mode 100644 index 0000000..3c3595a --- /dev/null +++ b/plugins/execraw.c @@ -0,0 +1,12 @@ +#define TYPE DISORDER_PLAYER_RAW +#include "exec.c" + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:gNkzdgw34U7sQf/CWqcWrQ */ diff --git a/plugins/fs.c b/plugins/fs.c new file mode 100644 index 0000000..3212a04 --- /dev/null +++ b/plugins/fs.c @@ -0,0 +1,85 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <syslog.h> +#include <errno.h> +#include <dirent.h> +#include <stdio.h> +#include <unistd.h> + +#include <disorder.h> + +void disorder_scan(const char *path) { + struct stat sb; + DIR *dp; + struct dirent *de; + char *np; + + if(stat(path, &sb) < 0) { + disorder_error(errno, "cannot lstat %s", path); + return; + } + /* skip files that aren't world-readable */ + if(!(sb.st_mode & 0004)) + return; + if(S_ISDIR(sb.st_mode)) { + if(!(dp = opendir(path))) { + disorder_error(errno, "cannot open directory %s", path); + return; + } + while((errno = 0), + (de = readdir(dp))) { + if(de->d_name[0] != '.') { + disorder_asprintf(&np, "%s/%s", path, de->d_name); + disorder_scan(np); + } + } + if(errno) + disorder_error(errno, "error reading directory %s", path); + closedir(dp); + } else if(S_ISREG(sb.st_mode)) + if(printf("%s%c", path, 0) < 0) + disorder_fatal(errno, "error writing to scanner output pipe"); +} + +int disorder_check(const char attribute((unused)) *root, const char *path) { + if(access(path, R_OK) == 0) + return 1; + else if(errno == ENOENT) + return 0; + else { + disorder_error(errno, "cannot access %s", path); + return -1; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:070e62d92ebc26329f0a3961d615476f */ diff --git a/plugins/mad.c b/plugins/mad.c new file mode 100644 index 0000000..32cbded --- /dev/null +++ b/plugins/mad.c @@ -0,0 +1,232 @@ +/* This file is a subset of the debian source tarball of mpg321-0.2.10.3/mad.c + - see http://mpg321.sourceforge.net/ */ + +/* + mpg321 - a fully free clone of mpg123. + Copyright (C) 2001 Joe Drew + + Originally based heavily upon: + plaympeg - Sample MPEG player using the SMPEG library + Copyright (C) 1999 Loki Entertainment Software + + Also uses some code from + mad - MPEG audio decoder + Copyright (C) 2000-2001 Robert Leslie + + Original playlist code contributed by Tobias Bengtsson <tobbe@tobbe.nu> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +#include <sys/types.h> +#include <string.h> + +#include <mad.h> + +#include "madshim.h" + +/* XING parsing is from the MAD winamp input plugin */ + +struct xing { + int flags; + unsigned long frames; + unsigned long bytes; + unsigned char toc[100]; + long scale; +}; + +enum { + XING_FRAMES = 0x0001, + XING_BYTES = 0x0002, + XING_TOC = 0x0004, + XING_SCALE = 0x0008 +}; + +# define XING_MAGIC (('X' << 24) | ('i' << 16) | ('n' << 8) | 'g') + +static +int parse_xing(struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen) +{ + if (bitlen < 64 || mad_bit_read(&ptr, 32) != XING_MAGIC) + goto fail; + + xing->flags = mad_bit_read(&ptr, 32); + bitlen -= 64; + + if (xing->flags & XING_FRAMES) { + if (bitlen < 32) + goto fail; + + xing->frames = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_BYTES) { + if (bitlen < 32) + goto fail; + + xing->bytes = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_TOC) { + int i; + + if (bitlen < 800) + goto fail; + + for (i = 0; i < 100; ++i) + xing->toc[i] = mad_bit_read(&ptr, 8); + + bitlen -= 800; + } + + if (xing->flags & XING_SCALE) { + if (bitlen < 32) + goto fail; + + xing->scale = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + return 1; + + fail: + xing->flags = 0; + return 0; +} + +/* Following two functions are adapted from mad_timer, from the + libmad distribution */ +void scan_mp3(void const *ptr, ssize_t len, buffer *buf) +{ + struct mad_stream stream; + struct mad_header header; + struct xing xing; + + unsigned long bitrate = 0; + int has_xing = 0; + int is_vbr = 0; + + memset(&xing, 0, sizeof xing); + + mad_stream_init(&stream); + mad_header_init(&header); + + mad_stream_buffer(&stream, ptr, len); + + buf->num_frames = 0; + + /* There are three ways of calculating the length of an mp3: + 1) Constant bitrate: One frame can provide the information + needed: # of frames and duration. Just see how long it + is and do the division. + 2) Variable bitrate: Xing tag. It provides the number of + frames. Each frame has the same number of samples, so + just use that. + 3) All: Count up the frames and duration of each frames + by decoding each one. We do this if we've no other + choice, i.e. if it's a VBR file with no Xing tag. + */ + + while (1) + { + if (mad_header_decode(&header, &stream) == -1) + { + if (MAD_RECOVERABLE(stream.error)) + continue; + else + break; + } + + /* Limit xing testing to the first frame header */ + if (!buf->num_frames++) + { + if(parse_xing(&xing, stream.anc_ptr, stream.anc_bitlen)) + { + is_vbr = 1; + + if (xing.flags & XING_FRAMES) + { + /* We use the Xing tag only for frames. If it doesn't have that + information, it's useless to us and we have to treat it as a + normal VBR file */ + has_xing = 1; + buf->num_frames = xing.frames; + break; + } + } + } + + /* Test the first n frames to see if this is a VBR file */ + if (!is_vbr && !(buf->num_frames > 20)) + { + if (bitrate && header.bitrate != bitrate) + { + is_vbr = 1; + } + + else + { + bitrate = header.bitrate; + } + } + + /* We have to assume it's not a VBR file if it hasn't already been + marked as one and we've checked n frames for different bitrates */ + else if (!is_vbr) + { + break; + } + + mad_timer_add(&buf->duration, header.duration); + } + + if (!is_vbr) + { + double time = (len * 8.0) / (header.bitrate); /* time in seconds */ + double timefrac = (double)time - ((long)(time)); + long nsamples = 32 * MAD_NSBSAMPLES(&header); /* samples per frame */ + + /* samplerate is a constant */ + buf->num_frames = (long) (time * header.samplerate / nsamples); + + mad_timer_set(&buf->duration, (long)time, (long)(timefrac*100), 100); + } + + else if (has_xing) + { + /* modify header.duration since we don't need it anymore */ + mad_timer_multiply(&header.duration, buf->num_frames); + buf->duration = header.duration; + } + + else + { + /* the durations have been added up, and the number of frames + counted. We do nothing here. */ + } + + mad_header_finish(&header); + mad_stream_finish(&stream); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2b2d6ec382b3b37f3ba8a0bb1d345fbd */ diff --git a/plugins/madshim.h b/plugins/madshim.h new file mode 100644 index 0000000..20ec3dc --- /dev/null +++ b/plugins/madshim.h @@ -0,0 +1,41 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MADSHIM_H +#define MADSHIM_H + +/* shim to integrate code from mpg123 */ + +typedef struct { + int num_frames; + mad_timer_t duration; +} buffer; + +void scan_mp3(void const *ptr, ssize_t len, buffer *buf); + +#endif /* MADSHIM_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7b06901432784a575a0a9fe7df7010ab */ diff --git a/plugins/notify.c b/plugins/notify.c new file mode 100644 index 0000000..eede49f --- /dev/null +++ b/plugins/notify.c @@ -0,0 +1,93 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> + +#include <disorder.h> + +static void record(const char *track, const char *what) { + const char *count; + int ncount; + char buf[64]; + + if((count = disorder_track_get_data(track, what))) + ncount = atoi(count); + else + ncount = 0; + disorder_snprintf(buf, sizeof buf, "%d", ncount + 1); + disorder_track_set_data(track, what, buf); +} + +void disorder_notify_play(const char *track, + const char *submitter) { + char buf[64]; + + if(submitter) + record(track, "requested"); + record(track, "played"); + disorder_snprintf(buf, sizeof buf, "%"PRIdMAX, (intmax_t)time(0)); + disorder_track_set_data(track, "played_time", buf); +} + +void disorder_notify_queue(const char attribute((unused)) *track, + const char attribute((unused)) *submitter) { +} + +void disorder_notify_scratch(const char *track, + const char attribute((unused)) *submitter, + const char attribute((unused)) *scratcher, + int attribute((unused)) seconds) { + record(track, "scratched"); +} + +void disorder_notify_not_scratched(const char *track, + const char attribute((unused)) *submitter) { + record(track, "unscratched"); +} + +void disorder_notify_queue_remove(const char attribute((unused)) *track, + const char attribute((unused)) *remover) { +} + +void disorder_notify_queue_move(const char attribute((unused)) *track, + const char attribute((unused)) *mover) { +} + +void disorder_notify_pause(const char attribute((unused)) *track, + const char attribute((unused)) *who) { +} + +void disorder_notify_resume(const char attribute((unused)) *track, + const char attribute((unused)) *who) { +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:09c6471644c1b075a86e1d79093b5542 */ diff --git a/plugins/shell.c b/plugins/shell.c new file mode 100644 index 0000000..0fd3333 --- /dev/null +++ b/plugins/shell.c @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> +#include <stdlib.h> + +#include <disorder.h> + +const unsigned long disorder_player_type = DISORDER_PLAYER_STANDALONE; + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void attribute((unused)) *data) { + const char *vec[4]; + char *env_track, *env_path; + + vec[1] = "-c"; + vec[3] = 0; + switch(nparameters) { + case 0: + disorder_fatal(0, "missing argument to shell player module"); + case 1: + vec[0] = "sh"; + vec[2] = parameters[0]; + break; + case 2: + vec[0] = parameters[0]; + vec[2] = parameters[1]; + break; + default: + disorder_fatal(0, "extra arguments to shell player module"); + } + disorder_asprintf(&env_path, "TRACK=%s", path); + if(putenv(env_path) < 0) disorder_fatal(errno, "error calling putenv"); + disorder_asprintf(&env_track, "TRACK_UTF8=%s", track); + if(putenv(env_track) < 0) disorder_fatal(errno, "error calling putenv"); + execvp(vec[0], (char **)vec); + disorder_fatal(errno, "error executing %s", vec[0]); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:5e4b82a463c04d36c6cb9142db00ed07 */ diff --git a/plugins/tracklength.c b/plugins/tracklength.c new file mode 100644 index 0000000..8d21ad8 --- /dev/null +++ b/plugins/tracklength.c @@ -0,0 +1,265 @@ +/* + * This file is part of DisOrder. + * Portions copyright (C) 2004, 2005 Richard Kettlewell (see also below) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdio.h> +#include <math.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <errno.h> + +#include <vorbis/vorbisfile.h> +#include <mad.h> + +#include <disorder.h> + +#include "madshim.h" + +static void *mmap_file(const char *path, size_t *lengthp) { + int fd; + void *base; + struct stat sb; + + if((fd = open(path, O_RDONLY)) < 0) { + disorder_error(errno, "error opening %s", path); + return 0; + } + if(fstat(fd, &sb) < 0) { + disorder_error(errno, "error calling stat on %s", path); + goto error; + } + if(sb.st_size == 0) /* can't map 0-length files */ + goto error; + if((base = mmap(0, sb.st_size, PROT_READ, + MAP_SHARED, fd, 0)) == (void *)-1) { + disorder_error(errno, "error calling mmap on %s", path); + goto error; + } + *lengthp = sb.st_size; + close(fd); + return base; +error: + close(fd); + return 0; +} + +static long tl_mp3(const char *path) { + size_t length; + void *base; + buffer b; + + if(!(base = mmap_file(path, &length))) return -1; + b.duration = mad_timer_zero; + scan_mp3(base, length, &b); + munmap(base, length); + return b.duration.seconds + !!b.duration.fraction; +} + +static long tl_ogg(const char *path) { + OggVorbis_File vf; + FILE *fp = 0; + double length; + + if(!path) goto error; + if(!(fp = fopen(path, "rb"))) goto error; + if(ov_open(fp, &vf, 0, 0)) goto error; + fp = 0; + length = ov_time_total(&vf, -1); + ov_clear(&vf); + return ceil(length); +error: + if(fp) fclose(fp); + return -1; +} + +static long tl_wav(const char *path) { + size_t length; + void *base; + long duration = -1; + unsigned char *ptr; + unsigned n, m, data_bytes = 0, samples_per_second = 0; + unsigned n_channels = 0, bits_per_sample = 0, sample_point_size; + unsigned sample_frame_size, n_samples; + + /* Sources: + * + * http://www.technology.niagarac.on.ca/courses/comp530/WavFileFormat.html + * http://www.borg.com/~jglatt/tech/wave.htm + * http://www.borg.com/~jglatt/tech/aboutiff.htm + * + * These files consists of a header followed by chunks. + * Multibyte values are little-endian. + * + * 12 byte file header: + * offset size meaning + * 00 4 'RIFF' + * 04 4 length of rest of file + * 08 4 'WAVE' + * + * The length includes 'WAVE' but excludes the 1st 8 bytes. + * + * Chunk header: + * 00 4 chunk ID + * 04 4 length of rest of chunk + * + * The stated length may be odd, if so then there is an implicit padding byte + * appended to the chunk to make it up to an even length (someone wasn't + * think about 32/64-bit worlds). + * + * Also some files seem to have extra stuff at the end of chunks that nobody + * I know of documents. Go figure, but check the length field rather than + * deducing the length from the ID. + * + * Format chunk: + * 00 4 'fmt' + * 04 4 length of rest of chunk + * 08 2 compression (1 = none) + * 0a 2 number of channels + * 0c 4 samples/second + * 10 4 average bytes/second, = (samples/sec) * (bytes/sample) + * 14 2 bytes/sample + * 16 2 bits/sample point + * + * 'sample' means 'sample frame' above, i.e. a sample point for each channel. + * + * Data chunk: + * 00 4 'data' + * 04 4 length of rest of chunk + * 08 ... data + * + * There is only allowed to be one data chunk. Some people violate this; we + * shall encourage people to fix their broken WAV files by not supporting + * this violation and because it's easier. + * + * As to the encoding of the data: + * + * Firstly, samples up to 8 bits in size are unsigned, larger samples are + * signed. Madness. + * + * Secondly sample points are stored rounded up to a multiple of 8 bits in + * size. Marginally saner. + * + * Written as a single word (of 8, 16, 24, whatever bits) the padding to + * implement this happens at the right hand (least significant) end. + * e.g. assuming a 9 bit sample: + * + * | padded sample word | + * | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 | + * | 8 7 6 5 4 3 2 1 0 - - - - - - - | + * + * But this is a little-endian file format so the least significant byte is + * the first, which means that the padding is "between" the bits if you + * imagine them in their usual order: + * + * | first byte | second byte | + * | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | + * | 0 - - - - - - - | 8 7 6 5 4 3 2 1 | + * + * Sample points are grouped into sample frames, consisting of as many + * samples points as their are channels. It seems that there are standard + * orderings of different channels. + * + * Given all of the above all we need to do is pick up some numbers from the + * format chunk, and the length of the data chunk, and do some arithmetic. + */ + if(!(base = mmap_file(path, &length))) return -1; +#define get16(p) ((p)[0] + 256 * (p)[1]) +#define get32(p) ((p)[0] + 256 * ((p)[1] + 256 * ((p)[2] + 256 * (p)[3]))) + ptr = base; + if(length < 12) goto out; + if(strncmp((char *)ptr, "RIFF", 4)) goto out; /* wrong type */ + n = get32(ptr + 4); /* file length */ + if(n > length - 8) goto out; /* truncated */ + ptr += 8; /* skip file header */ + if(n < 4 || strncmp((char *)ptr, "WAVE", 4)) goto out; /* wrong type */ + ptr += 4; /* skip 'WAVE' */ + n -= 4; + while(n >= 8) { + m = get32(ptr + 4); /* chunk length */ + if(m > n - 8) goto out; /* truncated */ + if(!strncmp((char *)ptr, "fmt ", 4)) { + if(samples_per_second) goto out; /* duplicate format chunk! */ + n_channels = get16(ptr + 0x0a); + samples_per_second = get32(ptr + 0x0c); + bits_per_sample = get16(ptr + 0x16); + if(!samples_per_second) goto out; /* bogus! */ + } else if(!strncmp((char *)ptr, "data", 4)) { + if(data_bytes) goto out; /* multiple data chunks! */ + data_bytes = m; /* remember data size */ + } + m += 8; /* include chunk header */ + ptr += m; /* skip chunk */ + n -= m; + } + sample_point_size = (bits_per_sample + 7) / 8; + sample_frame_size = sample_point_size * n_channels; + if(!sample_frame_size) goto out; /* bogus or overflow */ + n_samples = data_bytes / sample_frame_size; + duration = (n_samples + samples_per_second - 1) / samples_per_second; +out: + munmap(base, length); + return duration; +} + +static const struct { + const char *ext; + long (*fn)(const char *path); +} file_formats[] = { + { ".MP3", tl_mp3 }, + { ".OGG", tl_ogg }, + { ".WAV", tl_wav }, + { ".mp3", tl_mp3 }, + { ".ogg", tl_ogg }, + { ".wav", tl_wav } +}; +#define N_FILE_FORMATS (int)(sizeof file_formats / sizeof *file_formats) + +long disorder_tracklength(const char attribute((unused)) *track, + const char *path) { + const char *ext = strrchr(path, '.'); + int l, r, m = 0, c = 0; /* quieten compiler */ + + if(ext) { + l = 0; + r = N_FILE_FORMATS - 1; + while(l <= r && (c = strcmp(ext, file_formats[m = (l + r) / 2].ext))) + if(c < 0) + r = m - 1; + else + l = m + 1; + if(!c) + return file_formats[m].fn(path); + } + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:fb794bf679375f55bf26fbb7a96a39de */ diff --git a/prepare b/prepare new file mode 100755 index 0000000..4387123 --- /dev/null +++ b/prepare @@ -0,0 +1,46 @@ +#! /bin/bash +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e +set -x +srcdir=$(dirname $0) +here=$(pwd) +cd $srcdir +rm -f COPYING +for f in /usr/share/common-licenses/GPL-2 $HOME/doc/GPL-2; do + if test -e "$f"; then + ln -s "$f" COPYING + break + fi +done +if test -d $HOME/share/aclocal; then + aclocal --acdir=$HOME/share/aclocal +else + aclocal +fi +libtoolize +autoconf +autoheader +automake -a || true # for INSTALL +automake --foreign -a +cd "$here" +$srcdir/configure "$@" --sysconfdir=/etc --localstatedir=/var +# arch-tag:100027fc986857bb24d779ecdcf41345 diff --git a/python/Makefile.am b/python/Makefile.am new file mode 100644 index 0000000..5f858f8 --- /dev/null +++ b/python/Makefile.am @@ -0,0 +1,33 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_SCRIPTS=tkdisorder +nodist_python_PYTHON=disorder.py + +SEDFILES=disorder.py + +include ${top_srcdir}/scripts/sedfiles.make + +EXTRA_DIST=disorder.py.in tkdisorder + +BUILT_SOURCES=disorder.py + +CLEANFILES=$(SEDFILES) $(HTMLMAN) *.pyc +# arch-tag:6b3bfd0829ab35f1a30e7d8bf9111b95 diff --git a/python/disorder.py.in b/python/disorder.py.in new file mode 100644 index 0000000..dfaddbf --- /dev/null +++ b/python/disorder.py.in @@ -0,0 +1,1045 @@ +# +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +"""Python support for DisOrder + +Provides disorder.client, a class for accessing a DisOrder server. + +Example 1: + + #! /usr/bin/env python + import disorder + d = disorder.client() + p = d.playing() + if p: + print p['track'] + +Example 2: + + #! /usr/bin/env python + import disorder + import sys + d = disorder.client() + for path in sys.argv[1:]: + d.play(path) + +""" + +import re +import string +import os +import pwd +import socket +import binascii +import sha +import sys +import locale + +_configfile = "pkgconfdir/config" +_dbhome = "pkgstatedir" + +# various regexps we'll use +_ws = re.compile(r"^[ \t\n\r]+") +_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'") +_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"") +_unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*") + +_response = re.compile("([0-9]{3}) ?(.*)") + +version = "_version_" + +######################################################################## +# exception classes + +class Error(Exception): + """Base class for DisOrder exceptions.""" + +class _splitError(Error): + # _split failed + def __init__(self, value): + self.value = value + def __str__(self): + return str(self.value) + +class parseError(Error): + """Error parsing the configuration file.""" + def __init__(self, path, line, details): + self.path = path + self.line = line + self.details = details + def __str__(self): + return "%s:%d: %s" % (self.path, self.line, self.details) + +class protocolError(Error): + """DisOrder control protocol error. + + Indicates a mismatch between the client and server's understanding of + the control protocol. + """ + def __init__(self, who, error): + self.who = who + self.error = error + def __str__(self): + return "%s: %s" % (self.who, str(self.error)) + +class operationError(Error): + """DisOrder control protocol error response. + + Indicates that an operation failed (e.g. an attempt to play a + nonexistent track). The connection should still be usable. + """ + def __init__(self, res, details): + self.res_ = int(res) + self.details_ = details + def __str__(self): + """Return the complete response string from the server. + + Excludes the final newline. + """ + return "%d %s" % (self.res_, self.details_) + def response(self): + """Return the response code from the server.""" + return self.res_ + def details(self): + """Returns the detail string from the server.""" + return self.details_ + +class communicationError(Error): + """DisOrder control protocol communication error. + + Indicates that communication with the server went wrong, perhaps + because the server was restarted. The caller could report an error to + the user and wait for further user instructions, or even automatically + retry the operation. + """ + def __init__(self, who, error): + self.who = who + self.error = error + def __str__(self): + return "%s: %s" % (self.who, str(self.error)) + +######################################################################## +# DisOrder-specific text processing + +def _unescape(s): + # Unescape the contents of a string + # + # Arguments: + # + # s -- string to unescape + # + s = re.sub("\\\\n", "\n", s) + s = re.sub("\\\\(.)", "\\1", s) + return s + +def _split(s, *comments): + # Split a string into fields according to the usual Disorder string splitting + # conventions. + # + # Arguments: + # + # s -- string to parse + # comments -- if present, parse comments + # + # Return values: + # + # On success, a list of fields is returned. + # + # On error, disorder.parseError is thrown. + # + fields = [] + while s != "": + # discard comments + if comments and s[0] == '#': + break + # strip spaces + m = _ws.match(s) + if m: + s = s[m.end():] + continue + # pick of quoted fields of both kinds + m = _squote.match(s) + if not m: + m = _dquote.match(s) + if m: + fields.append(_unescape(m.group(1))) + s = s[m.end():] + continue + # and unquoted fields + m = _unquoted.match(s) + if m: + fields.append(m.group(0)) + s = s[m.end():] + continue + # anything left must be in error + if s[0] == '"' or s[0] == '\'': + raise _splitError("invalid quoted string") + else: + raise _splitError("syntax error") + return fields + +def _escape(s): + # Escape the contents of a string + # + # Arguments: + # + # s -- string to escape + # + if re.search("[\\\\\"'\n \t\r]", s) or s == '': + s = re.sub(r'[\\"]', r'\\\g<0>', s) + s = re.sub("\n", r"\\n", s) + return '"' + s + '"' + else: + return s + +def _quote(list): + # Quote a list of values + return ' '.join(map(_escape, list)) + +def _sanitize(s): + # Return the value of s in a form suitable for writing to stderr + return s.encode(locale.nl_langinfo(locale.CODESET), 'replace') + +def _list2dict(l): + # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN] + # to a dictionary {k1:v1, k2:v2, ..., kN:vN} + d = {} + i = iter(l) + try: + while True: + k = i.next() + v = i.next() + d[k] = v + except StopIteration: + pass + return d + +def _queueEntry(s): + # parse a queue entry + return _list2dict(_split(s)) + +######################################################################## +# The client class + +class client: + """DisOrder client class. + + This class provides access to the DisOrder server either on this + machine or across the internet. + + The server to connect to, and the username and password to use, are + determined from the configuration files as described in 'man + disorder_config'. + + All methods will connect if necessary, as soon as you have a + disorder.client object you can start calling operational methods on + it. + + However if the server is restarted then the next method called on a + connection will throw an exception. This may be considered a bug. + + All methods block until they complete. + + Operation methods raise communicationError if the connection breaks, + protocolError if the response from the server is malformed, or + operationError if the response is valid but indicates that the + operation failed. + """ + + debug_proto = 0x0001 + debug_body = 0x0002 + + def __init__(self): + """Constructor for DisOrder client class. + + The constructor reads the configuration file, but does not connect + to the server. + + If the environment variable DISORDER_PYTHON_DEBUG is set then the + debug flags are initialised to that value. This can be overridden + with the debug() method below. + + The constructor Raises parseError() if the configuration file is not + valid. + """ + pw = pwd.getpwuid(os.getuid()) + self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0)) + self.config = { 'collections': [], + 'username': pw.pw_name, + 'home': _dbhome } + home = os.getenv("HOME") + if not home: + home = pw.pw_dir + privconf = _configfile + "." + pw.pw_name + passfile = home + os.sep + ".disorder" + os.sep + "passwd" + self._readfile(_configfile) + if os.path.exists(privconf): + self._readfile(privconf) + if os.path.exists(passfile): + self._readfile(passfile) + self.state = 'disconnected' + + def debug(self, bits): + """Enable or disable protocol debugging. Debug messages are written + to sys.stderr. + + Arguments: + bits -- bitmap of operations that should generate debug information + + Bitmap values: + debug_proto -- dump control protocol messages (excluding bodies) + debug_body -- dump control protocol message bodies + """ + self.debugging = bits + + def _debug(self, bit, s): + # debug output + if self.debugging & bit: + sys.stderr.write(_sanitize(s)) + sys.stderr.write("\n") + sys.stderr.flush() + + def connect(self): + """Connect to the DisOrder server and authenticate. + + Raises communicationError if connection fails and operationError if + authentication fails (in which case disconnection is automatic). + + May be called more than once to retry connections (e.g. when the + server is down). If we are already connected and authenticated, + this is a no-op. + + Other operations automatically connect if we're not already + connected, so it is not strictly necessary to call this method. + """ + if self.state == 'disconnected': + try: + self.state = 'connecting' + if 'connect' in self.config and len(self.config['connect']) > 0: + c = self.config['connect'] + self.who = repr(c) # temporarily + if len(c) == 1: + a = socket.getaddrinfo(None, c[0], + socket.AF_INET, + socket.SOCK_STREAM, + 0, + 0) + else: + a = socket.getaddrinfo(c[0], c[1], + socket.AF_INET, + socket.SOCK_STREAM, + 0, + 0) + a = a[0] + s = socket.socket(a[0], a[1], a[2]); + s.connect(a[4]) + self.who = "%s" % a[3] + else: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); + self.who = self.config['home'] + os.sep + "socket" + s.connect(self.who) + self.w = s.makefile("wb") + self.r = s.makefile("rb") + (res, challenge) = self._simple() + h = sha.sha() + h.update(self.config['password']) + h.update(binascii.unhexlify(challenge)) + self._simple("user", self.config['username'], h.hexdigest()) + self.state = 'connected' + except socket.error, e: + self._disconnect() + raise communicationError(self.who, e) + except: + self._disconnect() + raise + + def _disconnect(self): + # disconnect from the server, whatever state we are in + try: + del self.w + del self.r + except: + pass + self.state = 'disconnected' + + ######################################################################## + # Operations + + def become(self, who): + """Become another user. + + Arguments: + who -- the user to become. + + Only trusted users can perform this operation. + """ + self._simple("become", who) + + def play(self, track): + """Play a track. + + Arguments: + track -- the path of the track to play. + """ + self._simple("play", track) + + def remove(self, track): + """Remove a track from the queue. + + Arguments: + track -- the path or ID of the track to remove. + """ + self._simple("remove", track) + + def enable(self): + """Enable playing.""" + self._simple("enable") + + def disable(self, *now): + """Disable playing. + + Arguments: + now -- if present (with any value), the current track is stopped + too. + """ + if now: + self._simple("disable", "now") + else: + self._simple("disable") + + def scratch(self, *id): + """Scratch the currently playing track. + + Arguments: + id -- if present, the ID of the track to scratch. + """ + if id: + self._simple("scratch", id[0]) + else: + self._simple("scratch") + + def shutdown(self): + """Shut down the server. + + Only trusted users can perform this operation. + """ + self._simple("shutdown") + + def reconfigure(self): + """Make the server reload its configuration. + + Only trusted users can perform this operation. + """ + self._simple("reconfigure") + + def rescan(self, pattern): + """Rescan one or more collections. + + Arguments: + pattern -- glob pattern matching collections to rescan. + + Only trusted users can perform this operation. + """ + self._simple("rescan", pattern) + + def version(self): + """Return the server's version number.""" + return self._simple("version")[1] + + def playing(self): + """Return the currently playing track. + + If a track is playing then it is returned as a dictionary. + If no track is playing then None is returned.""" + res, details = self._simple("playing") + if res % 10 != 9: + try: + return _queueEntry(details) + except _splitError, s: + raise protocolError(self.who, s.str()) + else: + return None + + def _somequeue(self, command): + self._simple(command) + try: + return map(lambda s: _queueEntry(s), self._body()) + except _splitError, s: + raise protocolError(self.who, s.str()) + + def recent(self): + """Return a list of recently played tracks. + + The return value is a list of dictionaries corresponding to + recently played tracks. The oldest track comes first.""" + return self._somequeue("recent") + + def queue(self): + """Return the current queue. + + The return value is a list of dictionaries corresponding to + recently played tracks. The next track to be played comes first.""" + return self._somequeue("queue") + + def _somedir(self, command, dir, re): + if re: + self._simple(command, dir, re[0]) + else: + self._simple(command, dir) + return self._body() + + def directories(self, dir, *re): + """List subdirectories of a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of the (nonempty) subdirectories of dir. + If dir is '' then a list of top-level directories is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("dirs", dir, re) + + def files(self, dir, *re): + """List files within a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of playable files in dir. If dir is '' + then a list of top-level files is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("files", dir, re) + + def allfiles(self, dir, *re): + """List subdirectories and files within a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of all (nonempty) subdirectories and + files within dir. If dir is '' then a list of top-level files and + directories is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("allfiles", dir, re) + + def set(self, track, key, value): + """Set a preference value. + + Arguments: + track -- the track to modify + key -- the preference name + value -- the new preference value + """ + self._simple("set", track, key, value) + + def unset(self, track, key): + """Unset a preference value. + + Arguments: + track -- the track to modify + key -- the preference to remove + """ + self._simple("set", track, key, value) + + def get(self, track, key): + """Get a preference value. + + Arguments: + track -- the track to query + key -- the preference to remove + + The return value is the preference + """ + ret, details = self._simple("get", track, key) + return details + + def prefs(self, track): + """Get all the preferences for a track. + + Arguments: + track -- the track to query + + The return value is a dictionary of all the track's preferences. + Note that even nominally numeric values remain encoded as strings. + """ + self._simple("prefs", track) + r = {} + for line in self._body(): + try: + kv = _split(line) + except _splitError, s: + raise protocolError(self.who, s.str()) + if len(kv) != 2: + raise protocolError(self.who, "invalid prefs body line") + r[kv[0]] = kv[1] + return r + + def _boolean(self, s): + return s[1] == 'yes' + + def exists(self, track): + """Return true if a track exists + + Arguments: + track -- the track to check for""" + return self._boolean(self._simple("exists", track)) + + def enabled(self): + """Return true if playing is enabled""" + return self._boolean(self._simple("enabled")) + + def random_enabled(self): + """Return true if random play is enabled""" + return self._boolean(self._simple("random-enabled")) + + def random_enable(self): + """Enable random play.""" + self._simple("random-enable") + + def random_disable(self): + """Disable random play.""" + self._simple("random-disable") + + def length(self, track): + """Return the length of a track in seconds. + + Arguments: + track -- the track to query. + """ + ret, details = self._simple("length", track) + return int(details) + + def search(self, words): + """Search for tracks. + + Arguments: + words -- the set of words to search for. + + The return value is a list of track path names, all of which contain + all of the required words (in their path name, trackname + preferences, etc.) + """ + self._simple("search", *words) + return self._body() + + def stats(self): + """Get server statistics. + + The return value is list of statistics. + """ + self._simple("stats") + return self._body() + + def dump(self): + """Get all preferences. + + The return value is an encoded dump of the preferences database. + """ + self._simple("dump") + return self._body() + + def set_volume(self, left, right): + """Set volume. + + Arguments: + left -- volume for the left speaker. + right -- volume for the right speaker. + """ + self._simple("volume", left, right) + + def get_volume(self): + """Get volume. + + The return value a tuple consisting of the left and right volumes. + """ + ret, details = self._simple("volume") + return map(int,string.split(details)) + + def move(self, track, delta): + """Move a track in the queue. + + Arguments: + track -- the name or ID of the track to move + delta -- the number of steps towards the head of the queue to move + """ + ret, details = self._simple("move", track, str(delta)) + return int(details) + + def log(self, callback): + """Read event log entries as they happen. + + Each event log entry is handled by passing it to callback. + + The callback takes two arguments, the first is the client and the + second the line from the event log. + + The callback should return True to continue or False to stop (don't + forget this, or your program will mysteriously misbehave). + + It is suggested that you use the disorder.monitor class instead of + calling this method directly, but this is not mandatory. + + See disorder_protocol(5) for the event log syntax. + + Arguments: + callback -- function to call with log entry + """ + ret, details = self._simple("log") + while True: + l = self._line() + self._debug(client.debug_body, "<<< %s" % l) + if l != '' and l[0] == '.': + if l == '.': + return + l = l[1:] + if not callback(self, l): + break + # tell the server to stop sending, eat the remains of the body, + # eat the response + self._send("version") + self._body() + self._response() + + def pause(self): + """Pause the current track.""" + self._simple("pause") + + def resume(self): + """Resume after a pause.""" + self._simple("resume") + + def part(self, track, context, part): + """Get a track name part + + Arguments: + track -- the track to query + context -- the context ('sort' or 'display') + part -- the desired part (usually 'artist', 'album' or 'title') + + The return value is the preference + """ + ret, details = self._simple("part", track, context, part) + return details + + ######################################################################## + # I/O infrastructure + + def _line(self): + # read one response line and return as some suitable string object + # + # If an I/O error occurs, disconnect from the server. + # + # XXX does readline() DTRT regarding character encodings? + try: + l = self.r.readline() + if not re.search("\n", l): + raise communicationError(self.who, "peer disconnected") + l = l[:-1] + except: + self._disconnect() + raise + return unicode(l, "UTF-8") + + def _response(self): + # read a response as a (code, details) tuple + l = self._line() + self._debug(client.debug_proto, "<== %s" % l) + m = _response.match(l) + if m: + return int(m.group(1)), m.group(2) + else: + raise protocolError(self.who, "invalid response %s") + + def _send(self, *command): + quoted = _quote(command) + self._debug(client.debug_proto, "==> %s" % quoted) + encoded = quoted.encode("UTF-8") + try: + self.w.write(encoded) + self.w.write("\n") + self.w.flush() + except IOError, e: + # e.g. EPIPE + self._disconnect() + raise communicationError(self.who, e) + except: + self._disconnect() + raise + + 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 returns response as a (code, details) tuple + # + # On error raise operationError + if self.state == 'disconnected': + self.connect() + if command: + self._send(*command) + res, details = self._response() + if res / 100 == 2: + return res, details + raise operationError(res, details) + + def _body(self): + # Fetch a dot-stuffed body + result = [] + while True: + l = self._line() + self._debug(client.debug_body, "<<< %s" % l) + if l != '' and l[0] == '.': + if l == '.': + return result + l = l[1:] + result.append(l) + + ######################################################################## + # Configuration file parsing + + def _readfile(self, path): + # Read a configuration file + # + # Arguments: + # + # path -- path of file to read + + # handlers for various commands + def _collection(self, command, args): + if len(args) != 3: + return "'%s' takes three args" % command + self.config["collections"].append(args) + + def _unary(self, command, args): + if len(args) != 1: + return "'%s' takes only one arg" % command + self.config[command] = args[0] + + def _include(self, command, args): + if len(args) != 1: + return "'%s' takes only one arg" % command + self._readfile(args[0]) + + def _any(self, command, args): + self.config[command] = args + + # mapping of options to handlers + _options = { "collection": _collection, + "username": _unary, + "password": _unary, + "home": _unary, + "connect": _any, + "include": _include } + + # the parser + for lno, line in enumerate(file(path, "r")): + try: + fields = _split(line, 'comments') + except _splitError, s: + raise parseError(path, lno + 1, str(s)) + if fields: + command = fields[0] + # we just ignore options we don't know about, so as to cope gracefully + # with version skew (and nothing to do with implementor laziness) + if command in _options: + e = _options[command](self, command, fields[1:]) + if e: + self._parseError(path, lno + 1, e) + + def _parseError(self, path, lno, s): + raise parseError(path, lno, s) + +######################################################################## +# monitor class + +class monitor: + """DisOrder event log monitor class + + 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 + + Can be passed a client to use. If none is specified then one + will be created specially for the purpose. + + Arguments: + c -- client""" + if c == None: + c = client(); + self.c = c + + 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!)""" + self.c.log(self._callback) + + def when(self): + """Return the timestamp of the current (or most recent) event log entry""" + return self.timestamp + + def _callback(self, c, line): + try: + bits = _split(line) + except: + return self.invalid(line) + if(len(bits) < 2): + return self.invalid(line) + self.timestamp = int(bits[0], 16) + keyword = bits[1] + bits = bits[2:] + if keyword == 'completed': + if len(bits) == 1: + return self.completed(bits[0]) + elif keyword == 'failed': + if len(bits) == 2: + return self.failed(bits[0], bits[1]) + elif keyword == 'moved': + if len(bits) == 3: + try: + n = int(bits[1]) + except: + return self.invalid(line) + return self.moved(bits[0], n, bits[2]) + elif keyword == 'playing': + if len(bits) == 1: + return self.playing(bits[0], None) + elif len(bits) == 2: + return self.playing(bits[0], bits[1]) + elif keyword == 'queue' or keyword == 'recent-added': + try: + q = _list2dict(bits) + except: + return self.invalid(line) + if keyword == 'queue': + return self.queue(q) + if keyword == 'recent-added': + return self.recent_added(q) + elif keyword == 'recent-removed': + if len(bits) == 1: + return self.recent_removed(bits[0]) + elif keyword == 'removed': + if len(bits) == 1: + return self.removed(bits[0], None) + elif len(bits) == 2: + return self.removed(bits[0], bits[1]) + elif keyword == 'scratched': + if len(bits) == 2: + return self.scratched(bits[0], bits[1]) + return self.invalid(line) + + def completed(self, track): + """Called when a track completes. + + Arguments: + track -- track that completed""" + return True + + def failed(self, track, error): + """Called when a player suffers an error. + + Arguments: + track -- track that failed + error -- error indicator""" + return True + + def moved(self, id, offset, user): + """Called when a track is moved in the queue. + + Arguments: + id -- queue entry ID + offset -- distance moved + user -- user responsible""" + return True + + def playing(self, track, user): + """Called when a track starts playing. + + Arguments: + track -- track that has started + user -- user that submitted track, or None""" + return True + + def queue(self, q): + """Called when a track is added to the queue. + + Arguments: + q -- dictionary of new queue entry""" + return True + + def recent_added(self, q): + """Called when a track is added to the recently played list + + Arguments: + q -- dictionary of new queue entry""" + return True + + def recent_removed(self, id): + """Called when a track is removed from the recently played list + + Arguments: + id -- ID of removed entry (always the oldest)""" + return True + + def removed(self, id, user): + """Called when a track is removed from the queue, either manually + or in order to play it. + + Arguments: + id -- ID of removed entry + user -- user responsible (or None if we're playing this track)""" + return True + + def scratched(self, track, user): + """Called when a track is scratched + + Arguments: + track -- track that was scratched + user -- user responsible""" + return True + + def invalid(self, line): + """Called when an event log line cannot be interpreted + + Arguments: + line -- line that could not be understood""" + return True + +# Local Variables: +# mode:python +# py-indent-offset:2 +# comment-column:40 +# fill-column:72 +# End: +# arch-tag:eea975737c837febf73a000630b5ecc4 diff --git a/python/tkdisorder b/python/tkdisorder new file mode 100755 index 0000000..243b15e --- /dev/null +++ b/python/tkdisorder @@ -0,0 +1,475 @@ +#! /usr/bin/env python +# +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +"""Graphical user interface for DisOrder""" + +from Tkinter import * +import tkFont +import Queue +import threading +import disorder +import time +import string +import re +import getopt +import sys + +######################################################################## + +# Architecture: +# +# The main (initial) thread of the program runs all GUI code. The GUI is only +# directly modified from inside this thread. We sometimes call this the +# master thread. +# +# We have a background thread, MonitorStateThread, which waits for changes to +# the server's state which we care about. Whenever such a change occurs it +# notifies all the widgets which care about it (and possibly other widgets; +# the current implementation is unsophisticated.) +# +# Widget poll() methods usually, but NOT ALWAYS, called in the +# MonitorStateThread. Other widget methods are call in the master thread. +# +# We have a separate disorder.client for each thread rather than locking a +# single client. MonitorStateThread also has a private disorder.client that +# it uses to watch for server state changes. + +######################################################################## + +class Intercom: + # communication queue into thread containing Tk event loop + # + # Sets up a callback on the event loop (in this thread) which periodically + # checks the queue for elements; if any are found they are executed. + def __init__(self, master): + self.q = Queue.Queue(); + self.master = master + self.poll() + + def poll(self): + try: + item = self.q.get_nowait() + item() + self.master.after_idle(self.poll) + except Queue.Empty: + self.master.after(100, self.poll) + + def put(self, item): + self.q.put(item) + +######################################################################## + +class ProgressBar(Canvas): + # progress bar widget + def __init__(self, master=None, **kw): + Canvas.__init__(self, master, highlightthickness=0, **kw) + self.outer = self.create_rectangle(0, 0, 0, 0, + outline="#000000", + width=1, + fill="#ffffff") + self.bar = self.create_rectangle(0, 0, 0, 0, + width=1, + fill="#ff0000", + outline='#ff0000') + self.current = None + self.total = None + self.bind("<Configure>", lambda e: self.redisplay()) + + def update(self, current, total): + self.current = current + if current > total: + current = total + elif current < 0: + current = 0 + self.total = total + self.redisplay() + + def clear(self): + self.current = None + self.total = None + self.redisplay() + + def redisplay(self): + w, h = self.winfo_width(), self.winfo_height() + if w > 0 and h > 0: + self.coords(self.outer, 0, 0, w - 1, h - 1) + if self.total: + bw = int((w - 2) * self.current / self.total) + self.itemconfig(self.bar, + fill="#ff0000", + outline="#ff0000") + self.coords(self.bar, 1, 1, bw, h - 2) + else: + self.itemconfig(self.bar, + fill="#909090", + outline="#909090") + self.coords(self.bar, 1, 1, w - 2, h - 2) + +# look up a track's name part, using client c. Maintains a cache. +part_cache = {} +def part(c, track, context, part): + key = "%s-%s-%s" % (part, context, track) + now = time.time() + if not part_cache.has_key(key) or part_cache[key]['when'] < now - 3600: + part_cache[key] = {'when': now, + 'what': c.part(track, context, part)} + return part_cache[key]['what'] + +class PlayingWidget(Frame): + # widget that always displays information about what's + # playing + def __init__(self, master=None, **kw): + Frame.__init__(self, master, **kw) + # column 0 is descriptions, column 1 is the values + self.columnconfigure(0,weight=0) + self.columnconfigure(1,weight=1) + self.fields = {} + self.field(0, 0, "artist", "Artist") + self.field(1, 0, "album", "Album") + self.field(2, 0, "title", "Title") + # column 1 also has the progress bar in it + self.p = ProgressBar(self, height=20) + self.p.grid(row=3, column=1, sticky=E+W) + # column 2 has operation buttons + b = Button(self, text="Quit", command=self.quit) + b.grid(row=0, column=2, sticky=E+W) + b = Button(self, text="Scratch", command=self.scratch) + b.grid(row=1, column=2, sticky=E+W) + b = Button(self, text="Recent", command=self.recent) + b.grid(row=2, column=2, sticky=E+W) + self.length = 0 + self.update_length() + self.last = None + self.recentw = None + + def field(self, row, column, name, label): + # create a field + Label(self, text=label).grid(row=row, column=column, sticky=E) + self.fields[name] = Text(self, height=1, state=DISABLED) + self.fields[name].grid(row=row, column=column + 1, sticky=W+E); + + def set(self, name, value): + # set a field's value + f = self.fields[name] + f.config(state=NORMAL) + f.delete(1.0, END) + f.insert(END, value) + f.config(state=DISABLED) + + def playing(self, p): + # called with new what's-playing information + values = {} + if p: + for tpart in ['artist', 'album', 'title']: + values[tpart] = part(client, p['track'], 'display', tpart) + try: + self.length = client.length(p['track']) + except disorder.operationError: + self.length = 0 + self.started = int(p['played']) + else: + self.length = 0 + for k in self.fields.keys(): + if k in values: + self.set(k, values[k]) + else: + self.set(k, "") + self.length_bar() + + def length_bar(self): + if self.length and self.length > 0: + self.p.update(time.time() - self.started, self.length) + else: + self.p.clear() + + def update_length(self): + self.length_bar() + self.after(1000, self.update_length) + + def poll(self, c): + p = c.playing() + if p != self.last: + intercom.put(lambda: self.playing(p)) + self.last = p + + def quit(self): + sys.exit(0) + + def scratch(self): + client.scratch() + + def recent_close(self): + self.recentw.destroy() + self.recentw = None + + def recent(self): + if self.recentw: + self.recentw.deiconify() + self.recentw.lift() + else: + w = 80*tracklistFont.measure('A') + h = 40*tracklistFont.metrics("linespace") + self.recentw = Toplevel() + self.recentw.protocol("WM_DELETE_WINDOW", self.recent_close) + self.recentw.title("Recently Played") + # XXX for some reason Toplevel(width=w,height=h) doesn't seem to work + self.recentw.geometry("%dx%d" % (w,h)) + w = RecentWidget(self.recentw) + w.pack(fill=BOTH, expand=1) + mst.add(w); + +class TrackListWidget(Frame): + def __init__(self, master=None, **kw): + Frame.__init__(self, master, **kw) + self.yscrollbar = Scrollbar(self) + self.xscrollbar = Scrollbar(self, orient=HORIZONTAL) + self.canvas = Canvas(self, + xscrollcommand=self.xscrollbar.set, + yscrollcommand=self.yscrollbar.set) + self.xscrollbar.config(command=self.canvas.xview) + self.yscrollbar.config(command=self.canvas.yview) + self.canvas.grid(row=0, column=0, sticky=N+S+E+W) + self.yscrollbar.grid(row=0, column=1, sticky=N+S) + self.xscrollbar.grid(row=1, column=0, sticky=E+W) + self.columnconfigure(0,weight=1) + self.rowconfigure(0,weight=1) + self.last = None + self.default_cursor = self['cursor'] + self.configure(cursor="watch") + + def queue(self, q, w_artists, w_albums, w_titles, artists, albums, titles): + # called with new queue state + # delete old contents + try: + for i in self.canvas.find_all(): + self.canvas.delete(i) + except TclError: + # if the call was queued but not received before the window was deleted + # we might get an error from Tcl/Tk, which no longer knows the window, + # here + return + w = tracklistHFont.measure("Artist") + if w > w_artists: + w_artists = w + w = tracklistHFont.measure("Album") + if w > w_albums: + w_albums = w + w = tracklistHFont.measure("Title") + if w > w_titles: + w_titles = w + hheading = tracklistHFont.metrics("linespace") + h = tracklistFont.metrics('linespace') + x_artist = 8 + x_album = x_artist + w_artists + 16 + x_title = x_album + w_albums + 16 + w = x_title + w_titles + 8 + self.canvas['scrollregion'] = (0, 0, w, h * len(artists) + hheading) + self.canvas.create_text(x_artist, 0, text="Artist", + font=tracklistHFont, + anchor='nw') + self.canvas.create_text(x_album, 0, text="Album", + font=tracklistHFont, + anchor='nw') + self.canvas.create_text(x_title, 0, text="Title", + font=tracklistHFont, + anchor='nw') + y = hheading + for n in range(0,len(artists)): + artist = artists[n] + album = albums[n] + title = titles[n] + if artist != "": + self.canvas.create_text(x_artist, y, text=artist, + font=tracklistFont, + anchor='nw') + if album != "": + self.canvas.create_text(x_album, y, text=album, + font=tracklistFont, + anchor='nw') + if title != "": + self.canvas.create_text(x_title, y, text=title, + font=tracklistFont, + anchor='nw') + y += h + self.last = q + self.configure(cursor=self.default_cursor) + + def poll(self, c): + q = self.getqueue(c) + if q != self.last: + # we do the track name calculation in the background thread so that + # the gui can still be responsive + artists = [] + albums = [] + titles = [] + w_artists = w_albums = w_titles = 16 + for t in q: + artist = part(c, t['track'], 'display', 'artist') + album = part(c, t['track'], 'display', 'album') + title = part(c, t['track'], 'display', 'title') + w = tracklistFont.measure(artist) + if w > w_artists: + w_artists = w + w = tracklistFont.measure(album) + if w > w_albums: + w_albums = w + w = tracklistFont.measure(title) + if w > w_titles: + w_titles = w + artists.append(artist) + albums.append(album) + titles.append(title) + intercom.put(lambda: self.queue(q, w_artists, w_albums, w_titles, + artists, albums, titles)) + self.last = q + +class QueueWidget(TrackListWidget): + def __init__(self, master=None, **kw): + TrackListWidget.__init__(self, master, **kw) + + def getqueue(self, c): + return c.queue() + +class RecentWidget(TrackListWidget): + def __init__(self, master=None, **kw): + TrackListWidget.__init__(self, master, **kw) + + def getqueue(self, c): + l = c.recent() + l.reverse() + return l + +class MonitorStateThread: + # thread to pick up current server state and publish it + # + # Creates a client and monitors it in a daemon thread for state changes. + # Whenever one occurs, call w.poll(c) for every member w of widgets with + # a client owned by the thread in which the call occurs. + def __init__(self, widgets, masterclient=None): + self.logclient = disorder.client() + self.client = disorder.client() + self.clientlock = threading.Lock() + if not masterclient: + masterclient = disorder.client() + self.masterclient = masterclient + self.widgets = widgets + self.lock = threading.Lock() + # the main thread + self.thread = threading.Thread(target=self.run) + self.thread.setDaemon(True) + self.thread.start() + # spare thread for processing additions + self.adderq = Queue.Queue() + self.adder = threading.Thread(target=self.runadder) + self.adder.setDaemon(True) + self.adder.start() + + def notify(self, line): + self.lock.acquire() + widgets = self.widgets + self.lock.release() + for w in widgets: + self.clientlock.acquire() + w.poll(self.client) + self.clientlock.release() + return 1 + + def add(self, w): + self.lock.acquire() + self.widgets.append(w) + self.lock.release() + self.adderq.put(lambda client: w.poll(client)) + + def remove(self, what): + self.lock.acquire() + self.widgets.remove(what) + self.lock.release() + + def run(self): + self.notify("") + self.logclient.log(lambda client, line: self.notify(line)) + + def runadder(self): + while True: + item = self.adderq.get() + self.clientlock.acquire() + item(self.client) + self.clientlock.release() + +######################################################################## + +def usage(s): + # display usage on S + s.write( + """Usage: + + tkdisorder [OPTIONS] + +Options: + + -h, --help Display this message + -V, --version Display version number + +tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell. +""") + +######################################################################## + +try: + opts, rest = getopt.getopt(sys.argv[1:], "Vh", ["version", "help"]) +except getopt.GetoptError, e: + sys.stderr.write("ERROR: %s, try --help for help\n" % e.msg) + sys.exit(1) +for o, v in opts: + if o in ('-V', '--version'): + print "%s" % disorder.version + sys.stdout.close() + sys.exit(0) + if o in ('h', '--help'): + usage(sys.stdout) + sys.stdout.close() + sys.exit(0) + +client = disorder.client() # master thread's client + +root = Tk() +root.title("DisOrder") + +tracklistFont = tkFont.Font(family='Helvetica', size=10) +tracklistHFont = tracklistFont.copy() +tracklistHFont.config(weight="bold") + +p = PlayingWidget(root) +p.pack(fill=BOTH, expand=1) + +q = QueueWidget(root) +q.pack(fill=BOTH, expand=1) + +intercom = Intercom(root) # only need a single intercom +mst = MonitorStateThread([p, q], client) + +root.mainloop() + +# Local Variables: +# py-indent-offset:2 +# comment-column:40 +# fill-column:79 +# End: +# arch-tag:d2c64241856ce5b19824e848f26c674b diff --git a/scripts/Makefile.am b/scripts/Makefile.am new file mode 100644 index 0000000..84ad902 --- /dev/null +++ b/scripts/Makefile.am @@ -0,0 +1,24 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +dist_pkgdata_DATA=completion.bash + +EXTRA_DIST=htmlman sedfiles.make text2c oggrename +# arch-tag:49137b3a04a5bc92cd8c17d4372db87b diff --git a/scripts/check b/scripts/check new file mode 100755 index 0000000..b105277 --- /dev/null +++ b/scripts/check @@ -0,0 +1,78 @@ +#! /usr/bin/perl -w +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +use strict; + +my $year; +my %checked = (); +my %removed = (); +my $prefix = -e ".src" ? ".src/" : ""; +my $what = ""; +my $original; +my @exceptions; +my %exceptions; +my %missing = (); + +open(F, "<${prefix}scripts/copyright.exceptions") or die "$0: scripts/copyright.exceptions: $!"; +chomp(@exceptions = <F>); +%exceptions = map(($_, 1), grep !/^\#/, @exceptions); + +opendir(D, "${prefix}ChangeLog.d") or die "$0: ChangeLog.d: $!\n"; +for my $dir (readdir D) { + next if $dir =~ /cvs/; + open(C, "<${prefix}ChangeLog.d/$dir") or die "$0: ChangeLog.d/$dir: $!\n"; + while(defined($_ = <C>)) { + if(/^(\d{4})-\d{2}-\d{2}/) { + $year = $1; + } + if(/^\s+(modified|removed|renamed) files:$/) { + $what = $1; + next; + } + if(/^\s*$/) { + $what = ""; + } + if($what eq 'modified') { + my @files = split(/\s+/, $_); + for my $file (@files) { + next if exists $checked{"$file-$year"}; + next if $file =~ /^ChangeLog\.d/; + next if ! -e "$prefix$file"; + open(INPUT, "<$prefix$file") or die "$0: $prefix$file: $!\n"; + my $good = 0; + while(defined(my $line = <INPUT>)) { + if($line =~ /Copyright.*$year/i) { + $good = 1; + last; + } + } + close INPUT; + $checked{"$file-$year"} = $good; + $missing{"$file: missing $year"} = 1 + if !$good && !exists $exceptions{$file}; + } + } + } +} + +print map("$_\n", sort keys %missing); + +# arch-tag:PRdTcEnpHD8PVsb6Bp+QTQ diff --git a/scripts/completion.bash b/scripts/completion.bash new file mode 100644 index 0000000..cf237c3 --- /dev/null +++ b/scripts/completion.bash @@ -0,0 +1,63 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +complete -r disorder 2>/dev/null || true +complete -r disorderd 2>/dev/null || true +complete -r disorder-dump 2>/dev/null || true +complete -r disobedience 2>/dev/null || true + +complete -o default \ + -A file \ + -W "allfiles authorize become dirs disable disable-random + enable enable-random files get get-volume length log move + play playing prefs quack queue random-disable + random-enable recent reconfigure remove rescan scratch + search set set-volume shutdown stats unset version resolve + part pause resume scratch-id get-global set-global unset-global + tags + -h --help -H --help-commands --version -V --config -c + --length --debug -d" \ + disorder + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c + --debug --dump -d --undump -u --recover -r --recompute-aliases + -a" \ + disorder-dump + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c --debug -d --foreground -f + --pidfile -P" \ + disorderd + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c --debug -d --tufnel -t + --gtk-module --g-fatal-warnings --gtk-debug --gtk-no-debug + --class --name --gdk-debug --gdk-no-debug + --display --screen --sync --gxid-host --gxid-port" \ + disobedience + +# Local Variables: +# mode:sh +# End: +# arch-tag:2XNe1BR5K6TxF+njsXAByw diff --git a/scripts/copyright.exceptions b/scripts/copyright.exceptions new file mode 100644 index 0000000..4d64d50 --- /dev/null +++ b/scripts/copyright.exceptions @@ -0,0 +1,25 @@ +BUGS +CHANGES +README.raw +README.upgrades +README.client +debian/README.Debian +debian/changelog +debian/changelog +debian/conffiles +debian/control +debian/disorder.config +debian/options.debian +debian/templates +doc/checklist.txt +examples/config.sample.in +images/edit.png +sounds/slap.ogg +templates/options +templates/options.labels +templates/options.labels +{arch}/=tagging-method +{arch}/=tagging-method +disobedience/TODO +scripts/copyright.exceptions +# arch-tag:ez2jSX+4DAfwt7/ucp/JYQ diff --git a/scripts/dist b/scripts/dist new file mode 100755 index 0000000..5583849 --- /dev/null +++ b/scripts/dist @@ -0,0 +1,38 @@ +#! /bin/bash +# +# This file is part of DisOrder +# Copyright (C) 2005, 2006 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e +[ -d =build ] && cd =build +make +make check +make dist-bzip2 +d=$(make echo-distdir) +cp $d.tar.bz2 $HOME/work/web/disorder +cp .src/CHANGES $HOME/work/web/disorder/CHANGES.txt +cp .src/README $HOME/work/web/disorder/README.txt +cp .src/ChangeLog.d/*--* $HOME/work/web/disorder/ChangeLog.d +cd doc +for f in *.[1-9].html; do + echo $f + rm -f $HOME/work/web/disorder/$f + sed < $f > $HOME/work/web/disorder/$f 's/^@.*//' +done +# arch-tag:jpK3z3qFK+Sv/8QVvpMUIg diff --git a/scripts/htmlman b/scripts/htmlman new file mode 100755 index 0000000..54b1402 --- /dev/null +++ b/scripts/htmlman @@ -0,0 +1,52 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +title=$(basename $1) + +cat <<EOF +<html> + <head> +@include{stdhead}@ + <title>$title + + +@include{@label{menu}@}@ +EOF +printf "
"
+# this is kind of painful using only BREs
+nroff -man "$1" | sed 's/&/\&/g;
+                       s//\>/g;
+                       s/@/\@/g;
+                       s!\(.\)\1!\1!g;
+                       s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
+                       s!_\(.\)!\1!g;
+                       s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
+                       s!<\1>!!g'
+cat <
+@include{@label{menu}@end}@
+ 
+
+EOF
+# arch-tag:c0096f33b8a8f7d88236043ed970ae83
diff --git a/scripts/inst b/scripts/inst
new file mode 100755
index 0000000..e0a51f0
--- /dev/null
+++ b/scripts/inst
@@ -0,0 +1,29 @@
+#! /bin/bash
+#
+# Copyright (C) 2004, 2005, 2006 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+set -e
+set -x
+[ -d =build ] && cd =build
+make "$@"
+make check
+really make "$@" install
+really ./libtool --mode=install install -m 755 server/disorder.cgi /home/jukebox/public_html/index.cgi
+really ldconfig
+# arch-tag:cbb189b7f23939cc806cf6dc0c2cb0d5
diff --git a/scripts/makedeb b/scripts/makedeb
new file mode 100755
index 0000000..0531460
--- /dev/null
+++ b/scripts/makedeb
@@ -0,0 +1,37 @@
+#! /bin/bash
+#
+# Copyright (C) 2004, 2005 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+set -e
+buildhost=${buildhost:-lyonesse}
+builddir=${builddir:-.}
+make dist-bzip2
+distdir=`make echo-distdir`
+scp $distdir.tar.bz2 $buildhost:$builddir/$distdir.tar.bz2
+ssh $buildhost "
+  set -e
+  set -x
+  cd $builddir
+  rm -rf $distdir
+  tar xfj $distdir.tar.bz2
+  cd $distdir
+  CC='ccache cc' debian/rules build
+  fakeroot debian/rules binary
+"
+# arch-tag:3ebf6ef38076b7cb313b86843e741d97
diff --git a/scripts/oggrename b/scripts/oggrename
new file mode 100644
index 0000000..458a32b
--- /dev/null
+++ b/scripts/oggrename
@@ -0,0 +1,58 @@
+#! /usr/bin/perl -w
+#
+# This file is part of DisOrder
+# Copyright (C) 2006 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+use strict;
+
+my %rename = ();
+my $bad = 0;
+
+for my $path (@ARGV) {
+    local $_ = `ogginfo \Q$path\E`;
+    my ($title, $number);
+    if(/title=(.*)/) { $title = $1; }
+    if(/tracknumber=(\d+)/) { $number = $1; }
+    if(!defined $title || !defined $number) {
+	print STDERR "ERROR: cannot find details for $path\n";
+	++$bad;
+	next;
+    }
+    $rename{$path} = sprintf("%02d:%s.ogg", $number, $title);
+}
+exit 1 if $bad;
+
+while(scalar %rename) {
+    my $worked = 0;
+    while(my ($f, $t) = each %rename) {
+	if($f eq $t) {
+	    delete $rename{$f};
+	    $worked = 1;
+	} elsif(link($f, $t)) {
+	    unlink($f);
+	    delete $rename{$f};
+	    print "$f -> $t\n";
+	    $worked = 1;
+	} else {
+	    print "deferring $f -> $t\n";
+	}
+    }
+    die "stuck in a loop!\n" if !$worked;
+}
+# arch-tag:AU/Rh0oscz3GBqwzJYRy1w
diff --git a/scripts/sedfiles.make b/scripts/sedfiles.make
new file mode 100644
index 0000000..534c5de
--- /dev/null
+++ b/scripts/sedfiles.make
@@ -0,0 +1,36 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+$(SEDFILES) : % : %.in Makefile
+	rm -f $@.new
+	sed 's!sbindir!${sbindir}!g;\
+	     s!bindir!${bindir}!g;\
+	     s!pkgconfdir!${sysconfdir}/disorder!g;\
+	     s!pkgstatedir!${localstatedir}/disorder!g;\
+	     s!pkgdatadir!${pkgdatadir}!g;\
+	     s!_version_!${VERSION}!g;\
+	        ' < $< > $@.new
+	chmod 444 $@.new
+	mv -f $@.new $@
+
+# Local Variables:
+# mode:makefile
+# End:
+# arch-tag:97f84bf68cbbc5a0d72871aa0e704447
diff --git a/scripts/text2c b/scripts/text2c
new file mode 100755
index 0000000..23079f3
--- /dev/null
+++ b/scripts/text2c
@@ -0,0 +1,15 @@
+#! /usr/bin/perl -w
+my $name = shift;
+push(@out, "/* autogenerated file, do not edit */\n\n");
+push(@out, "static const char $name\[] = \n");
+while(<>) {
+    next if /arch-tag/;
+    s/[\\\"\?]/\\$&/g;
+    s/\n/\\n/g;
+    push(@out, "  \"$_\"\n");
+}
+push(@out, ";\n");
+((print @out)
+ and (close STDOUT))
+    or die "$0: stdout: $!\n";
+# arch-tag:2eRG7Dpm6lpL7BR3hPvriQ
diff --git a/server/Makefile.am b/server/Makefile.am
new file mode 100644
index 0000000..d0e8087
--- /dev/null
+++ b/server/Makefile.am
@@ -0,0 +1,84 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005, 2006 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
+	      disorder-speaker
+noinst_PROGRAMS=disorder.cgi trackname
+
+AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
+
+disorderd_SOURCES=disorderd.c				\
+	api.c api-server.c				\
+	daemonize.c daemonize.h				\
+	play.c play.h 					\
+	server.c server.h     				\
+	state.c state.h					\
+	trackdb.c trackdb.h trackdb-int.h
+disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBPCRE) $(LIBDB) $(LIBAO)
+disorderd_LDFLAGS=-export-dynamic
+disorderd_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_deadlock_SOURCES=deadlock.c                    \
+	trackdb.c trackdb.h
+disorder_deadlock_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBDB)
+disorder_deadlock_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_speaker_SOURCES=speaker.c
+disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBASOUND)
+disorder_speaker_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_rescan_SOURCES=rescan.c                        \
+	api.c api-server.c				\
+	trackdb.c trackdb.h
+disorder_rescan_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBDB)
+disorder_rescan_LDFLAGS=-export-dynamic
+disorder_rescan_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_dump_SOURCES=dump.c                           \
+        trackdb.c trackdb.h
+disorder_dump_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBPCRE) $(LIBDB)
+disorder_dump_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la
+
+disorder_cgi_SOURCES=dcgi.c dcgi.h			\
+	api.c api-client.c api-client.h			\
+	cgi.c cgi.h cgimain.c
+disorder_cgi_LDADD=../lib/libdisorder.la $(LIBPCRE)
+disorder_cgi_LDFLAGS=-export-dynamic
+disorder_cgi_DEPENDENCIES=../lib/libdisorder.la
+
+trackname_SOURCES=trackname.c
+trackname_LDADD=../lib/libdisorder.la
+trackname_DEPENDENCIES=../lib/libdisorder.la
+
+install-exec-hook:
+	$(LIBTOOL) --mode=finish $(DESTDIR)$(libdir)
+
+check: check-help
+
+# check everything has working --help
+check-help: all
+	./disorderd --help > /dev/null
+	./disorder-dump --help > /dev/null
+	./disorder-deadlock --help > /dev/null
+	./trackname --help > /dev/null
+	./disorder-speaker --help > /dev/null
+
+cgi.o: ../lib/definitions.h
+# arch-tag:f36fc0fa65dd5143a80874c1c83f595f
diff --git a/server/api-client.c b/server/api-client.c
new file mode 100644
index 0000000..a69ec7e
--- /dev/null
+++ b/server/api-client.c
@@ -0,0 +1,73 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+
+static disorder_client *c;
+
+disorder_client *disorder_get_client(void) {
+  if(!c)
+    if(!(c = disorder_new(0))) exit(EXIT_FAILURE);
+  return c;
+}
+
+int disorder_track_exists(const char *track) {
+  int result;
+
+  return disorder_exists(c, track, &result) ? 0 : result;
+}
+
+const char *disorder_track_get_data(const char *track, const char *key) {
+  char *value;
+
+  if(disorder_get(c, track, key, &value)) return 0;
+  return value;
+}
+
+int disorder_track_set_data(const char *track,
+			    const char *key,
+			    const char *value) {
+  if(value)
+    return disorder_set(c, track, key, value);
+  else
+    return disorder_unset(c, track, key);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:3c00ffad971b689e5af1d9a9ff45a120 */
diff --git a/server/api-client.h b/server/api-client.h
new file mode 100644
index 0000000..174405d
--- /dev/null
+++ b/server/api-client.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#ifndef API_CLIENT_H
+#define API_CLIENT_H
+
+disorder_client *disorder_get_client(void);
+
+#endif /* API_CLIENT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:fe8ca21b625a51949cea0f5e2d319215 */
diff --git a/server/api-server.c b/server/api-server.c
new file mode 100644
index 0000000..18056f2
--- /dev/null
+++ b/server/api-server.c
@@ -0,0 +1,60 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "event.h"
+#include "trackdb.h"
+
+int disorder_track_exists(const char *track)  {
+  return trackdb_exists(track);
+}
+
+const char *disorder_track_get_data(const char *track, const char *key)  {
+  return trackdb_get(track, key);
+}
+
+int disorder_track_set_data(const char *track,
+			    const char *key, const char *value)  {
+  return trackdb_set(track, key, value);
+}
+
+const char *disorder_track_random(void)  {
+  return trackdb_random(16);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:b39fbeb5a45190fa30e7f3ac20cd6c34 */
diff --git a/server/api.c b/server/api.c
new file mode 100644
index 0000000..8d631ca
--- /dev/null
+++ b/server/api.c
@@ -0,0 +1,78 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "printf.h"
+
+/* shared implementation of vararg functions */
+#include "log-impl.h"
+#include "mem-impl.h"
+
+void *disorder_malloc(size_t n) {
+  return xmalloc(n);
+}
+
+void *disorder_realloc(void *p, size_t n) {
+  return xrealloc(p, n);
+}
+
+void *disorder_malloc_noptr(size_t n) {
+  return xmalloc_noptr(n);
+}
+
+void *disorder_realloc_noptr(void *p, size_t n) {
+  return xrealloc_noptr(p, n);
+}
+
+char *disorder_strdup(const char *p) {
+  return xstrdup(p);
+}
+
+char *disorder_strndup(const char *p, size_t n) {
+  return xstrndup(p, n);
+}
+
+int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) {
+  int n;
+  va_list ap;
+
+  va_start(ap, fmt);
+  n = byte_vsnprintf(buffer, bufsize, fmt, ap);
+  va_end(ap);
+  return n;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a033645978e202c94260fc21959e59a7 */
diff --git a/server/cgi.c b/server/cgi.c
new file mode 100644
index 0000000..02bad31
--- /dev/null
+++ b/server/cgi.c
@@ -0,0 +1,619 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "mem.h"
+#include "log.h"
+#include "hex.h"
+#include "charset.h"
+#include "configuration.h"
+#include "table.h"
+#include "syscalls.h"
+#include "kvp.h"
+#include "vector.h"
+#include "split.h"
+#include "inputline.h"
+#include "regsub.h"
+#include "defs.h"
+#include "sink.h"
+#include "cgi.h"
+#include "printf.h"
+#include "mime.h"
+#include "utf8.h"
+
+struct kvp *cgi_args;
+
+/* options */
+struct column {
+  struct column *next;
+  char *name;
+  int ncolumns;
+  char **columns;
+};
+
+#define RELIST(x) struct re *x, **x##_tail = &x
+
+static int have_read_options;
+static struct kvp *labels;
+static struct column *columns;
+
+static void include_options(const char *name);
+
+static void cgi_parse_get(void) {
+  const char *q;
+
+  if(!(q = getenv("QUERY_STRING"))) fatal(0, "QUERY_STRING not set");
+  cgi_args = kvp_urldecode(q, strlen(q));
+}
+
+static void cgi_input(char **ptrp, size_t *np) {
+  const char *cl;
+  char *q;
+  size_t n, m = 0;
+  int r;
+
+  if(!(cl = getenv("CONTENT_LENGTH"))) fatal(0, "CONTENT_LENGTH not set");
+  n = atol(cl);
+  q = xmalloc_noptr(n + 1);
+  while(m < n) {
+    r = read(0, q + m, n - m);
+    if(r > 0)
+      m += r;
+    else if(r == 0)
+      fatal(0, "unexpected end of file reading request body");
+    else switch(errno) {
+    case EINTR: break;
+    default: fatal(errno, "error reading request body");
+    }
+  }
+  if(memchr(q, 0, n)) fatal(0, "null character in request body");
+  q[n + 1] = 0;
+  *ptrp = q;
+  if(np) *np = n;
+}
+
+static int cgi_field_callback(const char *name, const char *value,
+			      void *u) {
+  char *disposition, *pname, *pvalue;
+  char **namep = u;
+
+  if(!strcmp(name, "content-disposition")) {
+    if(mime_rfc2388_content_disposition(value,
+					&disposition,
+					&pname,
+					&pvalue))
+      fatal(0, "error parsing Content-Disposition field");
+    if(!strcmp(disposition, "form-data")
+       && pname
+       && !strcmp(pname, "name")) {
+      if(*namep)
+	fatal(0, "duplicate Content-Disposition field");
+      *namep = pvalue;
+    }
+  }
+  return 0;
+}
+
+static int cgi_part_callback(const char *s,
+			     void attribute((unused)) *u) {
+  char *name = 0;
+  struct kvp *k;
+  
+  if(!(s = mime_parse(s, cgi_field_callback, &name)))
+    fatal(0, "error parsing part header");
+  if(!name) fatal(0, "no name found");
+  k = xmalloc(sizeof *k);
+  k->next = cgi_args;
+  k->name = name;
+  k->value = s;
+  cgi_args = k;
+  return 0;
+}
+
+static void cgi_parse_multipart(const char *boundary) {
+  char *q;
+  
+  cgi_input(&q, 0);
+  if(mime_multipart(q, cgi_part_callback, boundary, 0))
+    fatal(0, "invalid multipart object");
+}
+
+static void cgi_parse_post(void) {
+  const char *ct;
+  char *q, *type, *pname, *pvalue;
+  size_t n;
+
+  if(!(ct = getenv("CONTENT_TYPE")))
+    ct = "application/x-www-form-urlencoded";
+  if(mime_content_type(ct, &type, &pname, &pvalue))
+    fatal(0, "invalid content type '%s'", ct);
+  if(!strcmp(type, "application/x-www-form-urlencoded")) {
+    cgi_input(&q, &n);
+    cgi_args = kvp_urldecode(q, n);
+    return;
+  }
+  if(!strcmp(type, "multipart/form-data")) {
+    if(!pname || strcmp(pname, "boundary"))
+      fatal(0, "expected a boundary parameter, found %s",
+	    pname ? pname : "nothing");
+    cgi_parse_multipart(pvalue);
+    return;
+  }
+  fatal(0, "unrecognized content type '%s'", type);
+}
+
+void cgi_parse(void) {
+  const char *p;
+  struct kvp *k;
+
+  if(!(p = getenv("REQUEST_METHOD"))) fatal(0, "REQUEST_METHOD not set");
+  if(!strcmp(p, "GET"))
+    cgi_parse_get();
+  else if(!strcmp(p, "POST"))
+    cgi_parse_post();
+  else
+    fatal(0, "unknown request method %s", p);
+  for(k = cgi_args; k; k = k->next)
+    if(!validutf8(k->name)
+       || !validutf8(k->value))
+      fatal(0, "invalid UTF-8 sequence in cgi argument");
+}
+
+const char *cgi_get(const char *name) {
+  return kvp_get(cgi_args, name);
+}
+
+void cgi_output(cgi_sink *output, const char *fmt, ...) {
+  va_list ap;
+  int n;
+  char *r;
+
+  va_start(ap, fmt);
+  n = byte_vasprintf(&r, fmt, ap);
+  if(n < 0)
+    fatal(errno, "error calling byte_vasprintf");
+  if(output->quote)
+    r = cgi_sgmlquote(r, 0);
+  output->sink->write(output->sink, r, strlen(r));
+  va_end(ap);
+}
+
+void cgi_header(struct sink *output, const char *name, const char *value) {
+  sink_printf(output, "%s: %s\r\n", name, value);
+}
+
+void cgi_body(struct sink *output) {
+  sink_printf(output, "\r\n");
+}
+
+char *cgi_sgmlquote(const char *s, int raw) {
+  uint32_t *ucs, *p, c;
+  char *b, *bp;
+  int n;
+
+  if(!raw) {
+    if(!(ucs = utf82ucs4(s))) exit(EXIT_FAILURE);
+  } else {
+    ucs = xmalloc_noptr((strlen(s) + 1) * sizeof(uint32_t));
+    for(n = 0; s[n]; ++n)
+      ucs[n] = (unsigned char)s[n];
+    ucs[n] = 0;
+  }
+
+  n = 1;
+  /* estimate the length we'll need */
+  for(p = ucs; (c = *p); ++p) {
+    switch(c) {
+    default:
+      if(c > 127 || c < 32) {
+      case '"':
+      case '&':
+      case '<':
+      case '>':
+	n += 12;
+	break;
+      } else
+	n++;
+    }
+  }
+  /* format the string */
+  b = bp = xmalloc_noptr(n);
+  for(p = ucs; (c = *p); ++p) {
+    switch(c) {
+    default:
+      if(*p > 127 || *p < 32) {
+      case '"':
+      case '&':
+      case '<':
+      case '>':
+	bp += sprintf(bp, "&#%lu;", (unsigned long)c);
+	break;
+      } else
+	*bp++ = c;
+    }
+  }
+  *bp = 0;
+  return b;
+}
+
+void cgi_attr(struct sink *output, const char *name, const char *value) {
+  if(!value[strspn(value, "abcdefghijklmnopqrstuvwxyz"
+		   "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+		   "0123456789")])
+    sink_printf(output, "%s=%s", name, value);
+  else
+    sink_printf(output, "%s=\"%s\"", name, cgi_sgmlquote(value, 0));
+}
+
+void cgi_opentag(struct sink *output, const char *name, ...) {
+  va_list ap;
+  const char *n, *v;
+   
+  sink_printf(output, "<%s", name);
+  va_start(ap, name);
+  while((n = va_arg(ap, const char *))) {
+    sink_printf(output, " ");
+    v = va_arg(ap, const char *);
+    if(v)
+      cgi_attr(output, n, v);
+    else
+      sink_printf(output, n);
+  }
+  sink_printf(output, ">");
+}
+
+void cgi_closetag(struct sink *output, const char *name) {
+  sink_printf(output, "", name);
+}
+
+static int template_open(const char *name,
+			 const char *ext,
+			 const char **filenamep) {
+  const char *dirs[2];
+  int fd = -1, n;
+  char *fullpath;
+
+  dirs[0] = pkgconfdir;
+  dirs[1] = pkgdatadir;
+  if(name[0] == '/') {
+    if((fd = open(name, O_RDONLY)) < 0) fatal(0, "cannot open %s", name);
+    *filenamep = name;
+  } else {
+    for(n = 0; n < config->templates.n + (int)(sizeof dirs / sizeof *dirs); ++n) {
+      byte_xasprintf(&fullpath, "%s/%s%s",
+		     n < config->templates.n ? config->templates.s[n]
+		                             : dirs[n - config->templates.n],
+		     name, ext);
+      if((fd = open(fullpath, O_RDONLY)) >= 0) break;
+    }
+    if(fd < 0) error(0, "cannot find %s%s in template path", name, ext);
+    *filenamep = fullpath;
+  }
+  return fd;
+}
+
+static int valid_template_name(const char *name) {
+  if(strchr(name, '/') || name[0] == '.')
+    return 0;
+  return 1;
+}
+
+void cgi_expand(const char *template,
+		const struct cgi_expansion *expansions,
+		size_t nexpansions,
+		cgi_sink *output,
+		void *u) {
+  int fd = -1;
+  int n;
+  off_t m;
+  char *b;
+  struct stat sb;
+
+  if(!valid_template_name(template))
+    fatal(0, "invalid template name '%s'", template);
+  if((fd = template_open(template, ".html", &template)) < 0)
+    exitfn(EXIT_FAILURE);
+  if(fstat(fd, &sb) < 0) fatal(errno, "cannot stat %s", template);
+  m = 0;
+  b = xmalloc_noptr(sb.st_size + 1);
+  while(m < sb.st_size) {
+    n = read(fd, b + m, sb.st_size - m);
+    if(n > 0) m += n;
+    else if(n == 0) fatal(0, "unexpected EOF reading %s", template);
+    else if(errno != EINTR) fatal(errno, "error reading %s", template);
+  }
+  b[sb.st_size] = 0;
+  xclose(fd);
+  cgi_expand_string(template, b, expansions, nexpansions, output, u);
+}
+
+void cgi_expand_string(const char *name,
+		       const char *template,
+		       const struct cgi_expansion *expansions,
+		       size_t nexpansions,
+		       cgi_sink *output,
+		       void *u) {
+  int braces, n, m, line = 1, sline;
+  char *argname;
+  const char *p;
+  struct vector v;
+  struct dynstr d;
+  cgi_sink parameter_output;
+  
+  while(*template) {
+    if(*template != '@') {
+      p = template;
+      while(*p && *p != '@') {
+	if(*p == '\n') ++line;
+	++p;
+      }
+      output->sink->write(output->sink, template, p - template);
+      template = p;
+      continue;
+    }
+    vector_init(&v);
+    braces = 0;
+    ++template;
+    sline = line;
+    while(*template != '@') {
+      dynstr_init(&d);
+      if(*template == '{') {
+	/* bracketed arg */
+	++template;
+	while(*template && (*template != '}' || braces > 0)) {
+	  switch(*template) {
+	  case '{': ++braces; break;
+	  case '}': --braces; break;
+	  case '\n': ++line; break;
+	  }
+	  dynstr_append(&d, *template++);
+	}
+	if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+	++template;
+	/* skip whitespace after closing bracket */
+	while(isspace((unsigned char)*template))
+	  ++template;
+      } else {
+	/* unbracketed arg */
+	/* leading whitespace is not significant in unquoted args */
+	while(isspace((unsigned char)*template))
+	  ++template;
+	while(*template
+	      && *template != '@' && *template != '{' && *template != ':') {
+	  if(*template == '\n') ++line;
+	  dynstr_append(&d, *template++);
+	}
+	if(*template == ':')
+	  ++template;
+	if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+	/* trailing whitespace is not significant in unquoted args */
+	while(d.nvec && (isspace((unsigned char)d.vec[d.nvec - 1])))
+	  --d.nvec;
+      }
+      dynstr_terminate(&d);
+      vector_append(&v, d.vec);
+    }
+    ++template;
+    vector_terminate(&v);
+    /* @@ terminates this file */
+    if(v.nvec == 0)
+      break;
+    if((n = table_find(expansions,
+		       offsetof(struct cgi_expansion, name),
+		       sizeof (struct cgi_expansion),
+		       nexpansions,
+		       v.vec[0])) < 0)
+      fatal(0, "%s:%d: unknown expansion '%s'", name, line, v.vec[0]);
+    if(v.nvec - 1 < expansions[n].minargs)
+      fatal(0, "%s:%d: insufficient arguments to @%s@ (min %d, got %d)",
+	    name, line, v.vec[0], expansions[n].minargs, v.nvec - 1);
+    if(v.nvec - 1 > expansions[n].maxargs)
+      fatal(0, "%s:%d: too many arguments to @%s@ (max %d, got %d)",
+	    name, line, v.vec[0], expansions[n].maxargs, v.nvec - 1);
+    /* for ordinary expansions, recursively expand the arguments */
+    if(!(expansions[n].flags & EXP_MAGIC)) {
+      for(m = 1; m < v.nvec; ++m) {
+	dynstr_init(&d);
+	byte_xasprintf(&argname, "<%s:%d arg #%d>", name, sline, m);
+	parameter_output.quote = 0;
+	parameter_output.sink = sink_dynstr(&d);
+	cgi_expand_string(argname, v.vec[m],
+			  expansions, nexpansions,
+			  ¶meter_output, u);
+	dynstr_terminate(&d);
+	v.vec[m] = d.vec;
+      }
+    }
+    expansions[n].handler(v.nvec - 1, v.vec + 1, output, u);
+  }
+}
+
+char *cgi_makeurl(const char *url, ...) {
+  va_list ap;
+  struct kvp *kvp, *k, **kk = &kvp;
+  struct dynstr d;
+  const char *n, *v;
+  
+  dynstr_init(&d);
+  dynstr_append_string(&d, url);
+  va_start(ap, url);
+  while((n = va_arg(ap, const char *))) {
+    v = va_arg(ap, const char *);
+    *kk = k = xmalloc(sizeof *k);
+    kk = &k->next;
+    k->name = n;
+    k->value = v;
+  }
+  *kk = 0;
+  if(kvp) {
+    dynstr_append(&d, '?');
+    dynstr_append_string(&d, kvp_urlencode(kvp, 0));
+  }
+  dynstr_terminate(&d);
+  return d.vec;
+}
+
+void cgi_set_option(const char *name, const char *value) {
+  struct kvp *k = xmalloc(sizeof *k);
+
+  k->next = labels;
+  k->name = name;
+  k->value = value;
+  labels = k;
+}
+
+static void option_label(int attribute((unused)) nvec,
+			 char **vec) {
+  cgi_set_option(vec[0], vec[1]);
+}
+
+static void option_include(int attribute((unused)) nvec,
+			   char **vec) {
+  include_options(vec[0]);
+}
+
+static void option_columns(int nvec,
+			    char **vec) {
+  struct column *c = xmalloc(sizeof *c);
+  
+  c->next = columns;
+  c->name = vec[0];
+  c->ncolumns = nvec - 1;
+  c->columns = &vec[1];
+  columns = c;
+}
+
+static struct option {
+  const char *name;
+  int minargs, maxargs;
+  void (*handler)(int nvec, char **vec);
+} options[] = {
+  { "columns", 1, INT_MAX, option_columns },
+  { "include", 1, 1, option_include },
+  { "label", 2, 2, option_label },
+};
+
+struct read_options_state {
+  const char *name;
+  int line;
+};
+
+static void read_options_error(const char *msg,
+			       void *u) {
+  struct read_options_state *cs = u;
+  
+  error(0, "%s:%d: %s", cs->name, cs->line, msg);
+}
+
+static void include_options(const char *name) {
+  int n, i;
+  int fd;
+  FILE *fp;
+  char **vec, *buffer;
+  struct read_options_state cs;
+
+  if((fd = template_open(name, "", &cs.name)) < 0) return;
+  if(!(fp = fdopen(fd, "r"))) fatal(errno, "error calling fdopen");
+  cs.line = 0;
+  while(!inputline(cs.name, fp, &buffer, '\n')) {
+    ++cs.line;
+    if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
+		     read_options_error, &cs)))
+      continue;
+    if(!n) continue;
+    if((i = TABLE_FIND(options, struct option, name, vec[0])) == -1) {
+      error(0, "%s:%d: unknown option '%s'", cs.name, cs.line, vec[0]);
+      continue;
+    }
+    ++vec;
+    --n;
+    if(n < options[i].minargs) {
+      error(0, "%s:%d: too few arguments to '%s'", cs.name, cs.line, vec[-1]);
+      continue;
+    }
+    if(n > options[i].maxargs) {
+      error(0, "%s:%d: too many arguments to '%s'", cs.name, cs.line, vec[-1]);
+      continue;
+    }
+    options[i].handler(n, vec);
+  }
+  fclose(fp);
+}
+
+static void read_options(void) {
+  if(!have_read_options) {
+    have_read_options = 1;
+    include_options("options");
+  }
+}
+
+const char *cgi_label(const char *key) {
+  const char *label;
+
+  read_options();
+  if(!(label = kvp_get(labels, key))) {
+    if((label = strchr(key, '.')))
+      ++label;
+    else
+      label = key;
+  }
+  return label;
+}
+
+char **cgi_columns(const char *name, int *ncolumns) {
+  struct column *c;
+
+  read_options();
+  for(c = columns; c && strcmp(name, c->name); c = c->next)
+    ;
+  if(c) {
+    if(ncolumns)
+      *ncolumns = c->ncolumns;
+    return c->columns;
+  } else {
+    if(ncolumns)
+      *ncolumns = 0;
+    return 0;
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a7a5220f29b8bb8d64c0f836f7f41f1f */
diff --git a/server/cgi.h b/server/cgi.h
new file mode 100644
index 0000000..e871052
--- /dev/null
+++ b/server/cgi.h
@@ -0,0 +1,108 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef CGI_H
+#define CGI_H
+
+extern struct kvp *cgi_args;
+
+typedef struct {
+  int quote;
+  struct sink *sink;
+} cgi_sink;
+
+void cgi_parse(void);
+/* parse CGI args */
+
+const char *cgi_get(const char *name);
+/* get an argument */
+
+void cgi_header(struct sink *output, const char *name, const char *value);
+/* output a header.  @name@ and @value@ are ASCII. */
+
+void cgi_body(struct sink *output);
+/* indicate the start of the body */
+
+void cgi_output(cgi_sink *output, const char *fmt, ...)
+  attribute((format (printf, 2, 3)));
+/* SGML-quote formatted UTF-8 data and write it.  Checks errors. */
+
+char *cgi_sgmlquote(const char *s, int raw);
+/* SGML-quote multibyte string @s@ */
+
+void cgi_attr(struct sink *output, const char *name, const char *value);
+/* write an attribute */
+
+void cgi_opentag(struct sink *output, const char *name, ...);
+/* write an open tag, including attribute name-value pairs terminate
+ * by (char *)0 */
+
+void cgi_closetag(struct sink *output, const char *name);
+/* write a close tag */
+
+struct cgi_expansion {
+  const char *name;
+  int minargs, maxargs;
+  unsigned flags;
+#define EXP_MAGIC 0x0001
+  void (*handler)(int nargs, char **args, cgi_sink *output, void *u);
+};
+
+void cgi_expand(const char *name,
+		const struct cgi_expansion *expansions,
+		size_t nexpansions,
+		cgi_sink *output,
+		void *u);
+/* find @name@ and substitute for expansions */
+
+void cgi_expand_string(const char *name,
+		       const char *template,
+		       const struct cgi_expansion *expansions,
+		       size_t nexpansions,
+		       cgi_sink *output,
+		       void *u);
+/* same but @template@ is text of template */
+
+char *cgi_makeurl(const char *url, ...);
+/* make up a URL */
+
+const char *cgi_label(const char *key);
+/* look up the translated label @key@ */
+
+char **cgi_columns(const char *name, int *nheadings);
+/* return the list of columns for @name@ */
+
+const char *cgi_transform(const char *type,
+			  const char *track,
+			  const char *context);
+/* transform a track or directory name for display */
+
+void cgi_set_option(const char *name, const char *value);
+/* set an option */
+
+#endif /* CGI_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:46351d0acf757f7867a139f369342949 */
diff --git a/server/cgimain.c b/server/cgimain.c
new file mode 100644
index 0000000..9be0d1b
--- /dev/null
+++ b/server/cgimain.c
@@ -0,0 +1,80 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+  
+int main(int argc, char **argv) {
+  const char *user, *conf;
+  dcgi_global g;
+  dcgi_state s;
+  cgi_sink output;
+
+  mem_init(0);
+  if(argc > 0) progname = argv[0];
+  cgi_parse();
+  if((conf = getenv("DISORDER_CONFIG"))) configfile = xstrdup(conf);
+  if(getenv("DISORDER_DEBUG")) debugging = 1;
+  if(config_read()) exit(EXIT_FAILURE);
+  memset(&g, 0, sizeof g);
+  memset(&s, 0, sizeof s);
+  s.g = &g;
+  g.client = disorder_get_client();
+  output.quote = 1;
+  output.sink = sink_stdio("stdout", stdout); 
+  if(!(user = getenv("REMOTE_USER"))) fatal(0, "REMOTE_USER is not set");
+  if(disorder_connect(g.client)) {
+    disorder_cgi_error(&output, &s, "connect");
+    return 0;
+  }
+  if(disorder_become(g.client, user)) {
+    disorder_cgi_error(&output, &s, "become");
+    return 0;
+  }
+  disorder_cgi(&output, &s);
+  if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:1c8016a19d7f117427c184c8aadf1bba */
diff --git a/server/daemonize.c b/server/daemonize.c
new file mode 100644
index 0000000..bddc167
--- /dev/null
+++ b/server/daemonize.c
@@ -0,0 +1,89 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "daemonize.h"
+#include "syscalls.h"
+#include "log.h"
+
+void daemonize(const char *tag, int fac, const char *pidfile) {
+  pid_t pid, r;
+  int w, dn;
+  FILE *fp;
+
+  D(("daemonize tag=%s fac=%d pidfile=%s",
+     tag ? tag : "NULL", fac, pidfile ? pidfile : "NULL"));
+  /* make sure that FDs 0, 1, 2 all at least exist (and get a
+   * /dev/null) */
+  do {
+    if((dn = open("/dev/null", O_RDWR, 0)) < 0)
+      fatal(errno, "error opening /dev/null");
+  } while(dn < 3);
+  pid = xfork();
+  if(pid) {
+    /* Parent process.  Wait for the first child to finish, then
+     * return to the caller. */
+    exitfn = _exit;
+    while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+      ;
+    if(r < 0) fatal(errno, "error calling waitpid");
+    if(w) error(0, "subprocess exited with wait status %#x", (unsigned)w);
+    _exit(0);
+  }
+  /* First child process.  This will be the session leader, and will
+   * be transient. */
+  D(("first child pid=%lu", (unsigned long)getpid()));
+  if(setsid() < 0) fatal(errno, "error calling setsid");
+  /* we'll log to syslog */
+  openlog(tag, LOG_PID, fac);
+  log_default = &log_syslog;
+  /* stdin/out/err we lose */
+  xdup2(dn, 0);
+  xdup2(dn, 1);
+  xdup2(dn, 2);
+  xclose(dn);
+  pid = xfork();
+  if(pid)
+    _exit(0);
+  /* second child.  Write a pidfile if someone wanted it. */
+  D(("second child pid=%lu", (unsigned long)getpid()));
+  if(pidfile) {
+    if(!(fp = fopen(pidfile, "w"))
+       || fprintf(fp, "%lu\n", (unsigned long)getpid()) < 0
+       || fclose(fp) < 0)
+      fatal(errno, "error creating %s", pidfile);
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:dc74d9007ddce97d782d3dacf9aa2ed6 */
diff --git a/server/daemonize.h b/server/daemonize.h
new file mode 100644
index 0000000..97e9edd
--- /dev/null
+++ b/server/daemonize.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef DAEMONIZE_H
+#define DAEMONIZE_H
+
+void daemonize(const char *tag, int fac, const char *pidfile);
+/* Go into background.  Send stdout/stderr to syslog.
+ * If @pri@ is non-null, it should be "facility.level"
+ * If @tag@ is non-null, it is used as a tag to each message
+ * If @pidfile@ is non-null, the PID is written to that file.
+ */
+
+#endif /* DAEMONIZE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:6fae05bd34750bba75b336637f6a0c56 */
diff --git a/server/dcgi.c b/server/dcgi.c
new file mode 100644
index 0000000..1f2d044
--- /dev/null
+++ b/server/dcgi.c
@@ -0,0 +1,1482 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "mem.h"
+#include "vector.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "log.h"
+#include "configuration.h"
+#include "table.h"
+#include "queue.h"
+#include "plugin.h"
+#include "split.h"
+#include "words.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "syscalls.h"
+#include "printf.h"
+#include "regsub.h"
+#include "defs.h"
+#include "trackname.h"
+
+static void expand(cgi_sink *output,
+		   const char *template,
+		   dcgi_state *ds);
+static void expandstring(cgi_sink *output,
+			 const char *string,
+			 dcgi_state *ds);
+
+struct entry {
+  const char *path;
+  const char *sort;
+  const char *display;
+};
+
+static const char *nonce(void) {
+  static unsigned long count;
+  char *s;
+
+  byte_xasprintf(&s, "%lx%lx%lx",
+	   (unsigned long)time(0),
+	   (unsigned long)getpid(),
+	   count++);
+  return s;
+}
+
+static int compare_entry(const void *a, const void *b) {
+  const struct entry *ea = a, *eb = b;
+
+  return compare_tracks(ea->sort, eb->sort,
+			ea->display, eb->display,
+			ea->path, eb->path);
+}
+
+static const char *front_url(void) {
+  char *url;
+  const char *mgmt;
+
+  /* preserve management interface visibility */
+  if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
+    byte_xasprintf(&url, "%s?mgmt=true", config->url);
+    return url;
+  }
+  return config->url;
+}
+
+static void redirect(struct sink *output) {
+  const char *back;
+
+  cgi_header(output, "Location",
+	     (back = cgi_get("back")) ? back : front_url());
+  cgi_body(output);
+}
+
+static void lookups(dcgi_state *ds, unsigned want) {
+  unsigned need;
+  struct queue_entry *r, *rnext;
+  const char *dir, *re;
+
+  if(ds->g->client && (need = want ^ (ds->g->flags & want)) != 0) {
+    if(need & DC_QUEUE)
+      disorder_queue(ds->g->client, &ds->g->queue);
+    if(need & DC_PLAYING)
+      disorder_playing(ds->g->client, &ds->g->playing);
+    if(need & DC_RECENT) {
+      /* we need to reverse the order of the list */
+      disorder_recent(ds->g->client, &r);
+      while(r) {
+	rnext = r->next;
+	r->next = ds->g->recent;
+	ds->g->recent = r;
+	r = rnext;
+      }
+    }
+    if(need & DC_VOLUME)
+      disorder_get_volume(ds->g->client,
+			  &ds->g->volume_left, &ds->g->volume_right);
+    if(need & (DC_FILES|DC_DIRS)) {
+      if(!(dir = cgi_get("directory")))
+	dir = "";
+      re = cgi_get("regexp");
+      if(need & DC_DIRS)
+	if(disorder_directories(ds->g->client, dir, re,
+				&ds->g->dirs, &ds->g->ndirs))
+	  ds->g->ndirs = 0;
+      if(need & DC_FILES)
+	if(disorder_files(ds->g->client, dir, re,
+			  &ds->g->files, &ds->g->nfiles))
+	  ds->g->nfiles = 0;
+    }
+    ds->g->flags |= need;
+  }
+}
+
+/* actions ********************************************************************/
+
+static void act_disable(cgi_sink *output,
+			dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_disable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_enable(cgi_sink *output,
+			      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_enable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_random_disable(cgi_sink *output,
+			       dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_random_disable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_random_enable(cgi_sink *output,
+			      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_random_enable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_remove(cgi_sink *output,
+		       dcgi_state *ds) {
+  const char *id;
+
+  if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+  if(ds->g->client)
+    disorder_remove(ds->g->client, id);
+  redirect(output->sink);
+}
+
+static void act_move(cgi_sink *output,
+		     dcgi_state *ds) {
+  const char *id, *delta;
+
+  if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+  if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
+  if(ds->g->client)
+    disorder_move(ds->g->client, id, atoi(delta));
+  redirect(output->sink);
+}
+
+static void act_scratch(cgi_sink *output,
+			dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_scratch(ds->g->client, cgi_get("id"));
+  redirect(output->sink);
+}
+
+static void act_playing(cgi_sink *output, dcgi_state *ds) {
+  char r[1024];
+  long refresh = config->refresh, length;
+  time_t now, fin;
+  int random_enabled = 0;
+  int enabled = 0;
+
+  lookups(ds, DC_PLAYING|DC_QUEUE);
+  cgi_header(output->sink, "Content-Type", "text/html");
+  disorder_random_enabled(ds->g->client, &random_enabled);
+  disorder_enabled(ds->g->client, &enabled);
+  if(ds->g->playing
+     && ds->g->playing->state == playing_started /* i.e. not paused */
+     && !disorder_length(ds->g->client, ds->g->playing->track, &length)
+     && length
+     && ds->g->playing->sofar >= 0) {
+    /* Try to put the next refresh at the start of the next track. */
+    time(&now);
+    fin = now + length - ds->g->playing->sofar + config->gap;
+    if(now + refresh > fin)
+      refresh = fin - now;
+  }
+  if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
+    /* next track is a scratch, don't leave more than the inter-track gap */
+    if(refresh > config->gap)
+      refresh = config->gap;
+  }
+  if(!ds->g->playing && ((ds->g->queue
+			  && ds->g->queue->state != playing_random)
+			 || random_enabled) && enabled) {
+    /* no track playing but playing is enabled and there is something coming
+     * up, must be in a gap */
+    if(refresh > config->gap)
+      refresh = config->gap;
+  }
+  byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
+		front_url());
+  cgi_header(output->sink, "Refresh", r);
+  cgi_body(output->sink);
+  expand(output, "playing", ds);
+}
+
+static void act_play(cgi_sink *output,
+		     dcgi_state *ds) {
+  const char *track, *dir;
+  char **tracks;
+  int ntracks, n;
+  struct entry *e;
+
+  if((track = cgi_get("file"))) {
+    disorder_play(ds->g->client, track);
+  } else if((dir = cgi_get("directory"))) {
+    if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
+    if(ntracks) {
+      e = xmalloc(ntracks * sizeof (struct entry));
+      for(n = 0; n < ntracks; ++n) {
+	e[n].path = tracks[n];
+	e[n].sort = trackname_transform("track", tracks[n], "sort");
+	e[n].display = trackname_transform("track", tracks[n], "display");
+      }
+      qsort(e, ntracks, sizeof (struct entry), compare_entry);
+      for(n = 0; n < ntracks; ++n)
+	disorder_play(ds->g->client, e[n].path);
+    }
+  }
+  /* XXX error handling */
+  redirect(output->sink);
+}
+
+static int clamp(int n, int min, int max) {
+  if(n < min)
+    return min;
+  if(n > max)
+    return max;
+  return n;
+}
+
+static const char *volume_url(void) {
+  char *url;
+  
+  byte_xasprintf(&url, "%s?action=volume", config->url);
+  return url;
+}
+
+static void act_volume(cgi_sink *output, dcgi_state *ds) {
+  const char *l, *r, *d, *back;
+  int nd, changed = 0;;
+
+  if((d = cgi_get("delta"))) {
+    lookups(ds, DC_VOLUME);
+    nd = clamp(atoi(d), -255, 255);
+    disorder_set_volume(ds->g->client,
+			clamp(ds->g->volume_left + nd, 0, 255),
+			clamp(ds->g->volume_right + nd, 0, 255));
+    changed = 1;
+  } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
+    disorder_set_volume(ds->g->client, atoi(l), atoi(r));
+    changed = 1;
+  }
+  if(changed) {
+    /* redirect back to ourselves (but without the volume-changing bits in the
+     * URL) */
+    cgi_header(output->sink, "Location",
+	       (back = cgi_get("back")) ? back : volume_url());
+    cgi_body(output->sink);
+  } else {
+    cgi_header(output->sink, "Content-Type", "text/html");
+    cgi_body(output->sink);
+    expand(output, "volume", ds);
+  }
+}
+
+static void act_prefs_errors(const char *msg,
+			     void attribute((unused)) *u) {
+  fatal(0, "error splitting parts list: %s", msg);
+}
+
+static const char *numbered_arg(const char *argname, int numfile) {
+  char *fullname;
+
+  byte_xasprintf(&fullname, "%d_%s", numfile, argname);
+  return cgi_get(fullname);
+}
+
+static void process_prefs(dcgi_state *ds, int numfile) {
+  const char *file, *name, *value, *part, *parts, *current, *context;
+  char **partslist;
+
+  if(!(file = numbered_arg("file", numfile)))
+    /* The first file doesn't need numbering. */
+    if(numfile > 0 || !(file = cgi_get("file")))
+      return;
+  if((parts = numbered_arg("parts", numfile))
+     || (parts = cgi_get("parts"))) {
+    /* Default context is display.  Other contexts not actually tested. */
+    if(!(context = numbered_arg("context", numfile))) context = "display";
+    partslist = split(parts, 0, 0, act_prefs_errors, 0);
+    while((part = *partslist++)) {
+      if(!(value = numbered_arg(part, numfile)))
+	continue;
+      /* If it's already right (whether regexps or db) don't change anything,
+       * so we don't fill the database up with rubbish. */
+      if(disorder_part(ds->g->client, (char **)¤t,
+		       file, context, part))
+	fatal(0, "disorder_part() failed");
+      if(!strcmp(current, value))
+	continue;
+      byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
+      disorder_set(ds->g->client, file, name, value);
+    }
+    if((value = numbered_arg("random", numfile)))
+      disorder_unset(ds->g->client, file, "pick_at_random");
+    else
+      disorder_set(ds->g->client, file, "pick_at_random", "0");
+    if((value = numbered_arg("tags", numfile)))
+      disorder_set(ds->g->client, file, "tags", value);
+  } else if((name = cgi_get("name"))) {
+    /* Raw preferences.  Not well supported in the templates at the moment. */
+    value = cgi_get("value");
+    if(value)
+      disorder_set(ds->g->client, file, name, value);
+    else
+      disorder_unset(ds->g->client, file, name);
+  }
+}
+
+static void act_prefs(cgi_sink *output, dcgi_state *ds) {
+  const char *files;
+  int nfiles, numfile;
+
+  if((files = cgi_get("files"))) nfiles = atoi(files);
+  else nfiles = 1;
+  for(numfile = 0; numfile < nfiles; ++numfile)
+    process_prefs(ds, numfile);
+  cgi_header(output->sink, "Content-Type", "text/html");
+  cgi_body(output->sink);
+  expand(output, "prefs", ds);
+}
+
+static void act_pause(cgi_sink *output,
+		      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_pause(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_resume(cgi_sink *output,
+		       dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_resume(ds->g->client);
+  redirect(output->sink);
+}
+
+static const struct action {
+  const char *name;
+  void (*handler)(cgi_sink *output, dcgi_state *ds);
+} actions[] = {
+  { "disable", act_disable },
+  { "enable", act_enable },
+  { "move", act_move },
+  { "pause", act_pause },
+  { "play", act_play },
+  { "playing", act_playing },
+  { "prefs", act_prefs },
+  { "random-disable", act_random_disable },
+  { "random-enable", act_random_enable },
+  { "remove", act_remove },
+  { "resume", act_resume },
+  { "scratch", act_scratch },
+  { "volume", act_volume },
+};
+
+/* expansions *****************************************************************/
+
+static void exp_include(int attribute((unused)) nargs,
+			char **args,
+			cgi_sink *output,
+			void *u) {
+  expand(output, args[0], u);
+}
+
+static void exp_server_version(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  const char *v;
+
+  if(ds->g->client) {
+    if(disorder_version(ds->g->client, (char **)&v)) v = "(cannot get version)";
+  } else
+    v = "(server not running)";
+  cgi_output(output, "%s", v);
+}
+
+static void exp_version(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void attribute((unused)) *u) {
+  cgi_output(output, "%s", disorder_version_string);
+}
+
+static void exp_nonce(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  cgi_output(output, "%s", nonce());
+}
+
+static void exp_label(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  cgi_output(output, "%s", cgi_label(args[0]));
+}
+
+struct trackinfo_state {
+  dcgi_state *ds;
+  const struct queue_entry *q;
+  long length;
+  time_t when;
+};
+
+static void exp_who(int attribute((unused)) nargs,
+		    char attribute((unused)) **args,
+		    cgi_sink *output,
+		    void *u) {
+  dcgi_state *ds = u;
+  
+  if(ds->track && ds->track->submitter)
+    cgi_output(output, "%s", ds->track->submitter);
+}
+
+static void exp_length(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  long length;
+
+  if(ds->track
+     && (ds->track->state == playing_started
+	 || ds->track->state == playing_paused)
+     && ds->track->sofar >= 0)
+    cgi_output(output, "%ld:%02ld/",
+	       ds->track->sofar / 60, ds->track->sofar % 60);
+  if(!ds->track || disorder_length(ds->g->client, ds->track->track, &length))
+    length = 0;
+  if(length)
+    cgi_output(output, "%ld:%02ld", length / 60, length % 60);
+  else
+    sink_printf(output->sink, "%s", " ");
+}
+
+static void exp_when(int attribute((unused)) nargs,
+		     char attribute((unused)) **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+  const struct tm *w = 0;
+
+  if(ds->track)
+    switch(ds->track->state) {
+    case playing_isscratch:
+    case playing_unplayed:
+    case playing_random:
+      if(ds->track->expected)
+	w = localtime(&ds->track->expected);
+      break;
+    case playing_failed:
+    case playing_no_player:
+    case playing_ok:
+    case playing_scratched:
+    case playing_started:
+    case playing_paused:
+    case playing_quitting:
+      if(ds->track->played)
+	w = localtime(&ds->track->played);
+      break;
+    }
+  if(w)
+    cgi_output(output, "%d:%02d", w->tm_hour, w->tm_min);
+  else
+    sink_printf(output->sink, " ");
+}
+
+static void exp_part(int nargs,
+		     char **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+  const char *s, *track, *part, *context;
+
+  if(nargs == 3)
+    track = args[2];
+  else {
+    if(ds->track)
+      track = ds->track->track;
+    else if(ds->tracks)
+      track = ds->tracks[0];
+    else
+      track = 0;
+  }
+  if(track) {
+    switch(nargs) {
+    case 1:
+      context = "display";
+      part = args[0];
+      break;
+    case 2:
+    case 3:
+      context = args[0];
+      part = args[1];
+      break;
+    default:
+      abort();
+    }
+    if(disorder_part(ds->g->client, (char **)&s, track, context, part))
+      fatal(0, "disorder_part() failed");
+    cgi_output(output, "%s", s);
+  } else
+    sink_printf(output->sink, " ");
+}
+
+static void exp_playing(int attribute((unused)) nargs,
+			char **args,
+			cgi_sink *output,
+			void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+
+  lookups(ds, DC_PLAYING);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  if(ds->g->playing) {
+    s.track = ds->g->playing;
+    expandstring(output, args[0], &s);
+  }
+}
+
+static void exp_queue(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+  struct queue_entry *q;
+
+  lookups(ds, DC_QUEUE);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  s.first = 1;
+  for(q = ds->g->queue; q; q = q->next) {
+    s.last = !q->next;
+    s.track = q;
+    expandstring(output, args[0], &s);
+    s.index++;
+    s.first = 0;
+  }
+}
+
+static void exp_recent(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+  struct queue_entry *q;
+
+  lookups(ds, DC_RECENT);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  s.first = 1;
+  for(q = ds->g->recent; q; q = q->next) {
+    s.last = !q;
+    s.track = q;
+    expandstring(output, args[0], &s);
+    s.index++;
+    s.first = 0;
+  }
+}
+
+static void exp_url(int attribute((unused)) nargs,
+		    char attribute((unused)) **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  cgi_output(output, "%s", config->url);
+}
+
+struct result {
+  char *track;
+  const char *sort;
+};
+
+static int compare_result(const void *a, const void *b) {
+  const struct result *ra = a, *rb = b;
+  int c;
+
+  if(!(c = strcmp(ra->sort, rb->sort)))
+    c = strcmp(ra->track, rb->track);
+  return c;
+}
+
+static void exp_search(int nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u, substate;
+  char **tracks;
+  const char *q, *context, *part, *template;
+  int ntracks, n, m;
+  struct result *r;
+
+  switch(nargs) {
+  case 2:
+    part = args[0];
+    context = "sort";
+    template = args[1];
+    break;
+  case 3:
+    part = args[0];
+    context = args[1];
+    template = args[2];
+    break;
+  default:
+    assert(!"should never happen");
+    part = context = template = 0;	/* quieten compiler */
+  }
+  if(ds->tracks == 0) {
+    /* we are the top level, let's get some search results */
+    if(!(q = cgi_get("query"))) return;	/* no results yet */
+    if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
+    if(!ntracks) return;
+  } else {
+    tracks = ds->tracks;
+    ntracks = ds->ntracks;
+  }
+  assert(ntracks != 0);
+  /* sort tracks by the appropriate part */
+  r = xmalloc(ntracks * sizeof *r);
+  for(n = 0; n < ntracks; ++n) {
+    r[n].track = tracks[n];
+    if(disorder_part(ds->g->client, (char **)&r[n].sort,
+		     tracks[n], context, part))
+      fatal(0, "disorder_part() failed");
+  }
+  qsort(r, ntracks, sizeof (struct result), compare_result);
+  /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
+   * array as we know it's guaranteed to be big enough and isn't going to be
+   * used for anything else any more. */
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  n = 0;
+  while(n < ntracks) {
+    substate.tracks = tracks;
+    substate.ntracks = 0;
+    m = n;
+    while(m < ntracks
+	  && !strcmp(r[m].sort, r[n].sort))
+      tracks[substate.ntracks++] = r[m++].track;
+    substate.last = (m == ntracks);
+    expandstring(output, template, &substate);
+    substate.index++;
+    substate.first = 0;
+    n = m;
+  }
+  assert(substate.last != 0);
+}
+
+static void exp_arg(int attribute((unused)) nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  const char *v;
+
+  if((v = cgi_get(args[0])))
+    cgi_output(output, "%s", v);
+}
+
+static void exp_stats(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  char **v;
+
+  cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
+  if(!disorder_stats(ds->g->client, &v, 0)) {
+    while(*v)
+      cgi_output(output, "%s\n", *v++);
+  }
+  cgi_closetag(output->sink, "pre");
+}
+
+static void exp_volume(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_VOLUME);
+  if(!strcmp(args[0], "left"))
+    cgi_output(output, "%d", ds->g->volume_left);
+  else
+    cgi_output(output, "%d", ds->g->volume_right);
+}
+
+static void exp_shell(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  int w, p[2], n;
+  char buffer[4096];
+  pid_t pid;
+  
+  xpipe(p);
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    xclose(p[0]);
+    xdup2(p[1], 1);
+    xclose(p[1]);
+    execlp("sh", "sh", "-c", args[0], (char *)0);
+    fatal(errno, "error executing sh");
+  }
+  xclose(p[1]);
+  while((n = read(p[0], buffer, sizeof buffer))) {
+    if(n < 0) {
+      if(errno == EINTR) continue;
+      else fatal(errno, "error reading from pipe");
+    }
+    output->sink->write(output->sink, buffer, n);
+  }
+  xclose(p[0]);
+  while((n = waitpid(pid, &w, 0)) < 0 && errno == EINTR)
+    ;
+  if(n < 0) fatal(errno, "error calling waitpid");
+  if(w)
+    error(0, "shell command '%s' %s", args[0], wstat(w));
+}
+
+static inline int str2bool(const char *s) {
+  return !strcmp(s, "true");
+}
+
+static inline const char *bool2str(int n) {
+  return n ? "true" : "false";
+}
+
+static char *expandarg(const char *arg, dcgi_state *ds) {
+  struct dynstr d;
+  cgi_sink output;
+
+  dynstr_init(&d);
+  output.quote = 0;
+  output.sink = sink_dynstr(&d);
+  expandstring(&output, arg, ds);
+  dynstr_terminate(&d);
+  return d.vec;
+}
+
+static void exp_prefs(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  struct kvp *k;
+  const char *file = expandarg(args[0], ds);
+  
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  if(disorder_prefs(ds->g->client, file, &k)) return;
+  while(k) {
+    substate.last = !k->next;
+    substate.pref = k;
+    expandstring(output, args[1], &substate);
+    ++substate.index;
+    k = k->next;
+    substate.first = 0;
+  }
+}
+
+static void exp_pref(int attribute((unused)) nargs,
+		     char **args,
+		     cgi_sink *output,
+		     void *u) {
+  char *value;
+  dcgi_state *ds = u;
+
+  if(!disorder_get(ds->g->client, args[0], args[1], &value))
+    cgi_output(output, "%s", value);
+}
+
+static void exp_if(int nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+  int n = str2bool(expandarg(args[0], ds)) ? 1 : 2;
+  
+  if(n < nargs)
+    expandstring(output, args[n], ds);
+}
+
+static void exp_and(int nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void *u) {
+  dcgi_state *ds = u;
+  int n, result = 1;
+
+  for(n = 0; n < nargs; ++n)
+    if(!str2bool(expandarg(args[n], ds))) {
+      result = 0;
+      break;
+    }
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_or(int nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+  int n, result = 0;
+
+  for(n = 0; n < nargs; ++n)
+    if(str2bool(expandarg(args[n], ds))) {
+      result = 1;
+      break;
+    }
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_not(int attribute((unused)) nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  sink_printf(output->sink, "%s", bool2str(!str2bool(args[0])));
+}
+
+static void exp_isplaying(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_PLAYING);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->playing));
+}
+
+static void exp_isqueue(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_QUEUE);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->queue));
+}
+
+static void exp_isrecent(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_RECENT);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->recent));
+}
+
+static void exp_id(int attribute((unused)) nargs,
+		   char attribute((unused)) **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", ds->track->id);
+}
+
+static void exp_track(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", ds->track->track);
+}
+
+static void exp_parity(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+
+  cgi_output(output, "%s", ds->index % 2 ? "odd" : "even");
+}
+
+static void exp_comment(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink attribute((unused)) *output,
+			void attribute((unused)) *u) {
+  /* do nothing */
+}
+
+static void exp_prefname(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->pref && ds->pref->name)
+    cgi_output(output, "%s", ds->pref->name);
+}
+
+static void exp_prefvalue(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->pref && ds->pref->value)
+    cgi_output(output, "%s", ds->pref->value);
+}
+
+static void exp_isfiles(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_FILES);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
+}
+
+static void exp_isdirectories(int attribute((unused)) nargs,
+			      char attribute((unused)) **args,
+			      cgi_sink *output,
+			      void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_DIRS);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
+}
+
+static void exp_choose(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  int nfiles, n;
+  char **files;
+  struct entry *e;
+  const char *type, *what = expandarg(args[0], ds);
+
+  if(!strcmp(what, "files")) {
+    lookups(ds, DC_FILES);
+    files = ds->g->files;
+    nfiles = ds->g->nfiles;
+    type = "track";
+  } else if(!strcmp(what, "directories")) {
+    lookups(ds, DC_DIRS);
+    files = ds->g->dirs;
+    nfiles = ds->g->ndirs;
+    type = "dir";
+  } else {
+    error(0, "unknown @choose@ argument '%s'", what);
+    return;
+  }
+  e = xmalloc(nfiles * sizeof (struct entry));
+  for(n = 0; n < nfiles; ++n) {
+    e[n].path = files[n];
+    e[n].sort = trackname_transform(type, files[n], "sort");
+    e[n].display = trackname_transform(type, files[n], "display");
+  }
+  qsort(e, nfiles, sizeof (struct entry), compare_entry);
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  for(n = 0; n < nfiles; ++n) {
+    substate.last = (n == nfiles - 1);
+    substate.index = n;
+    substate.entry = &e[n];
+    expandstring(output, args[1], &substate);
+    substate.first = 0;
+  }
+}
+
+static void exp_file(int attribute((unused)) nargs,
+		     char attribute((unused)) **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->entry)
+    cgi_output(output, "%s", ds->entry->path);
+  else if(ds->track)
+    cgi_output(output, "%s", ds->track->track);
+  else if(ds->tracks)
+    cgi_output(output, "%s", ds->tracks[0]);
+}
+
+static void exp_transform(int nargs,
+			  char **args,
+			  cgi_sink *output,
+			  void attribute((unused)) *u) {
+  const char *context = nargs > 2 ? args[2] : "display";
+
+  cgi_output(output, "%s", trackname_transform(args[1], args[0], context));
+}
+
+static void exp_urlquote(int attribute((unused)) nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void attribute((unused)) *u) {
+  cgi_output(output, "%s", urlencodestring(args[0]));
+}
+
+static void exp_scratchable(int attribute((unused)) nargs,
+			    char attribute((unused)) **args,
+			    cgi_sink *output,
+			    void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  int result;
+
+  if(config->restrictions & RESTRICT_SCRATCH) {
+    lookups(ds, DC_PLAYING);
+    result = (ds->g->playing
+	      && (!ds->g->playing->submitter
+		  || !strcmp(ds->g->playing->submitter,
+			     disorder_user(ds->g->client))));
+  } else
+    result = 1;
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_removable(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  int result;
+
+  if(config->restrictions & RESTRICT_REMOVE)
+    result = (ds->track
+	      && ds->track->submitter
+	      && !strcmp(ds->track->submitter,
+			 disorder_user(ds->g->client)));
+  else
+    result = 1;
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_navigate(int attribute((unused)) nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  const char *path = expandarg(args[0], ds);
+  const char *ptr;
+  int dirlen;
+
+  if(*path) {
+    memset(&substate, 0, sizeof substate);
+    substate.g = ds->g;
+    ptr = path + 1;			/* skip root */
+    dirlen = 0;
+    substate.nav_path = path;
+    substate.first = 1;
+    while(*ptr) {
+      while(*ptr && *ptr != '/')
+	++ptr;
+      substate.last = !*ptr;
+      substate.nav_len = ptr - path;
+      substate.nav_dirlen = dirlen;
+      expandstring(output, args[1], &substate);
+      dirlen = substate.nav_len;
+      if(*ptr) ++ptr;
+      substate.first = 0;
+    }
+  }
+}
+
+static void exp_fullname(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
+}
+
+static void exp_basename(int nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  const char *s;
+  
+  if(nargs) {
+    if((s = strrchr(args[0], '/'))) ++s;
+    else s = args[0];
+    cgi_output(output, "%s", s);
+  } else
+    cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
+	       ds->nav_path + ds->nav_dirlen + 1);
+}
+
+static void exp_dirname(int nargs,
+			char **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+  const char *s;
+  
+  if(nargs) {
+    if((s = strrchr(args[0], '/')))
+      cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
+  } else
+    cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
+}
+
+static void exp_eq(int attribute((unused)) nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void attribute((unused)) *u) {
+  cgi_output(output, "%s", bool2str(!strcmp(args[0], args[1])));
+}
+
+static void exp_ne(int attribute((unused)) nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void attribute((unused)) *u) {
+  cgi_output(output, "%s", bool2str(strcmp(args[0], args[1])));
+}
+
+static void exp_enabled(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  int enabled = 0;
+
+  if(ds->g->client)
+    disorder_enabled(ds->g->client, &enabled);
+  cgi_output(output, "%s", bool2str(enabled));
+}
+
+static void exp_random_enabled(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  int enabled = 0;
+
+  if(ds->g->client)
+    disorder_random_enabled(ds->g->client, &enabled);
+  cgi_output(output, "%s", bool2str(enabled));
+}
+
+static void exp_trackstate(int attribute((unused)) nargs,
+			   char **args,
+			   cgi_sink *output,
+			   void *u) {
+  dcgi_state *ds = u;
+  struct queue_entry *q;
+  char *track;
+
+  if(disorder_resolve(ds->g->client, &track, args[0])) return;
+  lookups(ds, DC_QUEUE|DC_PLAYING);
+  if(ds->g->playing && !strcmp(ds->g->playing->track, track))
+    cgi_output(output, "playing");
+  else {
+    for(q = ds->g->queue; q && strcmp(q->track, track); q = q->next)
+      ;
+    if(q)
+      cgi_output(output, "queued");
+  }
+}
+
+static void exp_thisurl(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void attribute((unused)) *u) {
+  kvp_set(&cgi_args, "nonce", nonce());	/* nonces had better differ! */
+  cgi_output(output, "%s?%s", config->url, kvp_urlencode(cgi_args, 0));
+}
+
+static void exp_isfirst(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  sink_printf(output->sink, "%s", bool2str(!!ds->first));
+}
+
+static void exp_islast(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  sink_printf(output->sink, "%s", bool2str(!!ds->last));
+}
+
+static void exp_action(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void attribute((unused)) *u) {
+  const char *action = cgi_get("action"), *mgmt;
+
+  if(!action) action = "playing";
+  if(!strcmp(action, "playing")
+     && (mgmt = cgi_get("mgmt"))
+     && !strcmp(mgmt, "true"))
+    action = "manage";
+  sink_printf(output->sink, "%s", action);
+}
+
+static void exp_resolve(int attribute((unused)) nargs,
+                      char  **args,
+                      cgi_sink *output,
+                      void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  char *track;
+  
+  if(!disorder_resolve(ds->g->client, &track, args[0]))
+    sink_printf(output->sink, "%s", track);
+}
+ 
+static void exp_paused(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  int paused = 0;
+
+  lookups(ds, DC_PLAYING);
+  if(ds->g->playing && ds->g->playing->state == playing_paused)
+    paused = 1;
+  cgi_output(output, "%s", bool2str(paused));
+}
+
+static void exp_state(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", playing_states[ds->track->state]);
+}
+
+static void exp_files(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  const char *nfiles_arg, *directory;
+  int nfiles, numfile;
+  struct kvp *k;
+
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  if((directory = cgi_get("directory"))) {
+    /* Prefs for whole directory. */
+    lookups(ds, DC_FILES);
+    /* Synthesize args for the file list. */
+    nfiles = ds->g->nfiles;
+    for(numfile = 0; numfile < nfiles; ++numfile) {
+      k = xmalloc(sizeof *k);
+      byte_xasprintf((char **)&k->name, "%d_file", numfile);
+      k->value = ds->g->files[numfile];
+      k->next = cgi_args;
+      cgi_args = k;
+    }
+  } else {
+    /* Args already present. */
+    if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
+    else nfiles = 1;
+  }
+  for(numfile = 0; numfile < nfiles; ++numfile) {
+    substate.index = numfile;
+    expandstring(output, args[0], &substate);
+  }
+}
+
+static void exp_index(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  cgi_output(output, "%d", ds->index);
+}
+
+static void exp_nfiles(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  const char *files_arg;
+
+  if(cgi_get("directory")) {
+    lookups(ds, DC_FILES);
+    cgi_output(output, "%d", ds->g->nfiles);
+  } else if((files_arg = cgi_get("files")))
+    cgi_output(output, "%s", files_arg);
+  else
+    cgi_output(output, "1");
+}
+
+static const struct cgi_expansion expansions[] = {
+  { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
+  { "action", 0, 0, 0, exp_action },
+  { "and", 0, INT_MAX, EXP_MAGIC, exp_and },
+  { "arg", 1, 1, 0, exp_arg },
+  { "basename", 0, 1, 0, exp_basename },
+  { "choose", 2, 2, EXP_MAGIC, exp_choose },
+  { "dirname", 0, 1, 0, exp_dirname },
+  { "enabled", 0, 0, 0, exp_enabled },
+  { "eq", 2, 2, 0, exp_eq },
+  { "file", 0, 0, 0, exp_file },
+  { "files", 1, 1, EXP_MAGIC, exp_files },
+  { "fullname", 0, 0, 0, exp_fullname },
+  { "id", 0, 0, 0, exp_id },
+  { "if", 2, 3, EXP_MAGIC, exp_if },
+  { "include", 1, 1, 0, exp_include },
+  { "index", 0, 0, 0, exp_index },
+  { "isdirectories", 0, 0, 0, exp_isdirectories },
+  { "isfiles", 0, 0, 0, exp_isfiles },
+  { "isfirst", 0, 0, 0, exp_isfirst },
+  { "islast", 0, 0, 0, exp_islast },
+  { "isplaying", 0, 0, 0, exp_isplaying },
+  { "isqueue", 0, 0, 0, exp_isqueue },
+  { "isrecent", 0, 0, 0, exp_isrecent },
+  { "label", 1, 1, 0, exp_label },
+  { "length", 0, 0, 0, exp_length },
+  { "navigate", 2, 2, EXP_MAGIC, exp_navigate },
+  { "ne", 2, 2, 0, exp_ne },
+  { "nfiles", 0, 0, 0, exp_nfiles },
+  { "nonce", 0, 0, 0, exp_nonce },
+  { "not", 1, 1, 0, exp_not },
+  { "or", 0, INT_MAX, EXP_MAGIC, exp_or },
+  { "parity", 0, 0, 0, exp_parity },
+  { "part", 1, 3, 0, exp_part },
+  { "paused", 0, 0, 0, exp_paused },
+  { "playing", 1, 1, EXP_MAGIC, exp_playing },
+  { "pref", 2, 2, 0, exp_pref },
+  { "prefname", 0, 0, 0, exp_prefname },
+  { "prefs", 2, 2, EXP_MAGIC, exp_prefs },
+  { "prefvalue", 0, 0, 0, exp_prefvalue },
+  { "queue", 1, 1, EXP_MAGIC, exp_queue },
+  { "random-enabled", 0, 0, 0, exp_random_enabled },
+  { "recent", 1, 1, EXP_MAGIC, exp_recent },
+  { "removable", 0, 0, 0, exp_removable },
+  { "resolve", 1, 1, 0, exp_resolve },
+  { "scratchable", 0, 0, 0, exp_scratchable },
+  { "search", 2, 3, EXP_MAGIC, exp_search },
+  { "server-version", 0, 0, 0, exp_server_version },
+  { "shell", 1, 1, 0, exp_shell },
+  { "state", 0, 0, 0, exp_state },
+  { "stats", 0, 0, 0, exp_stats },
+  { "thisurl", 0, 0, 0, exp_thisurl },
+  { "track", 0, 0, 0, exp_track },
+  { "trackstate", 1, 1, 0, exp_trackstate },
+  { "transform", 2, 3, 0, exp_transform },
+  { "url", 0, 0, 0, exp_url },
+  { "urlquote", 1, 1, 0, exp_urlquote },
+  { "version", 0, 0, 0, exp_version },
+  { "volume", 1, 1, 0, exp_volume },
+  { "when", 0, 0, 0, exp_when },
+  { "who", 0, 0, 0, exp_who }
+};
+
+static void expand(cgi_sink *output,
+		   const char *template,
+		   dcgi_state *ds) {
+  cgi_expand(template,
+	     expansions, sizeof expansions / sizeof *expansions,
+	     output,
+	     ds);
+}
+
+static void expandstring(cgi_sink *output,
+			 const char *string,
+			 dcgi_state *ds) {
+  cgi_expand_string("",
+		    string,
+		    expansions, sizeof expansions / sizeof *expansions,
+		    output,
+		    ds);
+}
+
+static void perform_action(cgi_sink *output, dcgi_state *ds,
+			   const char *action) {
+  int n;
+
+  if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
+    actions[n].handler(output, ds);
+  else {
+    cgi_header(output->sink, "Content-Type", "text/html");
+    cgi_body(output->sink);
+    expand(output, action, ds);
+  }
+}
+
+void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
+  const char *action = cgi_get("action");
+
+  if(!action) action = "playing";
+  perform_action(output, ds, action);
+}
+
+void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
+			const char *msg) {
+  cgi_set_option("error", msg);
+  perform_action(output, ds, "error");
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:a3ec8cc0814587e3c39540b2b5ca9e18 */
diff --git a/server/dcgi.h b/server/dcgi.h
new file mode 100644
index 0000000..2846001
--- /dev/null
+++ b/server/dcgi.h
@@ -0,0 +1,66 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef DCGI_H
+#define DCGI_H
+
+typedef struct dcgi_global {
+  disorder_client *client;
+  unsigned flags;
+#define DC_QUEUE 0x0001
+#define DC_PLAYING 0x0002
+#define DC_RECENT 0x0004
+#define DC_VOLUME 0x0008
+#define DC_DIRS 0x0010
+#define DC_FILES 0x0020
+  struct queue_entry *queue, *playing, *recent;
+  int volume_left, volume_right;
+  char **files, **dirs;
+  int nfiles, ndirs;
+} dcgi_global;
+
+typedef struct dcgi_state {
+  dcgi_global *g;
+  struct queue_entry *track;
+  struct kvp *pref;
+  int index;
+  int first, last;
+  struct entry *entry;
+  /* for searching */
+  int ntracks;
+  char **tracks;
+  /* for @navigate@ */
+  const char *nav_path;
+  int nav_len, nav_dirlen;
+} dcgi_state;
+
+void disorder_cgi(cgi_sink *output, dcgi_state *ds);
+void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
+			const char *msg);
+
+#endif /* DCGI_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a555827f0549ee9a35303c2d0f9dabc5 */
diff --git a/server/deadlock.c b/server/deadlock.c
new file mode 100644
index 0000000..aa29b41
--- /dev/null
+++ b/server/deadlock.c
@@ -0,0 +1,124 @@
+/*
+ * This file is part of DisOrder 
+ * Copyright (C) 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "kvp.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-deadlock [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Deadlock manager for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-deadlock version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n, err, aborted;
+
+  set_progname(argv);
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* if stderr is a TTY then log there, otherwise to syslog */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  info("started");
+  trackdb_init(0);
+  while(getppid() != 1) {
+    if((err = trackdb_env->lock_detect(trackdb_env,
+				       0,
+				       DB_LOCK_DEFAULT,
+				       &aborted)))
+      fatal(0, "trackdb_env->lock_detect: %s", db_strerror(err));
+    if(aborted)
+      D(("aborted %d lock requests", aborted));
+    sleep(1);
+  }
+  /* if our parent goes away, it's time to stop */
+  info("stopped (parent terminated)");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:8VzpRqqo2qifzXWK1GxF5A */
diff --git a/server/disorderd.c b/server/disorderd.c
new file mode 100644
index 0000000..06788f8
--- /dev/null
+++ b/server/disorderd.c
@@ -0,0 +1,275 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "daemonize.h"
+#include "event.h"
+#include "log.h"
+#include "configuration.h"
+#include "trackdb.h"
+#include "queue.h"
+#include "mem.h"
+#include "play.h"
+#include "server.h"
+#include "state.h"
+#include "syscalls.h"
+#include "defs.h"
+#include "user.h"
+#include "mixer.h"
+#include "eventlog.h"
+
+static ev_source *ev;
+
+static void rescan_after(long offset);
+static void dbgc_after(long offset);
+static void volumecheck_after(long offset);
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "foreground", no_argument, 0, 'f' },
+  { "log", required_argument, 0, 'l' },
+  { "pidfile", required_argument, 0, 'P' },
+  { "no-initial-rescan", no_argument, 0, 'N' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorderd [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h               Display usage message\n"
+	  "  --version, -V            Display version number\n"
+	  "  --config PATH, -c PATH   Set configuration file\n"
+	  "  --debug, -d              Turn on debugging\n"
+	  "  --foreground, -f         Do not become a daemon\n"
+	  "  --pidfile PATH, -P PATH  Leave a pidfile\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorderd version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* SIGHUP callback */
+static int handle_sighup(ev_source attribute((unused)) *ev_,
+			 int attribute((unused)) sig,
+			 void attribute((unused)) *u) {
+  info("received SIGHUP");
+  reconfigure(ev, 1);
+  return 0;
+}
+
+/* fatal signals */
+
+static int handle_sigint(ev_source attribute((unused)) *ev_,
+			 int attribute((unused)) sig,
+			 void attribute((unused)) *u) {
+  info("received SIGINT");
+  quit(ev);
+}
+
+static int handle_sigterm(ev_source attribute((unused)) *ev_,
+			  int attribute((unused)) sig,
+			  void attribute((unused)) *u) {
+  info("received SIGTERM");
+  quit(ev);
+}
+
+static int rescan_again(ev_source *ev_,
+			const struct timeval attribute((unused)) *now,
+			void attribute((unused)) *u) {
+  trackdb_rescan(ev_);
+  rescan_after(86400);
+  return 0;
+}
+
+static void rescan_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, rescan_again, 0);
+}
+
+static int dbgc_again(ev_source attribute((unused)) *ev_,
+		      const struct timeval attribute((unused)) *now,
+		      void attribute((unused)) *u) {
+  trackdb_gc();
+  dbgc_after(60);
+  return 0;
+}
+
+static void dbgc_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, dbgc_again, 0);
+}
+
+static int volumecheck_again(ev_source attribute((unused)) *ev_,
+			     const struct timeval attribute((unused)) *now,
+			     void attribute((unused)) *u) {
+  int l, r;
+  char lb[32], rb[32];
+
+  if(!mixer_control(&l, &r, 0)) {
+    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);
+    }
+  }
+  volumecheck_after(60);
+  return 0;
+}
+
+static void volumecheck_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, volumecheck_again, 0);
+}
+
+ int main(int argc, char **argv) {
+  int n, background = 1;
+  const char *pidfile = 0;
+  int initial_rescan = 1;
+
+  set_progname(argv);
+  mem_init(1);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  /* garbage-collect PCRE's memory */
+  pcre_malloc = xmalloc;
+  pcre_free = xfree;
+  while((n = getopt_long(argc, argv, "hVc:dfP:N", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'f': background = 0; break;
+    case 'P': pidfile = optarg; break;
+    case 'N': initial_rescan = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* go into background if necessary */
+  if(background)
+    daemonize(progname, LOG_DAEMON, pidfile);
+  info("process ID %lu", (unsigned long)getpid());
+  srand(time(0));			/* don't start the same every time */
+  /* create event loop */
+  ev = ev_new();
+  if(ev_child_setup(ev)) fatal(0, "ev_child_setup failed");
+  /* read config */
+  if(config_read())
+    fatal(0, "cannot read configuration");
+  /* Start the speaker process (as root! - so it can choose its nice value) */
+  speaker_setup(ev);
+  /* set server nice value _after_ starting the speaker, so that they
+   * are independently niceable */
+  xnice(config->nice_server);
+  /* change user */
+  become_mortal();
+  /* make sure we're not root, whatever the config says */
+  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+  /* open a lockfile - we only want one copy of the server to run at once. */
+  if(config->lock) {
+    const char *lockfile;
+    int lockfd;
+   struct flock lock;
+    
+    lockfile = config_get_file("lock");
+    if((lockfd = open(lockfile, O_RDWR|O_CREAT, 0600)) < 0)
+      fatal(errno, "error opening %s", lockfile);
+    cloexec(lockfd);
+    memset(&lock, 0, sizeof lock);
+    lock.l_type = F_WRLCK;
+    lock.l_whence = SEEK_SET;
+    if(fcntl(lockfd, F_SETLK, &lock) < 0)
+      fatal(errno, "error locking %s", lockfile);
+  }
+  /* initialize database environment */
+  trackdb_init(TRACKDB_NORMAL_RECOVER);
+  trackdb_master(ev);
+  /* install new config */
+  reconfigure(ev, 0);
+  /* re-read config if we receive a SIGHUP */
+  if(ev_signal(ev, SIGHUP, handle_sighup, 0)) fatal(0, "ev_signal failed");
+  /* exit on SIGINT/SIGTERM */
+  if(ev_signal(ev, SIGINT, handle_sigint, 0)) fatal(0, "ev_signal failed");
+  if(ev_signal(ev, SIGTERM, handle_sigterm, 0)) fatal(0, "ev_signal failed");
+  /* ignore SIGPIPE */
+  signal(SIGPIPE, SIG_IGN);
+  /* start a rescan straight away */
+  if(initial_rescan)
+    trackdb_rescan(ev);
+  rescan_after(86400);
+  /* periodically tidy up the database */
+  dbgc_after(60);
+  /* periodically check the volume */
+  volumecheck_after(60);
+  /* set initial state */
+  add_random_track();
+  play(ev);
+  /* enter the event loop */
+  n = ev_run(ev);
+  /* if we exit the event loop, something must have gone wrong */
+  fatal(errno, "ev_run returned %d", n);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:eaf909880c8fd3d1fef94ca8f12efe78 */
diff --git a/server/dump.c b/server/dump.c
new file mode 100644
index 0000000..39af755
--- /dev/null
+++ b/server/dump.c
@@ -0,0 +1,458 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "client.h"
+#include "sink.h"
+#include "mem.h"
+#include "defs.h"
+#include "printf.h"
+#include "kvp.h"
+#include "vector.h"
+#include "inputline.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "charset.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "dump", no_argument, 0, 'd' },
+  { "undump", no_argument, 0, 'u' },
+  { "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 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-dump [OPTIONS] --dump|--undump PATH\n"
+	  "  disorder-dump [OPTIONS] --recompute-aliases\n"
+	  "Options:\n"
+	  "  --help, -h               Display usage message\n"
+	  "  --version, -V            Display version number\n"
+	  "  --config PATH, -c PATH   Set configuration file\n"
+	  "  --dump, -d               Dump state to PATH\n"
+	  "  --undump, -u             Restore state from PATH\n"
+	  "  --recover, -r            Run database recovery\n"
+	  "  --recompute-aliases, -a  Recompute aliases\n"
+	  "  --remove-pathless, -P    Remove pathless tracks\n"
+	  "  --debug                  Debug mode\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-dump version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* dump prefs to FP, return nonzero on error */
+static void do_dump(FILE *fp, const char *tag,
+		    int tracksdb, int searchdb) {
+  DBC *cursor = 0;
+  DB_TXN *tid;
+  struct sink *s = sink_stdio(tag, fp);
+  int err;
+  DBT k, d;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(fseek(fp, 0, SEEK_SET) < 0)
+      fatal(errno, "error calling fseek");
+    if(fflush(fp) < 0)
+      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)
+      fatal(errno, "error writing to %s", tag);
+    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;
+
+    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; }
+    break;
+fail:
+    trackdb_closecursor(cursor);
+    cursor = 0;
+    info("aborting transaction and retrying dump");
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(fflush(fp) < 0) fatal(errno, "error writing to %s", tag);
+  /* caller might not be paranoid so we are paranoid on their behalf */
+  if(fsync(fileno(fp)) < 0) fatal(errno, "error syncing %s", tag);
+}
+
+/* delete all aliases prefs, return 0 or DB_LOCK_DEADLOCK */
+static int remove_aliases(DB_TXN *tid, int remove_pathless) {
+  DBC *cursor;
+  int err;
+  DBT k, d;
+  struct kvp *data;
+  int alias, pathless;
+
+  info("removing aliases");
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_FIRST)) == DB_LOCK_DEADLOCK) {
+    error(0, "cursor->c_get: %s", db_strerror(err));
+    goto done;
+  }
+  while(err == 0) {
+    data = kvp_urldecode(d.data, d.size);
+    alias = !!kvp_get(data, "_alias_for");
+    pathless = !kvp_get(data, "_path");
+    if(pathless && !remove_pathless)
+      info("no _path for %s", utf82mb(xstrndup(k.data, k.size)));
+    if(alias || (remove_pathless && pathless)) {
+      switch(err = cursor->c_del(cursor, 0)) {
+      case 0: break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "cursor->c_get: %s", db_strerror(err));
+        goto done;
+      default:
+        fatal(0, "cursor->c_del: %s", db_strerror(err));
+      }
+    }
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), DB_NEXT);
+  }
+  if(err == DB_LOCK_DEADLOCK) {
+    error(0, "cursor operation: %s", db_strerror(err));
+    goto done;
+  }
+  if(err != DB_NOTFOUND) fatal(0, "cursor->c_get: %s", db_strerror(err));
+  err = 0;
+done:
+  if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* truncate (i.e. empty) a database, return 0 or DB_LOCK_DEADLOCK */
+static int truncdb(DB_TXN *tid, DB *db) {
+  int err;
+  u_int32_t count;
+
+  switch(err = db->truncate(db, tid, &count, 0)) {
+  case 0: break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "db->truncate: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "db->truncate: %s", db_strerror(err));
+  }
+  return err;
+}
+
+/* read a DBT from FP, return 0 on success or -1 on input error */
+static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
+  char *s;
+  struct dynstr d;
+
+  if(inputline(tag, fp, &s, '\n')) return -1;
+  dynstr_init(&d);
+  if(urldecode(sink_dynstr(&d), s, strlen(s)))
+    fatal(0, "invalid URL-encoded data in %s", tag);
+  dbt->data = d.vec;
+  dbt->size = d.nvec;
+  return 0;
+}
+
+/* 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;
+
+  info("undumping");
+  if(fseek(fp, 0, SEEK_SET) < 0)
+    fatal(errno, "error calling fseek on %s", tag);
+  if((err = truncdb(tid, trackdb_prefsdb))) return err;
+  if((err = truncdb(tid, trackdb_searchdb))) return err;
+  c = getc(fp);
+  while(!ferror(fp) && !feof(fp)) {
+    switch(c) {
+    case 'V':
+      c = getc(fp);
+      if(c != '0')
+        fatal(0, "unknown version '%c'", c);
+      break;
+    case 'E':
+      return 0;
+    case 'P':
+      if(undump_dbt(fp, tag, prepare_data(&k))
+         || undump_dbt(fp, tag, prepare_data(&d)))
+        break;
+      switch(err = trackdb_prefsdb->put(trackdb_prefsdb, tid, &k, &d, 0)) {
+      case 0:
+        break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "error updating prefs.db: %s", db_strerror(err));
+        return err;
+      default:
+        fatal(0, "error updating prefs.db: %s", 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;
+    }
+    c = getc(fp);
+  }
+  if(ferror(fp))
+    fatal(errno, "error reading %s", tag);
+  else
+    fatal(0, "unexpected EOF reading %s", tag);
+  return 0;
+}
+
+/* recompute aliases and search database from prefs, return 0 or
+ * DB_LOCK_DEADLOCK */
+static int recompute_aliases(DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  int err;
+  struct kvp *data;
+  const char *path, *track;
+
+  info("recomputing aliases");
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_FIRST)) == DB_LOCK_DEADLOCK) goto done;
+  while(err == 0) {
+    data = kvp_urldecode(d.data, d.size);
+    track = xstrndup(k.data, k.size);
+    if(!kvp_get(data, "_alias_for")) {
+      if(!(path = kvp_get(data, "_path")))
+	error(0, "%s is not an alias but has no path", utf82mb(track));
+      else
+	if((err = trackdb_notice_tid(track, path, tid)) == DB_LOCK_DEADLOCK)
+	  goto done;
+    }
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                        DB_NEXT);
+  }
+  switch(err) {
+  case 0:
+    break;
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    break;
+  default:
+    fatal(0, "cursor->c_get: %s", db_strerror(err));
+  }
+done:
+  if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* restore prefs from FP */
+static void do_undump(FILE *fp, const char *tag, int remove_pathless) {
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(remove_aliases(tid, remove_pathless)
+       || undump_from_fp(tid, fp, tag)
+       || recompute_aliases(tid)) goto fail;
+    break;
+fail:
+    info("aborting transaction and retrying undump");
+    trackdb_abort_transaction(tid);
+  }
+  info("committing undump");
+  trackdb_commit_transaction(tid);
+}
+
+/* just recompute alisaes */
+static void do_recompute(int remove_pathless) {
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(remove_aliases(tid, remove_pathless)
+       || recompute_aliases(tid)) goto fail;
+    break;
+fail:
+    info("aborting transaction and retrying recomputation");
+    trackdb_abort_transaction(tid);
+  }
+  info("committing recomputed aliases");
+  trackdb_commit_transaction(tid);
+}
+
+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;
+  const char *path;
+  char *tmp;
+  FILE *fp;
+
+  mem_init(1);
+  while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    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;
+    case 'P': remove_pathless = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  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");
+    path = 0;
+  } else {
+    if(optind >= argc)
+      fatal(0, "missing dump file name");
+    if(optind + 1 < argc)
+      fatal(0, "specify only a dump file name");
+    path = argv[optind];
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  trackdb_init(recover);
+  trackdb_open();
+  if(dump) {
+    /* we write to a temporary file and rename into place */
+    byte_xasprintf(&tmp, "%s.%lx.tmp", path, (unsigned long)getpid());
+    if(!(fp = fopen(tmp, "w"))) fatal(errno, "error opening %s", tmp);
+    do_dump(fp, tmp, tracksdb, searchdb);
+    if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
+    if(rename(tmp, path) < 0)
+      fatal(errno, "error renaming %s to %s", tmp, path);
+  } else if(undump) {
+    /* the databases or logfiles might end up with wrong permissions
+     * if new ones are created */
+    if(getuid() == 0) info("you might need to chown database files");
+    if(!(fp = fopen(path, "r"))) fatal(errno, "error opening %s", path);
+    do_undump(fp, path, remove_pathless);
+    xfclose(fp);
+  } else if(recompute) {
+    do_recompute(remove_pathless);
+  }
+  trackdb_close();
+  trackdb_deinit();
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a446d6d9fcfbece4e3042e29c148a1cc */
diff --git a/server/play.c b/server/play.c
new file mode 100644
index 0000000..e70e116
--- /dev/null
+++ b/server/play.c
@@ -0,0 +1,725 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "log.h"
+#include "mem.h"
+#include "configuration.h"
+#include "queue.h"
+#include "trackdb.h"
+#include "play.h"
+#include "plugin.h"
+#include "wstat.h"
+#include "eventlog.h"
+#include "logfd.h"
+#include "syscalls.h"
+#include "speaker.h"
+#include "disorder.h"
+#include "signame.h"
+#include "hash.h"
+
+#define SPEAKER "disorder-speaker"
+
+struct queue_entry *playing;
+int paused;
+
+static void finished(ev_source *ev);
+
+static int speaker_fd = -1;
+static hash *player_pids;
+static int shutting_down;
+
+static void store_player_pid(const char *id, pid_t pid) {
+  if(!player_pids) player_pids = hash_new(sizeof (pid_t));
+  hash_add(player_pids, id, &pid, HASH_INSERT_OR_REPLACE);
+}
+
+static pid_t find_player_pid(const char *id) {
+  pid_t *pidp;
+
+  if(player_pids && (pidp = hash_find(player_pids, id))) return *pidp;
+  return -1;
+}
+
+static void forget_player_pid(const char *id) {
+  if(player_pids) hash_remove(player_pids, id);
+}
+
+/* called when speaker process terminates */
+static int speaker_terminated(ev_source attribute((unused)) *ev,
+			      pid_t attribute((unused)) pid,
+			      int attribute((unused)) status,
+			      const struct rusage attribute((unused)) *rusage,
+			      void attribute((unused)) *u) {
+  if(status)
+    error(0, "speaker subprocess terminated with status %s",
+	  wstat(status));
+  return 0;
+}
+
+/* called when speaker process has something to say */
+static int speaker_readable(ev_source *ev, int fd,
+			    void attribute((unused)) *u) {
+  struct speaker_message sm;
+  int ret = speaker_recv(fd, &sm, 0);
+  
+  if(ret < 0) return 0;			/* EAGAIN */
+  if(!ret) {				/* EOF */
+    ev_fd_cancel(ev, ev_read, fd);
+    return 0;
+  }
+  switch(sm.type) {
+  case SM_PAUSED:
+    /* track ID is paused, DATA seconds played */
+    D(("SM_PAUSED %s %ld", sm.id, sm.data));
+    playing->sofar = sm.data;
+    break;
+  case SM_FINISHED:
+    /* the playing track finished */
+    D(("SM_FINISHED %s", sm.id));
+    finished(ev);
+    break;
+  case SM_PLAYING:
+    /* track ID is playing, DATA seconds played */
+    D(("SM_PLAYING %s %ld", sm.id, sm.data));
+    playing->sofar = sm.data;
+    break;
+  default:
+    error(0, "unknown message type %d", sm.type);
+  }
+  return 0;
+}
+
+void speaker_setup(ev_source *ev) {
+  int sp[2], lfd;
+  pid_t pid;
+
+  if(socketpair(PF_UNIX, SOCK_DGRAM, 0, sp) < 0)
+    fatal(errno, "error calling socketpair");
+  if(!isatty(2))
+    lfd = logfd(ev, SPEAKER);
+  else
+    lfd = -1;
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    xdup2(sp[0], 0);
+    xdup2(sp[0], 1);
+    xclose(sp[0]);
+    xclose(sp[1]);
+    if(lfd != -1) {
+      xdup2(lfd, 2);
+      xclose(lfd);
+    }
+    signal(SIGPIPE, SIG_DFL);
+#if 0
+    execlp("valgrind", "valgrind", SPEAKER, "--config", configfile,
+	   debugging ? "--debug" : "--no-debug", (char *)0);
+#else
+    execlp(SPEAKER, SPEAKER, "--config", configfile,
+	   debugging ? "--debug" : "--no-debug", (char *)0);
+#endif
+    fatal(errno, "error invoking %s", SPEAKER);
+  }
+  ev_child(ev, pid, 0, speaker_terminated, 0);
+  speaker_fd = sp[1];
+  xclose(sp[0]);
+  cloexec(speaker_fd);
+  /* Don't need to make speaker_fd nonblocking because speaker_recv() uses
+   * MSG_DONTWAIT. */
+  ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0);
+}
+
+void speaker_reload(void) {
+  struct speaker_message sm;
+
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_RELOAD;
+  speaker_send(speaker_fd, &sm, -1);
+}
+
+/* timeout for play retry */
+static int play_again(ev_source *ev,
+		      const struct timeval attribute((unused)) *now,
+		      void attribute((unused)) *u) {
+  D(("play_again"));
+  play(ev);
+  return 0;
+}
+
+/* try calling play() again after @offset@ seconds */
+static void retry_play(ev_source *ev, int offset) {
+  struct timeval w;
+
+  D(("retry_play(%d)", offset));
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, play_again, 0);
+}
+
+/* Called when the currently playing track finishes playing.  This
+ * might be because the player finished or because the speaker process
+ * told us so. */
+static void finished(ev_source *ev) {
+  D(("finished playing=%p", (void *)playing));
+  if(!playing)
+    return;
+  if(playing->state != playing_scratched)
+    notify_not_scratched(playing->track, playing->submitter);
+  switch(playing->state) {
+  case playing_ok:
+    eventlog("completed", playing->track, (char *)0);
+    break;
+  case playing_scratched:
+    eventlog("scratched", playing->track, playing->scratched, (char *)0);
+    break;
+  case playing_failed:
+    eventlog("failed", playing->track, wstat(playing->wstat), (char *)0);
+    break;
+  default:
+    break;
+  }
+  queue_played(playing);
+  recent_write();
+  forget_player_pid(playing->id);
+  playing = 0;
+  if(ev) retry_play(ev, config->gap);
+}
+
+/* Called when a player terminates. */
+static int player_finished(ev_source *ev,
+			   pid_t pid,
+			   int status,
+			   const struct rusage attribute((unused)) *rusage,
+			   void *u) {
+  struct queue_entry *q = u;
+
+  D(("player_finished pid=%lu status=%#x",
+     (unsigned long)pid, (unsigned)status));
+  /* Record that this PID is dead.  If we killed the track we might know this
+   * already, but also it might have exited or crashed.  Either way we don't
+   * want to end up signalling it. */
+  if(pid == find_player_pid(q->id))
+    forget_player_pid(q->id);
+  switch(q->state) {
+  case playing_unplayed:
+  case playing_random:
+    /* If this was an SM_PREPARE track then either it failed or we deliberately
+     * stopped it because it was removed from the queue or moved down it.  So
+     * leave it state alone for future use. */
+    break;
+  default:
+    /* We actually started playing this track. */
+    if(status) {
+      if(q->state != playing_scratched)
+	q->state = playing_failed;
+    } else 
+      q->state = playing_ok;
+    break;
+  }
+  /* Regardless we always report and record the status and do cleanup for
+   * prefork calls. */
+  if(status)
+    error(0, "player for %s %s", q->track, wstat(status));
+  if(q->type & DISORDER_PLAYER_PREFORK)
+    play_cleanup(q->pl, q->data);
+  q->wstat = status;
+  /* If this actually was the current track, and does not use the speaker
+   * process, then it must have finished.  For raw-output players we will get a
+   * separate notification from the speaker process. */
+  if(q == playing
+     && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    finished(ev);
+  return 0;
+}
+
+/* Find the player for Q */
+static int find_player(const struct queue_entry *q) {
+  int n;
+  
+  for(n = 0; n < config->player.n; ++n)
+    if(fnmatch(config->player.s[n].s[0], q->track, 0) == 0)
+      break;
+  if(n >= config->player.n)
+    return -1;
+  else
+    return n;
+}
+
+/* Return values from start() */
+#define START_OK 0			/* Succeeded. */
+#define START_HARDFAIL 1		/* Track is broken. */
+#define START_SOFTFAIL 2	   /* Track OK, system (temporarily?) broken */
+
+/* Play or prepare Q */
+static int start(ev_source *ev,
+		 struct queue_entry *q,
+		 int smop) {
+  int n, lfd;
+  const char *p;
+  int sp[2];
+  struct speaker_message sm;
+  char buffer[64];
+  int optc;
+  ao_sample_format format;
+  ao_device *device;
+  int retries;
+  struct timespec ts;
+  const char *waitdevice = 0;
+  const char *const *optv;
+  pid_t pid;
+
+  memset(&sm, 0, sizeof sm);
+  if(find_player_pid(q->id) > 0) {
+    if(smop == SM_PREPARE) return START_OK;
+    /* We have already sent an SM_PREPARE for this track so we just need to
+     * tell the speaker process to start actually playing the queued up audio
+     * data */
+    strcpy(sm.id, q->id);
+    sm.type = SM_PLAY;
+    speaker_send(speaker_fd, &sm, -1);
+    return START_OK;
+  }
+  /* Find the player plugin. */
+  if((n = find_player(q)) < 0) return START_HARDFAIL;
+  if(!(q->pl = open_plugin(config->player.s[n].s[1], 0)))
+    return START_HARDFAIL;
+  q->type = play_get_type(q->pl);
+  /* Can't prepare non-raw tracks. */
+  if(smop == SM_PREPARE
+     && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return START_OK;
+  /* Call the prefork function. */
+  p = trackdb_rawpath(q->track);
+  if(q->type & DISORDER_PLAYER_PREFORK)
+    if(!(q->data = play_prefork(q->pl, p))) {
+      error(0, "prefork function for %s failed", q->track);
+      return START_HARDFAIL;
+    }
+  /* Use the second arg as the tag if available (it's probably a command name),
+   * otherwise the module name. */
+  lfd = logfd(ev, (config->player.s[n].s[2]
+		   ? config->player.s[n].s[2] : config->player.s[n].s[1]));
+  optc = config->player.s[n].n - 2;
+  optv = (void *)&config->player.s[n].s[2];
+  while(optc > 0 && optv[0][0] == '-') {
+    if(!strcmp(optv[0], "--")) {
+      ++optv;
+      --optc;
+      break;
+    }
+    if(!strcmp(optv[0], "--wait-for-device")
+       || !strncmp(optv[0], "--wait-for-device=", 18)) {
+      if((waitdevice = strchr(optv[0], '='))) {
+	++waitdevice;
+      } else
+	waitdevice = "";		/* use default */
+      ++optv;
+      --optc;
+    } else {
+      error(0, "unknown option %s", optv[0]);
+      return START_HARDFAIL;
+    }
+  }
+  switch(pid = fork()) {
+  case 0:			/* child */
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    signal(SIGPIPE, SIG_DFL);
+    xdup2(lfd, 1);
+    xdup2(lfd, 2);
+    xclose(lfd);			/* tidy up */
+    setpgid(0, 0);
+    if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+      /* Raw format players write down a pipe (in fact a socket) to
+       * the speaker process. */
+      sm.type = smop;
+      strcpy(sm.id, q->id);
+      if(socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0)
+	fatal(errno, "error calling socketpair");
+      xshutdown(sp[0], SHUT_WR);
+      xshutdown(sp[1], SHUT_RD);
+      speaker_send(speaker_fd, &sm, sp[0]);
+      /* Pass the file descriptor to the driver in an environment
+       * variable. */
+      snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", sp[1]);
+      if(putenv(buffer) < 0)
+	fatal(errno, "error calling putenv");
+      xclose(sp[0]);
+    }
+    if(waitdevice) {
+      ao_initialize();
+      if(*waitdevice) {
+	n = ao_driver_id(waitdevice);
+	if(n == -1)
+	  fatal(0, "invalid libao driver: %s", optv[0]);
+	} else
+	  n = ao_default_driver_id();
+      /* Make up a format. */
+      memset(&format, 0, sizeof format);
+      format.bits = 8;
+      format.rate = 44100;
+      format.channels = 1;
+      format.byte_format = AO_FMT_NATIVE;
+      retries = 20;
+      ts.tv_sec = 0;
+      ts.tv_nsec = 100000000;	/* 0.1s */
+      while((device = ao_open_live(n, &format, 0)) == 0 && retries-- > 0)
+	  nanosleep(&ts, 0);
+      if(device)
+	ao_close(device);
+    }
+    play_track(q->pl,
+	       optv, optc,
+	       p,
+	       q->track);
+    _exit(0);
+  case -1:			/* error */
+    error(errno, "error calling fork");
+    if(q->type & DISORDER_PLAYER_PREFORK)
+      play_cleanup(q->pl, q->data);	/* else would leak */
+    xclose(lfd);
+    return START_SOFTFAIL;
+  }
+  store_player_pid(q->id, pid);
+  xclose(lfd);
+  setpgid(pid, pid);
+  ev_child(ev, pid, 0, player_finished, q);
+  D(("player subprocess ID %lu", (unsigned long)pid));
+  return START_OK;
+}
+
+int prepare(ev_source *ev,
+	    struct queue_entry *q) {
+  int n;
+
+  /* Find the player plugin */
+  if(find_player_pid(q->id) > 0) return 0; /* Already going. */
+  if((n = find_player(q)) < 0) return -1; /* No player */
+  q->pl = open_plugin(config->player.s[n].s[1], 0); /* No player */
+  q->type = play_get_type(q->pl);
+  if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return 0;				/* Not a raw player */
+  return start(ev, q, SM_PREPARE);	/* Prepare it */
+}
+
+void abandon(ev_source attribute((unused)) *ev,
+	     struct queue_entry *q) {
+  struct speaker_message sm;
+  pid_t pid = find_player_pid(q->id);
+
+  if(pid < 0) return;			/* Not prepared. */
+  if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return;				/* Not a raw player. */
+  /* Terminate the player. */
+  kill(-pid, config->signal);
+  forget_player_pid(q->id);
+  /* Cancel the track. */
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_CANCEL;
+  strcpy(sm.id, q->id);
+  speaker_send(speaker_fd, &sm, -1);
+}
+
+int add_random_track(void) {
+  struct queue_entry *q;
+  const char *p;
+
+  /* If random play is not enabled then do nothing. */
+  if(shutting_down || !random_is_enabled())
+    return 0;
+  /* If there is already a random track, do nothing. */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if(q->state == playing_random)
+      return 0;
+  /* Try to pick a random track */
+  if(!(p = trackdb_random(16)))
+    return -1;
+  /* Add it to the end of the queue. */
+  q = queue_add(p, 0, WHERE_END);
+  q->state = playing_random;
+  /* Commit the queue */
+  queue_write();
+  D(("picked %p (%s) at random", (void *)q, q->track));
+  return 0;
+}
+
+/* try to play a track */
+void play(ev_source *ev) {
+  struct queue_entry *q;
+  int random_enabled = random_is_enabled();
+
+  D(("play playing=%p", (void *)playing));
+  if(shutting_down || playing || !playing_is_enabled()) return;
+  /* If the queue is empty then add a random track. */
+  if(qhead.next == &qhead) {
+    if(!random_enabled)
+      return;
+    if(add_random_track()) {
+      /* On error, try again in 10s. */
+      retry_play(ev, 10);
+      return;
+    }
+    /* Now there must be at least one track in the queue. */
+  }
+  q = qhead.next;
+  /* If random play is disabled but the track is a random one then don't play
+   * it.  play() will be called again when random play is re-enabled. */
+  if(!random_enabled && q->state == playing_random)
+    return;
+  D(("taken %p (%s) from queue", (void *)q, q->track));
+  /* Try to start playing. */
+  switch(start(ev, q, SM_PLAY)) {
+  case START_HARDFAIL:
+    if(q == qhead.next) {
+      queue_remove(q, 0);		/* Abandon this track. */
+      queue_played(q);
+      recent_write();
+    }
+    if(qhead.next == &qhead)
+      /* Queue is empty, wait a bit before trying something else (so we don't
+       * sit there looping madly in the presence of persistent problem).  Note
+       * that we might not reliably get a random track lookahead in this case,
+       * but if we get here then really there are bigger problems. */
+      retry_play(ev, 1);
+    else
+      /* More in queue, try again now. */
+      play(ev);
+    break;
+  case START_SOFTFAIL:
+    /* Try same track again in a bit. */
+    retry_play(ev, 10);
+    break;
+  case START_OK:
+    if(q == qhead.next) {
+      queue_remove(q, 0);
+      queue_write();
+    }
+    playing = q;
+    time(&playing->played);
+    playing->state = playing_started;
+    notify_play(playing->track, playing->submitter);
+    eventlog("playing", playing->track,
+	     playing->submitter ? playing->submitter : (const char *)0,
+	     (const char *)0);
+    /* Maybe add a random track. */
+    add_random_track();
+    /* If there is another track in the queue prepare it now.  This could
+     * potentially be a just-added random track. */
+    if(qhead.next != &qhead)
+      prepare(ev, qhead.next);
+    break;
+  }
+}
+
+int playing_is_enabled(void) {
+  const char *s = trackdb_get_global("playing");
+
+  return !s || !strcmp(s, "yes");
+}
+
+void enable_playing(const char *who, ev_source *ev) {
+  trackdb_set_global("playing", "yes", who);
+  /* Add a random track if necessary. */
+  add_random_track();
+  play(ev);
+}
+
+void disable_playing(const char *who) {
+  trackdb_set_global("playing", "no", who);
+}
+
+int random_is_enabled(void) {
+  const char *s = trackdb_get_global("random-play");
+
+  return !s || !strcmp(s, "yes");
+}
+
+void enable_random(const char *who, ev_source *ev) {
+  trackdb_set_global("random-play", "yes", who);
+  add_random_track();
+  play(ev);
+}
+
+void disable_random(const char *who) {
+  trackdb_set_global("random-play", "no", who);
+}
+
+void scratch(const char *who, const char *id) {
+  struct queue_entry *q;
+  struct speaker_message sm;
+  pid_t pid;
+
+  D(("scratch playing=%p state=%d id=%s playing->id=%s",
+     (void *)playing,
+     playing ? playing->state : 0,
+     id ? id : "(none)",
+     playing ? playing->id : "(none)"));
+  if(playing
+     && (playing->state == playing_started
+	 || playing->state == playing_paused)
+     && (!id
+	 || !strcmp(id, playing->id))) {
+    playing->state = playing_scratched;
+    playing->scratched = who ? xstrdup(who) : 0;
+    if((pid = find_player_pid(playing->id)) > 0) {
+      D(("kill -%d %lu", config->signal, (unsigned long)pid));
+      kill(-pid, config->signal);
+      forget_player_pid(playing->id);
+    } else
+      error(0, "could not find PID for %s", playing->id);
+    if((playing->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+      memset(&sm, 0, sizeof sm);
+      sm.type = SM_CANCEL;
+      strcpy(sm.id, playing->id);
+      speaker_send(speaker_fd, &sm, -1);
+      D(("sending SM_CANCEL for %s", playing->id));
+    }
+    /* put a scratch track onto the front of the queue (but don't
+     * bother if playing is disabled) */
+    if(playing_is_enabled() && config->scratch.n) {
+      int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+      q = queue_add(config->scratch.s[r], who, WHERE_START);
+      q->state = playing_isscratch;
+    }
+    notify_scratch(playing->track, playing->submitter, who,
+		   time(0) - playing->played);
+  }
+}
+
+void quitting(ev_source *ev) {
+  struct queue_entry *q;
+  pid_t pid;
+
+  /* Don't start anything new */
+  shutting_down = 1;
+  /* Shut down the current player */
+  if(playing) {
+    if((pid = find_player_pid(playing->id)) > 0) {
+      kill(-pid, config->signal);
+      forget_player_pid(playing->id);
+    } else
+      error(0, "could not find PID for %s", playing->id);
+    playing->state = playing_quitting;
+    finished(0);
+  }
+  /* Zap any other players */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if((pid = find_player_pid(q->id)) > 0) {
+      D(("kill -%d %lu", config->signal, (unsigned long)pid));
+      kill(-pid, config->signal);
+      forget_player_pid(q->id);
+    } else
+      error(0, "could not find PID for %s", q->id);
+  /* Don't need the speaker any more */
+  ev_fd_cancel(ev, ev_read, speaker_fd);
+  xclose(speaker_fd);
+}
+
+int pause_playing(const char *who) {
+  struct speaker_message sm;
+  long played;
+  
+  /* Can't pause if already paused or if nothing playing. */
+  if(!playing || paused) return 0;
+  switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+  case DISORDER_PLAYER_STANDALONE:
+    if(!(playing->type & DISORDER_PLAYER_PAUSES)) {
+    default:
+      error(0,  "cannot pause because player is not powerful enough");
+      return -1;
+    }
+    if(play_pause(playing->pl, &played, playing->data)) {
+      error(0, "player indicates it cannot pause");
+      return -1;
+    }
+    time(&playing->lastpaused);
+    playing->uptopause = played;
+    playing->lastresumed = 0;
+    break;
+  case DISORDER_PLAYER_RAW:
+    memset(&sm, 0, sizeof sm);
+    sm.type = SM_PAUSE;
+    speaker_send(speaker_fd, &sm, -1);
+    break;
+  }
+  if(who) info("paused by %s", who);
+  notify_pause(playing->track, who);
+  paused = 1;
+  if(playing->state == playing_started)
+    playing->state = playing_paused;
+  eventlog("state", "pause", (char *)0);
+  return 0;
+}
+
+void resume_playing(const char *who) {
+  struct speaker_message sm;
+
+  if(!paused) return;
+  paused = 0;
+  if(!playing) return;
+  switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+  case DISORDER_PLAYER_STANDALONE:
+    if(!playing->type & DISORDER_PLAYER_PAUSES) {
+    default:
+      /* Shouldn't happen */
+      return;
+    }
+    play_resume(playing->pl, playing->data);
+    time(&playing->lastresumed);
+    break;
+  case DISORDER_PLAYER_RAW:
+    memset(&sm, 0, sizeof sm);
+    sm.type = SM_RESUME;
+    speaker_send(speaker_fd, &sm, -1);
+    break;
+  }
+  if(who) info("resumed by %s", who);
+  notify_resume(playing->track, who);
+  if(playing->state == playing_paused)
+    playing->state = playing_started;
+  eventlog("state", "resume", (char *)0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:17f4e83cce4cbaa60a122475379e63f1 */
diff --git a/server/play.h b/server/play.h
new file mode 100644
index 0000000..396ff56
--- /dev/null
+++ b/server/play.h
@@ -0,0 +1,91 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef PLAY_H
+#define PLAY_H
+
+extern struct queue_entry *playing;	/* playing track or 0 */
+extern int paused;			/* non-0 if paused */
+
+void play(ev_source *ev);
+/* try to play something, if playing is enabled and nothing is playing
+ * already */
+
+int playing_is_enabled(void);
+/* return true iff playing is enabled */
+
+void enable_playing(const char *who, ev_source *ev);
+/* enable playing */
+
+void disable_playing(const char *who);
+/* disable playing. */
+
+int random_is_enabled(void);
+/* return true iff random play is enabled */
+
+void enable_random(const char *who, ev_source *ev);
+/* enable random play */
+
+void disable_random(const char *who);
+/* disable random play */
+
+void scratch(const char *who, const char *id);
+/* scratch the playing track.  @who@ identifies the scratcher. @id@ is
+ * the ID or a null pointer. */
+
+void quitting(ev_source *ev);
+/* called to terminate current track and shut down speaker process
+ * when quitting */
+
+void speaker_setup(ev_source *ev);
+/* set up the speaker subprocess */
+
+void speaker_reload(void);
+/* Tell the speaker process to reload its configuration. */
+
+int pause_playing(const char *who);
+/* Pause the current track.  Return 0 on success, -1 on error.  WHO
+ * can be 0. */
+
+void resume_playing(const char *who);
+/* Resume after a pause.  WHO can be 0. */
+
+int prepare(ev_source *ev,
+	    struct queue_entry *q);
+/* Prepare to play Q */
+
+void abandon(ev_source *ev,
+	     struct queue_entry *q);
+/* Abandon a possibly-prepared track. */
+
+int add_random_track(void);
+/* If random play is enabled then try to add a track to the queue.  On success
+ * (including deliberartely doing nothing) return 0.  On error return -1. */
+
+#endif /* PLAY_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:2bcb00c5b004ce3e91785adb8893e8de */
diff --git a/server/rescan.c b/server/rescan.c
new file mode 100644
index 0000000..0c1e6de
--- /dev/null
+++ b/server/rescan.c
@@ -0,0 +1,331 @@
+/*
+ * This file is part of DisOrder 
+ * Copyright (C) 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "plugin.h"
+#include "inputline.h"
+#include "charset.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "printf.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static DB_TXN *global_tid;
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-rescan [OPTIONS] [PATH...]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Rescanner for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-rescan version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+static volatile sig_atomic_t signalled;
+
+static void signal_handler(int sig) {
+  if(sig == 0) _exit(-1);               /* "Cannot happen" */
+  signalled = sig;
+}
+
+static int aborted(void) {
+  return signalled || getppid() == 1;
+}
+
+/* Exit if our parent has gone away or we have been told to stop. */
+static void checkabort(void) {
+  if(getppid() == 1) {
+    info("parent has terminated");
+    trackdb_abort_transaction(global_tid);
+    exit(0);
+  }
+  if(signalled) {
+    info("received signal %d", signalled);
+    trackdb_abort_transaction(global_tid);
+    exit(0);
+  }
+}
+
+/* rescan a collection */
+static void rescan_collection(const struct collection *c) {
+  pid_t pid, r;
+  int p[2], n, w;
+  FILE *fp = 0;
+  char *path, *track;
+  long ntracks = 0, nnew = 0;
+  
+  checkabort();
+  info("rescanning %s with %s", c->root, c->module);
+  /* plugin runs in a subprocess */
+  xpipe(p);
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    xclose(p[0]);
+    xdup2(p[1], 1);
+    xclose(p[1]);
+    scan(c->module, c->root);
+    if(fflush(stdout) < 0)
+      fatal(errno, "error writing to scanner pipe");
+    _exit(0);
+  }
+  xclose(p[1]);
+  if(!(fp = fdopen(p[0], "r")))
+    fatal(errno, "error calling fdopen");
+  /* read tracks from the plugin */
+  while(!inputline("rescanner", fp, &path, 0)) {
+    checkabort();
+    /* actually we can cope relatively well within the server, but they'll go
+     * wrong in track listings */
+    if(strchr(path, '\n')) {
+      error(0, "cannot cope with tracks with newlines in the name");
+      continue;
+    }
+    if(!(track = any2utf8(c->encoding, path))) {
+      error(0, "cannot convert track path to UTF-8: %s", path);
+      continue;
+    }
+    D(("track %s", track));
+    /* only tracks with a known player are admitted */
+    for(n = 0; (n < config->player.n
+		&& fnmatch(config->player.s[n].s[0], track, 0) != 0); ++n)
+      ;
+    if(n < config->player.n) {
+      nnew += !!trackdb_notice(track, path);
+      ++ntracks;
+    }
+  }
+  /* tidy up */
+  if(ferror(fp)) {
+    error(errno, "error reading from scanner pipe");
+    goto done;
+  }
+  xfclose(fp);
+  fp = 0;
+  while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+    ;
+  if(r < 0) fatal(errno, "error calling waitpid");
+  pid = 0;
+  if(w) {
+    error(0, "scanner subprocess: %s", wstat(w));
+    goto done;
+  }
+  info("rescanned %s, %ld tracks, %ld new", c->root, ntracks, nnew);
+done:
+  if(fp)
+    xfclose(fp);
+  if(pid)
+    while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+      ;
+}
+
+struct recheck_state {
+  const struct collection *c;
+  long nobsolete, nlength;
+};
+
+/* called for each non-alias track */
+static int recheck_callback(const char *track,
+                            struct kvp *data,
+                            void *u,
+                            DB_TXN *tid) {
+  struct recheck_state *cs = u;
+  const struct collection *c = cs->c;
+  const char *path = kvp_get(data, "_path");
+  char buffer[20];
+  int err;
+  long n;
+
+  if(aborted()) return EINTR;
+  D(("rechecking %s", track));
+  /* see if the track has evaporated */
+  if(check(c->module, c->root, path) == 0) {
+    D(("obsoleting %s", track));
+    if((err = trackdb_obsolete(track, tid))) return err;
+    ++cs->nobsolete;
+    return 0;
+  }
+  /* make sure we know the length */
+  if(!kvp_get(data, "_length")) {
+    D(("recalculating length of %s", track));
+    n = tracklength(track, path);
+    if(n > 0) {
+      byte_snprintf(buffer, sizeof buffer, "%ld", n);
+      kvp_set(&data, "_length", buffer);
+      if((err = trackdb_putdata(trackdb_tracksdb, track, data, tid, 0)))
+        return err;
+      ++cs->nlength;
+    }
+  }
+  return 0;
+}
+
+/* recheck a collection */
+static void recheck_collection(const struct collection *c) {
+  struct recheck_state cs;
+
+  info("rechecking %s", c->root);
+  for(;;) {
+    checkabort();
+    global_tid = trackdb_begin_transaction();
+    memset(&cs, 0, sizeof cs);
+    cs.c = c;
+    if(trackdb_scan(c->root, recheck_callback, &cs, global_tid)) goto fail;
+    break;
+  fail:
+    /* Maybe we need to shut down */
+    checkabort();
+    /* Abort the transaction and try again in a bit. */
+    trackdb_abort_transaction(global_tid);
+    global_tid = 0;
+    /* Let anything else that is going on get out of the way. */
+    sleep(10);
+    checkabort();
+    info("resuming recheck of %s", c->root);
+  }
+  trackdb_commit_transaction(global_tid);
+  global_tid = 0;
+  info("rechecked %s, %ld obsoleted, %ld lengths calculated",
+       c->root, cs.nobsolete, cs.nlength);
+}
+
+/* rescan/recheck a collection by name */
+static void do_directory(const char *s,
+			 void (*fn)(const struct collection *c)) {
+  int n;
+  
+  for(n = 0; (n < config->collection.n
+	      && strcmp(config->collection.s[n].root, s)); ++n)
+    ;
+  if(n < config->collection.n)
+    fn(&config->collection.s[n]);
+  else
+    error(0, "no collection has root '%s'", s);
+}
+
+/* rescan/recheck all collections */
+static void do_all(void (*fn)(const struct collection *c)) {
+  int n;
+
+  for(n = 0; n < config->collection.n; ++n)
+    fn(&config->collection.s[n]);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  struct sigaction sa;
+  
+  set_progname(argv);
+  mem_init(1);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* If stderr is a TTY then log there, otherwise to syslog. */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  xnice(config->nice_rescan);
+  sa.sa_handler = signal_handler;
+  sa.sa_flags = SA_RESTART;
+  sigemptyset(&sa.sa_mask);
+  xsigaction(SIGTERM, &sa, 0);
+  xsigaction(SIGINT, &sa, 0);
+  info("started");
+  trackdb_init(0);
+  trackdb_open();
+  if(optind == argc) {
+    do_all(rescan_collection);
+    do_all(recheck_collection);
+  }
+  else {
+    for(n = optind; n < argc; ++n)
+      do_directory(argv[n], rescan_collection);
+    for(n = optind; n < argc; ++n)
+      do_directory(argv[n], recheck_collection);
+  }
+  trackdb_close();
+  trackdb_deinit();
+  info("completed");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:yBYnk5wX3mDtVI1MI3LlWg */
diff --git a/server/server.c b/server/server.c
new file mode 100644
index 0000000..89ff5ab
--- /dev/null
+++ b/server/server.c
@@ -0,0 +1,1105 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "server.h"
+#include "syscalls.h"
+#include "queue.h"
+#include "play.h"
+#include "log.h"
+#include "mem.h"
+#include "state.h"
+#include "charset.h"
+#include "split.h"
+#include "configuration.h"
+#include "hex.h"
+#include "trackdb.h"
+#include "table.h"
+#include "kvp.h"
+#include "mixer.h"
+#include "sink.h"
+#include "authhash.h"
+#include "plugin.h"
+#include "printf.h"
+#include "trackname.h"
+#include "eventlog.h"
+#include "defs.h"
+#include "cache.h"
+
+#ifndef NONCE_SIZE
+# define NONCE_SIZE 16
+#endif
+
+int volume_left, volume_right;		/* last known volume */
+
+struct listener {
+  const char *name;
+  int pf;
+};
+
+struct conn {
+  ev_reader *r;
+  ev_writer *w;
+  int fd;
+  unsigned tag;
+  char *who;
+  ev_source *ev;
+  unsigned char nonce[NONCE_SIZE];
+  ev_reader_callback *reader;
+  struct eventlog_output *lo;
+  const struct listener *l;
+};
+
+static int reader_callback(ev_source *ev,
+			   ev_reader *reader,
+			   int fd,
+			   void *ptr,
+			   size_t bytes,
+			   int eof,
+			   void *u);
+
+static const char *noyes[] = { "no", "yes" };
+
+static int writer_error(ev_source attribute((unused)) *ev,
+			int fd,
+			int errno_value,
+			void *u) {
+  struct conn *c = u;
+
+  D(("server writer_error %d %d", fd, errno_value));
+  if(errno_value == 0) {
+    /* writer is done */
+    c->w = 0;
+    if(c->r == 0) {
+      D(("server writer_error closes %d", fd));
+      xclose(fd);		/* reader is done too, close */
+    } else {
+      D(("server writer_error shutdown %d SHUT_WR", fd));
+      xshutdown(fd, SHUT_WR);	/* reader is not done yet */
+    }
+  } else {
+    if(errno_value != EPIPE)
+      error(errno_value, "S%x write error on socket", c->tag);
+    if(c->r)
+      ev_reader_cancel(c->r);
+    xclose(fd);
+  }
+  return 0;
+}
+
+static int reader_error(ev_source attribute((unused)) *ev,
+			int fd,
+			int errno_value,
+			void *u) {
+  struct conn *c = u;
+
+  D(("server reader_error %d %d", fd, errno_value));
+  error(errno, "S%x read error on socket", c->tag);
+  ev_writer_cancel(c->w);
+  xclose(fd);
+  return 0;
+}
+
+/* return true if we are talking to a trusted user */
+static int trusted(struct conn *c) {
+  int n;
+  
+  for(n = 0; (n < config->trust.n
+	      && strcmp(config->trust.s[n], c->who)); ++n)
+    ;
+  return n < config->trust.n;
+}
+
+static int c_disable(struct conn *c, char **vec, int nvec) {
+  if(nvec == 0)
+    disable_playing(c->who);
+  else if(nvec == 1 && !strcmp(vec[0], "now"))
+    disable_playing(c->who);
+  else {
+    sink_writes(ev_writer_sink(c->w), "550 invalid argument\n");
+    return 1;			/* completed */
+  }
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_enable(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  enable_playing(c->who, c->ev);
+  /* Enable implicitly unpauses if there is nothing playing */
+  if(paused && !playing) resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_enabled(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[playing_is_enabled()]);
+  return 1;			/* completed */
+}
+
+static int c_play(struct conn *c, char **vec,
+		  int attribute((unused)) nvec) {
+  const char *track;
+  struct queue_entry *q;
+  
+  if(!trackdb_exists(vec[0])) {
+    sink_writes(ev_writer_sink(c->w), "550 track is not in database\n");
+    return 1;
+  }
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  q = queue_add(track, c->who, WHERE_BEFORE_RANDOM);
+  queue_write();
+  /* If we added the first track, and something is playing, then prepare the
+   * new track.  If nothing is playing then we don't bother as it wouldn't gain
+   * anything. */
+  if(q == qhead.next && playing)
+    prepare(c->ev, q);
+  sink_writes(ev_writer_sink(c->w), "250 queued\n");
+  /* If the queue was empty but we are for some reason paused then
+   * unpause. */
+  if(!playing) resume_playing(0);
+  play(c->ev);
+  return 1;			/* completed */
+}
+
+static int c_remove(struct conn *c, char **vec,
+		    int attribute((unused)) nvec) {
+  struct queue_entry *q;
+
+  if(!(q = queue_find(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+    return 1;
+  }
+  if(config->restrictions & RESTRICT_REMOVE) {
+    /* can only remove tracks that you submitted */
+    if(!q->submitter || strcmp(q->submitter, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+      return 1;
+    }
+  }
+  queue_remove(q, c->who);
+  /* De-prepare the track. */
+  abandon(c->ev, q);
+  /* If we removed the random track then add another one. */
+  if(q->state == playing_random)
+    add_random_track();
+  /* Prepare whatever the next head track is. */
+  if(qhead.next != &qhead)
+    prepare(c->ev, qhead.next);
+  queue_write();
+  sink_writes(ev_writer_sink(c->w), "250 removed\n");
+  return 1;			/* completed */
+}
+
+static int c_scratch(struct conn *c,
+		     char **vec,
+		     int nvec) {
+  if(!playing) {
+    sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+    return 1;			/* completed */
+  }
+  if(config->restrictions & RESTRICT_SCRATCH) {
+    /* can only scratch tracks you submitted and randomly selected ones */
+    if(playing->submitter && strcmp(playing->submitter, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+      return 1;
+    }
+  }
+  scratch(c->who, nvec == 1 ? vec[0] : 0);
+  /* If you scratch an unpaused track then it is automatically unpaused */
+  resume_playing(0);
+  sink_writes(ev_writer_sink(c->w), "250 scratched\n");
+  return 1;			/* completed */
+}
+
+static int c_pause(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  if(!playing) {
+    sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+    return 1;			/* completed */
+  }
+  if(paused) {
+    sink_writes(ev_writer_sink(c->w), "250 already paused\n");
+    return 1;			/* completed */
+  }
+  if(pause_playing(c->who) < 0)
+    sink_writes(ev_writer_sink(c->w), "550 cannot pause this track\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 paused\n");
+  return 1;
+}
+
+static int c_resume(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  if(!paused) {
+    sink_writes(ev_writer_sink(c->w), "250 not paused\n");
+    return 1;			/* completed */
+  }
+  resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 paused\n");
+  return 1;
+}
+
+static int c_shutdown(struct conn *c,
+		      char attribute((unused)) **vec,
+		      int attribute((unused)) nvec) {
+  info("S%x shut down by %s", c->tag, c->who);
+  sink_writes(ev_writer_sink(c->w), "250 shutting down\n");
+  ev_writer_flush(c->w);
+  quit(c->ev);
+}
+
+static int c_reconfigure(struct conn *c,
+			 char attribute((unused)) **vec,
+			 int attribute((unused)) nvec) {
+  info("S%x reconfigure by %s", c->tag, c->who);
+  if(reconfigure(c->ev, 1))
+    sink_writes(ev_writer_sink(c->w), "550 error reading new config\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 installed new config\n");
+  return 1;				/* completed */
+}
+
+static int c_rescan(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  info("S%x rescan by %s", c->tag, c->who);
+  trackdb_rescan(c->ev);
+  sink_writes(ev_writer_sink(c->w), "250 initiated rescan\n");
+  return 1;				/* completed */
+}
+
+static int c_version(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  /* VERSION had better only use the basic character set */
+  sink_printf(ev_writer_sink(c->w), "251 %s\n", disorder_version_string);
+  return 1;			/* completed */
+}
+
+static int c_playing(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  if(playing) {
+    queue_fix_sofar(playing);
+    playing->expected = 0;
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", queue_marshall(playing));
+  } else
+    sink_printf(ev_writer_sink(c->w), "259 nothing playing\n");
+  return 1;				/* completed */
+}
+
+static int c_become(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  c->who = vec[0];
+  sink_writes(ev_writer_sink(c->w), "230 OK\n");
+  return 1;
+}
+
+static int c_user(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  int n;
+  const char *res;
+  union {
+    struct sockaddr sa;
+    struct sockaddr_in in;
+    struct sockaddr_in6 in6;
+  } u;
+  socklen_t l;
+  char host[1024];
+
+  if(c->who) {
+    sink_writes(ev_writer_sink(c->w), "530 already authenticated\n");
+    return 1;
+  }
+  /* get connection data */
+  l = sizeof u;
+  if(getpeername(c->fd, &u.sa, &l) < 0) {
+    error(errno, "S%x error calling getpeername", c->tag);
+    sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+    return 1;
+  }
+  if(c->l->pf != PF_UNIX) {
+    if((n = getnameinfo(&u.sa, l,
+			host, sizeof host, 0, 0, NI_NUMERICHOST))) {
+      error(0, "S%x error calling getnameinfo: %s", c->tag, gai_strerror(n));
+      sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+      return 1;
+    }
+  }
+  /* find the user */
+  for(n = 0; n < config->allow.n
+	&& strcmp(config->allow.s[n].s[0], vec[0]); ++n)
+    ;
+  /* if it's a real user check whether the response is right */
+  if(n < config->allow.n) {
+    res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1]);
+    if(res && !strcmp(res, vec[1])) {
+      c->who = vec[0];
+      /* currently we only bother logging remote connections */
+      if(c->l->pf != PF_UNIX)
+	info("S%x %s connected from %s", c->tag, vec[0], host);
+      sink_writes(ev_writer_sink(c->w), "230 OK\n");
+      return 1;
+    }
+  }
+  /* oops, response was wrong */
+  if(c->l->pf != PF_UNIX)
+    info("S%x authentication failure for %s from %s", c->tag, vec[0], host);
+  else
+    info("S%x authentication failure for %s", c->tag, vec[0]);
+  sink_writes(ev_writer_sink(c->w), "530 authentication failed\n");
+  return 1;
+}
+
+static int c_recent(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  const struct queue_entry *q;
+
+  sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+  for(q = phead.next; q != &phead; q = q->next)
+    sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+}
+
+static int c_queue(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  struct queue_entry *q;
+  time_t when = 0;
+  const char *l;
+  long length;
+
+  sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+  if(playing_is_enabled() && !paused) {
+    if(playing) {
+      queue_fix_sofar(playing);
+      if((l = trackdb_get(playing->track, "_length"))
+	 && (length = atol(l))) {
+	time(&when);
+	when += length - playing->sofar + config->gap;
+      }
+    } else
+      /* Nothing is playing but playing is enabled, so whatever is
+       * first in the queue can be expected to start immediately. */
+      time(&when);
+  }
+  for(q = qhead.next; q != &qhead; q = q->next) {
+    /* fill in estimated start time */
+    q->expected = when;
+    sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+    /* update for next track */
+    if(when) {
+      if((l = trackdb_get(q->track, "_length"))
+	 && (length = atol(l)))
+	when += length + config->gap;
+      else
+	when = 0;
+    }
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+}
+
+static int output_list(struct conn *c, char **vec) {
+  while(*vec)
+    sink_printf(ev_writer_sink(c->w), "%s\n", *vec++);
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int files_dirs(struct conn *c,
+		      char **vec,
+		      int nvec,
+		      enum trackdb_listable what) {
+  const char *dir, *re, *errstr;
+  int erroffset;
+  pcre *rec;
+  char **fvec, *key;
+  
+  switch(nvec) {
+  case 0: dir = 0; re = 0; break;
+  case 1: dir = vec[0]; re = 0; break;
+  case 2: dir = vec[0]; re = vec[1]; break;
+  default: abort();
+  }
+  /* A bit of a bodge to make sure the args don't trample on cache keys */
+  if(dir && strchr(dir, '\n')) {
+    sink_writes(ev_writer_sink(c->w), "550 invalid directory name\n");
+    return 1;
+  }
+  if(re && strchr(re, '\n')) {
+    sink_writes(ev_writer_sink(c->w), "550 invalid regexp\n");
+    return 1;
+  }
+  /* We bother eliminating "" because the web interface is relatively
+   * likely to send it */
+  if(re && *re) {
+    byte_xasprintf(&key, "%d\n%s\n%s", (int)what, dir ? dir : "", re);
+    fvec = (char **)cache_get(&cache_files_type, key);
+    if(fvec) {
+      /* Got a cache hit, don't store the answer in the cache */
+      key = 0;
+      ++cache_files_hits;
+      rec = 0;				/* quieten compiler */
+    } else {
+      /* Cache miss, we'll do the lookup and key != 0 so we'll store the answer
+       * in the cache. */
+      if(!(rec = pcre_compile(re, PCRE_CASELESS|PCRE_UTF8,
+			      &errstr, &erroffset, 0))) {
+	sink_printf(ev_writer_sink(c->w), "550 Error compiling regexp: %s\n",
+		    errstr);
+	return 1;
+      }
+      /* It only counts as a miss if the regexp was valid. */
+      ++cache_files_misses;
+    }
+  } else {
+    /* No regexp, don't bother caching the result */
+    rec = 0;
+    key = 0;
+    fvec = 0;
+  }
+  if(!fvec) {
+    /* No cache hit (either because a miss, or because we did not look) so do
+     * the lookup */
+    if(dir && *dir)
+      fvec = trackdb_list(dir, 0, what, rec);
+    else
+      fvec = trackdb_list(0, 0, what, rec);
+  }
+  if(key)
+    /* Put the answer in the cache */
+    cache_put(&cache_files_type, key, fvec);
+  sink_writes(ev_writer_sink(c->w), "253 Listing follow\n");
+  return output_list(c, fvec);
+}
+
+static int c_files(struct conn *c,
+		  char **vec,
+		  int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_files);
+}
+
+static int c_dirs(struct conn *c,
+		  char **vec,
+		  int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_directories);
+}
+
+static int c_allfiles(struct conn *c,
+		      char **vec,
+		      int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_directories|trackdb_files);
+}
+
+static int c_get(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  const char *v;
+
+  if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_length(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  const char *track, *v;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if((v = trackdb_get(track, "_length")))
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_set(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  if(vec[1][0] != '_' && !trackdb_set(vec[0], vec[1], vec[2]))
+    sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_prefs(struct conn *c,
+		   char **vec,
+		   int attribute((unused)) nvec) {
+  struct kvp *k;
+
+  k = trackdb_get_all(vec[0]);
+  sink_writes(ev_writer_sink(c->w), "253 prefs follow\n");
+  for(; k; k = k->next)
+    if(k->name[0] != '_')		/* omit internal values */
+      sink_printf(ev_writer_sink(c->w),
+		  " %s %s\n", quoteutf8(k->name), quoteutf8(k->value));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int c_exists(struct conn *c,
+		    char **vec,
+		    int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[trackdb_exists(vec[0])]);
+  return 1;
+}
+
+static void search_parse_error(const char *msg, void *u) {
+  *(const char **)u = msg;
+}
+
+static int c_search(struct conn *c,
+			  char **vec,
+			  int attribute((unused)) nvec) {
+  char **terms, **results;
+  int nterms, nresults, n;
+  const char *e = "unknown error";
+
+  /* This is a bit of a bodge.  Initially it's there to make the eclient
+   * interface a bit more convenient to add searching to, but it has the more
+   * compelling advantage that if everything uses it, then interpretation of
+   * user-supplied search strings will be the same everywhere. */
+  if(!(terms = split(vec[0], &nterms, SPLIT_QUOTES, search_parse_error, &e))) {
+    sink_printf(ev_writer_sink(c->w), "550 %s\n", e);
+  } else {
+    results = trackdb_search(terms, nterms, &nresults);
+    sink_printf(ev_writer_sink(c->w), "253 %d matches\n", nresults);
+    for(n = 0; n < nresults; ++n)
+      sink_printf(ev_writer_sink(c->w), "%s\n", results[n]);
+    sink_writes(ev_writer_sink(c->w), ".\n");
+  }
+  return 1;
+}
+
+static int c_random_enable(struct conn *c,
+			   char attribute((unused)) **vec,
+			   int attribute((unused)) nvec) {
+  enable_random(c->who, c->ev);
+  /* Enable implicitly unpauses if there is nothing playing */
+  if(paused && !playing) resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_random_disable(struct conn *c,
+			    char attribute((unused)) **vec,
+			    int attribute((unused)) nvec) {
+  disable_random(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_random_enabled(struct conn *c,
+			    char attribute((unused)) **vec,
+			    int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[random_is_enabled()]);
+  return 1;			/* completed */
+}
+
+static int c_stats(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  char **v;
+  int nv, n;
+
+  v = trackdb_stats(&nv);
+  sink_printf(ev_writer_sink(c->w), "253 stats\n");
+  for(n = 0; n < nv; ++n) {
+    if(v[n][0] == '.')
+      sink_writes(ev_writer_sink(c->w), ".");
+    sink_printf(ev_writer_sink(c->w), "%s\n", v[n]);
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int c_volume(struct conn *c,
+		    char **vec,
+		    int nvec) {
+  int l, r, set;
+  char lb[32], rb[32];
+
+  switch(nvec) {
+  case 0:
+    set = 0;
+    break;
+  case 1:
+    l = r = atoi(vec[0]);
+    set = 1;
+    break;
+  case 2:
+    l = atoi(vec[0]);
+    r = atoi(vec[1]);
+    set = 1;
+    break;
+  default:
+    abort();
+  }
+  if(mixer_control(&l, &r, set))
+    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;
+}
+
+/* we are logging, and some data is available to read */
+static int logging_reader_callback(ev_source *ev,
+				   ev_reader *reader,
+				   int fd,
+				   void *ptr,
+				   size_t bytes,
+				   int eof,
+				   void *u) {
+  struct conn *c = u;
+
+  /* don't log to this conn any more */
+  eventlog_remove(c->lo);
+  /* terminate the log output */
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  /* restore the reader callback */
+  c->reader = reader_callback;
+  /* ...and exit via it */
+  return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+static void logclient(const char *msg, void *user) {
+  struct conn *c = user;
+
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n",
+	      (uintmax_t)time(0), msg);
+}
+
+static int c_log(struct conn *c,
+		 char attribute((unused)) **vec,
+		 int attribute((unused)) nvec) {
+  time_t now;
+
+  sink_writes(ev_writer_sink(c->w), "254 OK\n");
+  /* pump out initial state */
+  time(&now);
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      playing_is_enabled() ? "enable_play" : "disable_play");
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      random_is_enabled() ? "enable_random" : "disable_random");
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      paused ? "pause" : "resume");
+  c->lo = xmalloc(sizeof *c->lo);
+  c->lo->fn = logclient;
+  c->lo->user = c;
+  eventlog_add(c->lo);
+  c->reader = logging_reader_callback;
+  return 0;
+}
+
+static void post_move_cleanup(void) {
+  struct queue_entry *q;
+
+  /* If we have caused the random track to not be at the end then we make it no
+   * longer be random. */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if(q->state == playing_random && q->next != &qhead)
+      q->state = playing_unplayed;
+  /* That might mean we need to add a new random track. */
+  add_random_track();
+  queue_write();
+}
+
+static int c_move(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  struct queue_entry *q;
+  int n;
+
+  if(config->restrictions & RESTRICT_MOVE) {
+    if(!trusted(c)) {
+      sink_writes(ev_writer_sink(c->w),
+		  "550 only trusted users can move tracks\n");
+      return 1;
+    }
+  }
+  if(!(q = queue_find(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+    return 1;
+  }
+  n = queue_move(q, atoi(vec[1]), c->who);
+  post_move_cleanup();
+  sink_printf(ev_writer_sink(c->w), "252 %d\n", n);
+  /* If we've moved to the head of the queue then prepare the track. */
+  if(q == qhead.next)
+    prepare(c->ev, q);
+  return 1;
+}
+
+static int c_moveafter(struct conn *c,
+		       char **vec,
+		       int attribute((unused)) nvec) {
+  struct queue_entry *q, **qs;
+  int n;
+
+  if(config->restrictions & RESTRICT_MOVE) {
+    if(!trusted(c)) {
+      sink_writes(ev_writer_sink(c->w),
+		  "550 only trusted users can move tracks\n");
+      return 1;
+    }
+  }
+  if(vec[0][0]) {
+    if(!(q = queue_find(vec[0]))) {
+      sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+      return 1;
+    }
+  } else
+    q = 0;
+  ++vec;
+  --nvec;
+  qs = xcalloc(nvec, sizeof *qs);
+  for(n = 0; n < nvec; ++n)
+    if(!(qs[n] = queue_find(vec[n]))) {
+      sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+      return 1;
+    }
+  queue_moveafter(q, nvec, qs, c->who);
+  post_move_cleanup();
+  sink_printf(ev_writer_sink(c->w), "250 Moved tracks\n");
+  /* If we've moved to the head of the queue then prepare the track. */
+  if(q == qhead.next)
+    prepare(c->ev, q);
+  return 1;
+}
+
+static int c_part(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n",
+	      trackdb_getpart(vec[0], vec[1], vec[2]));
+  return 1;
+}
+
+static int c_resolve(struct conn *c,
+		     char **vec,
+		     int attribute((unused)) nvec) {
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", track);
+  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) {
+    sink_printf(ev_writer_sink(c->w), "%s%s\n",
+		**tags == '.' ? "." : "", *tags);
+    ++tags;
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+
+}
+
+static int c_set_global(struct conn *c,
+			char **vec,
+			int attribute((unused)) nvec) {
+  trackdb_set_global(vec[0], vec[1], c->who);
+  sink_printf(ev_writer_sink(c->w), "250 OK\n");
+  return 1;
+}
+
+static int c_get_global(struct conn *c,
+			char **vec,
+			int attribute((unused)) nvec) {
+  const char *s = trackdb_get_global(vec[0]);
+
+  if(s)
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", s);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+#define C_AUTH		0001		/* must be authenticated */
+#define C_TRUSTED	0002		/* must be trusted user */
+
+static const struct command {
+  const char *name;
+  int minargs, maxargs;
+  int (*fn)(struct conn *, char **, int);
+  unsigned flags;
+} commands[] = {
+  { "allfiles",       0, 2,       c_allfiles,       C_AUTH },
+  { "become",         1, 1,       c_become,         C_AUTH|C_TRUSTED },
+  { "dirs",           0, 2,       c_dirs,           C_AUTH },
+  { "disable",        0, 1,       c_disable,        C_AUTH },
+  { "enable",         0, 0,       c_enable,         C_AUTH },
+  { "enabled",        0, 0,       c_enabled,        C_AUTH },
+  { "exists",         1, 1,       c_exists,         C_AUTH },
+  { "files",          0, 2,       c_files,          C_AUTH },
+  { "get",            2, 2,       c_get,            C_AUTH },
+  { "get-global",     1, 1,       c_get_global,     C_AUTH },
+  { "length",         1, 1,       c_length,         C_AUTH },
+  { "log",            0, 0,       c_log,            C_AUTH },
+  { "move",           2, 2,       c_move,           C_AUTH },
+  { "moveafter",      1, INT_MAX, c_moveafter,      C_AUTH },
+  { "part",           3, 3,       c_part,           C_AUTH },
+  { "pause",          0, 0,       c_pause,          C_AUTH },
+  { "play",           1, 1,       c_play,           C_AUTH },
+  { "playing",        0, 0,       c_playing,        C_AUTH },
+  { "prefs",          1, 1,       c_prefs,          C_AUTH },
+  { "queue",          0, 0,       c_queue,          C_AUTH },
+  { "random-disable", 0, 0,       c_random_disable, C_AUTH },
+  { "random-enable",  0, 0,       c_random_enable,  C_AUTH },
+  { "random-enabled", 0, 0,       c_random_enabled, C_AUTH },
+  { "recent",         0, 0,       c_recent,         C_AUTH },
+  { "reconfigure",    0, 0,       c_reconfigure,    C_AUTH|C_TRUSTED },
+  { "remove",         1, 1,       c_remove,         C_AUTH },
+  { "rescan",         0, 0,       c_rescan,         C_AUTH|C_TRUSTED },
+  { "resolve",        1, 1,       c_resolve,        C_AUTH },
+  { "resume",         0, 0,       c_resume,         C_AUTH },
+  { "scratch",        0, 1,       c_scratch,        C_AUTH },
+  { "search",         1, 1,       c_search,         C_AUTH },
+  { "set",            3, 3,       c_set,            C_AUTH, },
+  { "set-global",     2, 2,       c_set_global,     C_AUTH },
+  { "shutdown",       0, 0,       c_shutdown,       C_AUTH|C_TRUSTED },
+  { "stats",          0, 0,       c_stats,          C_AUTH },
+  { "tags",           0, 0,       c_tags,           C_AUTH },
+  { "unset",          2, 2,       c_set,            C_AUTH },
+  { "unset-global",   1, 1,       c_set_global,      C_AUTH },
+  { "user",           2, 2,       c_user,           0 },
+  { "version",        0, 0,       c_version,        C_AUTH },
+  { "volume",         0, 2,       c_volume,         C_AUTH }
+};
+
+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. */
+static int command(struct conn *c, char *line) {
+  char **vec;
+  int nvec, n;
+
+  D(("server command %s", line));
+  if(!(vec = split(line, &nvec, SPLIT_QUOTES, command_error, c))) {
+    sink_writes(ev_writer_sink(c->w), "500 cannot parse command\n");
+    return 1;
+  }
+  if(nvec == 0) {
+    sink_writes(ev_writer_sink(c->w), "500 do what?\n");
+    return 1;
+  }
+  if((n = TABLE_FIND(commands, struct command, name, vec[0])) < 0)
+    sink_writes(ev_writer_sink(c->w), "500 unknown command\n");
+  else {
+    if((commands[n].flags & C_AUTH) && !c->who) {
+      sink_writes(ev_writer_sink(c->w), "530 not authenticated\n");
+      return 1;
+    }
+    if((commands[n].flags & C_TRUSTED) && !trusted(c)) {
+      sink_writes(ev_writer_sink(c->w), "530 insufficient privilege\n");
+      return 1;
+    }
+    ++vec;
+    --nvec;
+    if(nvec < commands[n].minargs) {
+      sink_writes(ev_writer_sink(c->w), "500 missing argument(s)\n");
+      return 1;
+    }
+    if(nvec > commands[n].maxargs) {
+      sink_writes(ev_writer_sink(c->w), "500 too many arguments\n");
+      return 1;
+    }
+    return commands[n].fn(c, vec, nvec);
+  }
+  return 1;			/* completed */
+}
+
+/* redirect to the right reader callback for our current state */
+static int redirect_reader_callback(ev_source *ev,
+				    ev_reader *reader,
+				    int fd,
+				    void *ptr,
+				    size_t bytes,
+				    int eof,
+				    void *u) {
+  struct conn *c = u;
+
+  return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+/* the main command reader */
+static int reader_callback(ev_source attribute((unused)) *ev,
+			   ev_reader *reader,
+			   int attribute((unused)) fd,
+			   void *ptr,
+			   size_t bytes,
+			   int eof,
+			   void *u) {
+  struct conn *c = u;
+  char *eol;
+  int complete;
+
+  D(("server reader_callback"));
+  while((eol = memchr(ptr, '\n', bytes))) {
+    *eol++ = 0;
+    ev_reader_consume(reader, eol - (char *)ptr);
+    complete = command(c, ptr);
+    bytes -= (eol - (char *)ptr);
+    ptr = eol;
+    if(!complete) {
+      /* the command had better have set a new reader callback */
+      if(bytes || eof)
+	/* there are further bytes to read, or we are at eof; arrange for the
+	 * command's reader callback to handle them */
+	return ev_reader_incomplete(reader);
+      /* nothing's going on right now */
+      return 0;
+    }
+    /* command completed, we can go around and handle the next one */
+  }
+  if(eof) {
+    if(bytes)
+      error(0, "S%x unterminated line", c->tag);
+    c->r = 0;
+    return ev_writer_close(c->w);
+  }
+  return 0;
+}
+
+static int listen_callback(ev_source *ev,
+			   int fd,
+			   const struct sockaddr attribute((unused)) *remote,
+			   socklen_t attribute((unused)) rlen,
+			   void *u) {
+  const struct listener *l = u;
+  struct conn *c = xmalloc(sizeof *c);
+  static unsigned tags;
+
+  D(("server listen_callback fd %d (%s)", fd, l->name));
+  nonblock(fd);
+  cloexec(fd);
+  c->tag = tags++;
+  c->ev = ev;
+  c->w = ev_writer_new(ev, fd, writer_error, c);
+  c->r = ev_reader_new(ev, fd, redirect_reader_callback, reader_error, c);
+  c->fd = fd;
+  c->reader = reader_callback;
+  c->l = l;
+  gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
+  sink_printf(ev_writer_sink(c->w), "231 %s\n", hex(c->nonce, sizeof c->nonce));
+  return 0;
+}
+
+int server_start(ev_source *ev, int pf,
+		 size_t socklen, const struct sockaddr *sa,
+		 const char *name) {
+  int fd;
+  struct listener *l = xmalloc(sizeof *l);
+  static const int one = 1;
+
+  D(("server_init socket %s", name));
+  fd = xsocket(pf, SOCK_STREAM, 0);
+  xsetsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
+  if(bind(fd, sa, socklen) < 0) {
+    error(errno, "error binding to %s", name);
+    return -1;
+  }
+  xlisten(fd, 128);
+  nonblock(fd);
+  cloexec(fd);
+  l->name = name;
+  l->pf = pf;
+  if(ev_listen(ev, fd, listen_callback, l)) exit(EXIT_FAILURE);
+  return fd;
+}
+
+int server_stop(ev_source *ev, int fd) {
+  xclose(fd);
+  return ev_listen_cancel(ev, fd);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:eb9b30c87008880f3f53535101356ab5 */
diff --git a/server/server.h b/server/server.h
new file mode 100644
index 0000000..012497c
--- /dev/null
+++ b/server/server.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef SERVER_H
+#define SERVER_H
+
+/* The command-response server.
+ *
+ * See disorder_protocol(5) for sometimes up-to-date protocol documentation.
+ */
+
+int server_start(ev_source *ev, int pf,
+		 size_t socklen, const struct sockaddr *sa,
+		 const char *name);
+/* start listening.  Return the fd. */
+
+int server_stop(ev_source *ev, int fd);
+/* Stop listening on @fd@ */
+
+extern int volume_left, volume_right;	/* last known volume */
+
+#endif /* SERVER_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:db22be3b0e3ce0914513e917df553937 */
diff --git a/server/speaker.c b/server/speaker.c
new file mode 100644
index 0000000..ef31931
--- /dev/null
+++ b/server/speaker.c
@@ -0,0 +1,634 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/* This program deliberately does not use the garbage collector even though it
+ * might be convenient to do so.  This is for two reasons.  Firstly some libao
+ * drivers are implemented using threads and we do not want to have to deal
+ * with potential interactions between threading and garbage collection.
+ * Secondly this process needs to be able to respond quickly and this is not
+ * compatible with the collector hanging the program even relatively
+ * briefly. */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "speaker.h"
+#include "user.h"
+
+#define BUFFER_SECONDS 5                /* How many seconds of input to
+                                         * buffer. */
+
+#define FRAMES 4096                     /* Frame batch size */
+
+#define NFDS 256                        /* Max FDs to poll for */
+
+/* Known tracks are kept in a linked list.  We don't normally to have
+ * more than two - maybe three at the outside. */
+static struct track {
+  struct track *next;                   /* next track */
+  int fd;                               /* input FD */
+  char id[24];                          /* ID */
+  size_t start, used;                   /* start + bytes used */
+  int eof;                              /* input is at EOF */
+  int got_format;                       /* got format yet? */
+  ao_sample_format format;              /* sample format */
+  unsigned long long played;            /* number of frames played */
+  char *buffer;                         /* sample buffer */
+  size_t size;                          /* sample buffer size */
+  int slot;                             /* poll array slot */
+} *tracks, *playing;                    /* all tracks + playing track */
+
+static time_t last_report;              /* when we last reported */
+static int paused;                      /* pause status */
+static snd_pcm_t *pcm;                  /* current pcm handle */
+static ao_sample_format pcm_format;     /* current format if aodev != 0 */
+static size_t bpf;                      /* bytes per frame */
+static struct pollfd fds[NFDS];         /* if we need more than that */
+static int fdno;                        /* fd number */
+static snd_pcm_uframes_t pcm_bufsize;   /* buffer size */
+static int forceplay;                   /* frames to force play */
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* Display usage message and terminate. */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-speaker [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Speaker process for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* Display version number and terminate. */
+static void version(void) {
+  xprintf("disorder-speaker version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* Return the number of bytes per frame in FORMAT. */
+static size_t bytes_per_frame(const ao_sample_format *format) {
+  return format->channels * format->bits / 8;
+}
+
+/* Find track ID, maybe creating it if not found. */
+static struct track *findtrack(const char *id, int create) {
+  struct track *t;
+
+  D(("findtrack %s %d", id, create));
+  for(t = tracks; t && strcmp(id, t->id); t = t->next)
+    ;
+  if(!t && create) {
+    t = xmalloc(sizeof *t);
+    t->next = tracks;
+    strcpy(t->id, id);
+    t->fd = -1;
+    tracks = t;
+    /* The initial input buffer will be the sample format. */
+    t->buffer = (void *)&t->format;
+    t->size = sizeof t->format;
+  }
+  return t;
+}
+
+/* Remove track ID (but do not destroy it). */
+static struct track *removetrack(const char *id) {
+  struct track *t, **tt;
+
+  D(("removetrack %s", id));
+  for(tt = &tracks; (t = *tt) && strcmp(id, t->id); tt = &t->next)
+    ;
+  if(t)
+    *tt = t->next;
+  return t;
+}
+
+/* Destroy a track. */
+static void destroy(struct track *t) {
+  D(("destroy %s", t->id));
+  if(t->fd != -1) xclose(t->fd);
+  if(t->buffer != (void *)&t->format) free(t->buffer);
+  free(t);
+}
+
+/* Notice a new FD. */
+static void acquire(struct track *t, int fd) {
+  D(("acquire %s %d", t->id, fd));
+  if(t->fd != -1)
+    xclose(t->fd);
+  t->fd = fd;
+  nonblock(fd);
+}
+
+/* Read data into a sample buffer.  Return 0 on success, -1 on EOF. */
+static int fill(struct track *t) {
+  size_t where, left;
+  int n;
+
+  D(("fill %s: eof=%d used=%zu size=%zu  got_format=%d",
+     t->id, t->eof, t->used, t->size, t->got_format));
+  if(t->eof) return -1;
+  if(t->used < t->size) {
+    /* there is room left in the buffer */
+    where = (t->start + t->used) % t->size;
+    if(t->got_format) {
+      /* We are reading audio data, get as much as we can */
+      if(where >= t->start) left = t->size - where;
+      else left = t->start - where;
+    } else
+      /* We are still waiting for the format, only get that */
+      left = sizeof (ao_sample_format) - t->used;
+    do {
+      n = read(t->fd, t->buffer + where, left);
+    } while(n < 0 && errno == EINTR);
+    if(n < 0) {
+      if(errno != EAGAIN) fatal(errno, "error reading sample stream");
+      return 0;
+    }
+    if(n == 0) {
+      D(("fill %s: eof detected", t->id));
+      t->eof = 1;
+      return -1;
+    }
+    t->used += n;
+    if(!t->got_format && t->used >= sizeof (ao_sample_format)) {
+      assert(t->used == sizeof (ao_sample_format));
+      /* Check that our assumptions are met. */
+      if(t->format.bits & 7)
+        fatal(0, "bits per sample not a multiple of 8");
+      /* Make a new buffer for audio data. */
+      t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS;
+      t->buffer = xmalloc(t->size);
+      t->used = 0;
+      t->got_format = 1;
+      D(("got format for %s", t->id));
+    }
+  }
+  return 0;
+}
+
+/* Return true if A and B denote identical libao formats, else false. */
+static int formats_equal(const ao_sample_format *a,
+                         const ao_sample_format *b) {
+  return (a->bits == b->bits
+          && a->rate == b->rate
+          && a->channels == b->channels
+          && a->byte_format == b->byte_format);
+}
+
+/* Close the sound device. */
+static void idle(void) {
+  int  err;
+
+  D(("idle"));
+  if(pcm) {
+    if((err = snd_pcm_nonblock(pcm, 0)) < 0)
+      fatal(0, "error calling snd_pcm_nonblock: %d", err);
+    D(("draining pcm"));
+    snd_pcm_drain(pcm);
+    D(("closing pcm"));
+    snd_pcm_close(pcm);
+    pcm = 0;
+    forceplay = 0;
+    D(("released audio device"));
+  }
+}
+
+/* Abandon the current track */
+static void abandon(void) {
+  struct speaker_message sm;
+
+  D(("abandon"));
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_FINISHED;
+  strcpy(sm.id, playing->id);
+  speaker_send(1, &sm, 0);
+  removetrack(playing->id);
+  destroy(playing);
+  playing = 0;
+  forceplay = 0;
+}
+
+/* Make sure the sound device is open and has the right sample format.  Return
+ * 0 on success and -1 on error. */
+static int activate(void) {
+  int err;
+  snd_pcm_hw_params_t *hwparams;
+  snd_pcm_sw_params_t *swparams;
+  int sample_format = 0;
+  unsigned rate;
+
+  /* If we don't know the format yet we cannot start. */
+  if(!playing->got_format) {
+    D((" - not got format for %s", playing->id));
+    return -1;
+  }
+  /* If we need to change format then close the current device. */
+  if(pcm && !formats_equal(&playing->format, &pcm_format))
+     idle();
+  if(!pcm) {
+    D(("snd_pcm_open"));
+    if((err = snd_pcm_open(&pcm,
+                           config->device,
+                           SND_PCM_STREAM_PLAYBACK,
+                           SND_PCM_NONBLOCK))) {
+      error(0, "error from snd_pcm_open: %d", err);
+      goto error;
+    }
+    snd_pcm_hw_params_alloca(&hwparams);
+    D(("set up hw params"));
+    if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+    if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
+                                           SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
+    switch(playing->format.bits) {
+    case 8:
+      sample_format = SND_PCM_FORMAT_S8;
+      break;
+    case 16:
+      switch(playing->format.byte_format) {
+      case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break;
+      case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
+      case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
+        error(0, "unrecognized byte format %d", playing->format.byte_format);
+        goto fatal;
+      }
+      break;
+    default:
+      error(0, "unsupported sample size %d", playing->format.bits);
+      goto fatal;
+    }
+    if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+                                           sample_format)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+            sample_format, err);
+      goto fatal;
+    }
+    rate = playing->format.rate;
+    if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
+            playing->format.rate, err);
+      goto fatal;
+    }
+    if(rate != (unsigned)playing->format.rate)
+      info("want rate %d, got %u", playing->format.rate, rate);
+    if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
+                                             playing->format.channels)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+            playing->format.channels, err);
+      goto fatal;
+    }
+    pcm_bufsize = 3 * FRAMES;
+    if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+                                                     &pcm_bufsize)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+            3 * FRAMES, err);
+    if(pcm_bufsize != 3 * FRAMES)
+      info("asked for PCM buffer of %d frames, got %d",
+           3 * FRAMES, (int)pcm_bufsize);
+    if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+      fatal(0, "error calling snd_pcm_hw_params: %d", err);
+    D(("set up sw params"));
+    snd_pcm_sw_params_alloca(&swparams);
+    if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+    if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+            FRAMES, err);
+    if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params: %d", err);
+    pcm_format = playing->format;
+    bpf = bytes_per_frame(&pcm_format);
+    D(("acquired audio device"));
+  }
+  return 0;
+fatal:
+  abandon();
+error:
+  /* We assume the error is temporary and that we'll retry in a bit. */
+  if(pcm) {
+    snd_pcm_close(pcm);
+    pcm = 0;
+  }
+  return -1;
+}
+
+/* Check to see whether the current track has finished playing */
+static void maybe_finished(void) {
+  if(playing
+     && playing->eof
+     && (!playing->got_format
+         || playing->used < bytes_per_frame(&playing->format)))
+    abandon();
+}
+
+static void play(size_t frames) {
+  snd_pcm_sframes_t written_frames;
+  size_t avail_bytes, avail_frames, written_bytes;
+  int err;
+
+  if(activate()) {
+    if(playing)
+      forceplay = frames;
+    else
+      forceplay = 0;                    /* Must have called abandon() */
+    return;
+  }
+  D(("play: play %zu/%zu%s %dHz %db %dc",  frames, playing->used / bpf,
+     playing->eof ? " EOF" : "",
+     playing->format.rate,
+     playing->format.bits,
+     playing->format.channels));
+  /* If we haven't got enough bytes yet wait until we have.  Exception: when
+   * we are at eof. */
+  if(playing->used < frames * bpf && !playing->eof) {
+    forceplay = frames;
+    return;
+  }
+  /* We have got enough data so don't force play again */
+  forceplay = 0;
+  /* Figure out how many frames there are available to write */
+  if(playing->start + playing->used > playing->size)
+    avail_bytes = playing->size - playing->start;
+  else
+    avail_bytes = playing->used;
+  avail_frames = avail_bytes / bpf;
+  if(avail_frames > frames)
+    avail_frames = frames;
+  if(!avail_frames)
+    return;
+  written_frames = snd_pcm_writei(pcm,
+                                  playing->buffer + playing->start,
+                                  avail_frames);
+  D(("actually play %zu frames, wrote %d",
+     avail_frames, (int)written_frames));
+  if(written_frames < 0) {
+    switch(written_frames) {
+    case -EPIPE:                        /* underrun */
+      error(0, "snd_pcm_writei reports underrun");
+      if((err = snd_pcm_prepare(pcm)) < 0)
+        fatal(0, "error calling snd_pcm_prepare: %d", err);
+      return;
+    case -EAGAIN:
+      return;
+    default:
+      fatal(0, "error calling snd_pcm_writei: %d", (int)written_frames);
+    }
+  }
+  written_bytes = written_frames * bpf;
+  playing->start += written_bytes;
+  playing->used -= written_bytes;
+  playing->played += written_frames;
+  /* If the pointer is at the end of the buffer (or the buffer is completely
+   * empty) wrap it back to the start. */
+  if(!playing->used || playing->start == playing->size)
+    playing->start = 0;
+  frames -= written_frames;
+}
+
+/* Notify the server what we're up to. */
+static void report(void) {
+  struct speaker_message sm;
+
+  if(playing && playing->buffer != (void *)&playing->format) {
+    memset(&sm, 0, sizeof sm);
+    sm.type = paused ? SM_PAUSED : SM_PLAYING;
+    strcpy(sm.id, playing->id);
+    sm.data = playing->played / playing->format.rate;
+    speaker_send(1, &sm, 0);
+  }
+  time(&last_report);
+}
+
+static int addfd(int fd, int events) {
+  if(fdno < NFDS) {
+    fds[fdno].fd = fd;
+    fds[fdno].events = events;
+    return fdno++;
+  } else
+    return -1;
+}
+
+int main(int argc, char **argv) {
+  int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, err;
+  unsigned short alsa_revents;
+  struct track *t;
+  struct speaker_message sm;
+
+  set_progname(argv);
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(getenv("DISORDER_DEBUG_SPEAKER")) debugging = 1;
+  /* If stderr is a TTY then log there, otherwise to syslog. */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  /* ignore SIGPIPE */
+  signal(SIGPIPE, SIG_IGN);
+  /* set nice value */
+  xnice(config->nice_speaker);
+  /* change user */
+  become_mortal();
+  /* make sure we're not root, whatever the config says */
+  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+  info("started");
+  while(getppid() != 1) {
+    fdno = 0;
+    /* Always ready for commands from the main server. */
+    stdin_slot = addfd(0, POLLIN);
+    /* Try to read sample data for the currently playing track if there is
+     * buffer space. */
+    if(playing && !playing->eof && playing->used < playing->size) {
+      playing->slot = addfd(playing->fd, POLLIN);
+    } else if(playing)
+      playing->slot = -1;
+    /* If forceplay is set then wait until it succeeds before waiting on the
+     * sound device. */
+    if(pcm && !forceplay) {
+      alsa_slots = fdno;
+      alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno);
+      fdno += alsa_nslots;
+    } else
+      alsa_slots = -1;
+    /* If any other tracks don't have a full buffer, try to read sample data
+     * from them. */
+    for(t = tracks; t; t = t->next)
+      if(t != playing) {
+        if(!t->eof && t->used < t->size) {
+          t->slot = addfd(t->fd,  POLLIN);
+        } else
+          t->slot = -1;
+      }
+    /* Wait up to a second before thinking about current state */
+    n = poll(fds, fdno, 1000);
+    if(n < 0) {
+      if(errno == EINTR) continue;
+      fatal(errno, "error calling poll");
+    }
+    /* Play some sound before doing anything else */
+    if(alsa_slots != -1) {
+      if((err = snd_pcm_poll_descriptors_revents(pcm,
+                                                 &fds[alsa_slots],
+                                                 alsa_nslots,
+                                                 &alsa_revents)) < 0)
+        fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+      if(alsa_revents & POLLOUT)
+        play(3 * FRAMES);
+    } else {
+      /* Some attempt to play must have failed */
+      if(playing && !paused)
+        play(forceplay);
+      else
+        forceplay = 0;                  /* just in case */
+    }
+    /* Perhaps we have a command to process */
+    if(fds[stdin_slot].revents & POLLIN) {
+      n = speaker_recv(0, &sm, &fd);
+      if(n > 0)
+	switch(sm.type) {
+	case SM_PREPARE:
+          D(("SM_PREPARE %s %d", sm.id, fd));
+	  if(fd == -1) fatal(0, "got SM_PREPARE but no file descriptor");
+	  t = findtrack(sm.id, 1);
+          acquire(t, fd);
+	  break;
+	case SM_PLAY:
+          D(("SM_PLAY %s %d", sm.id, fd));
+          if(playing) fatal(0, "got SM_PLAY but already playing something");
+	  t = findtrack(sm.id, 1);
+          if(fd != -1) acquire(t, fd);
+          playing = t;
+          play(pcm_bufsize);
+          report();
+	  break;
+	case SM_PAUSE:
+          D(("SM_PAUSE"));
+	  paused = 1;
+          report();
+          break;
+	case SM_RESUME:
+          D(("SM_RESUME"));
+          if(paused) {
+            paused = 0;
+            if(playing)
+              play(pcm_bufsize);
+          }
+          report();
+	  break;
+	case SM_CANCEL:
+          D(("SM_CANCEL %s",  sm.id));
+	  t = removetrack(sm.id);
+	  if(t) {
+	    if(t == playing) {
+              sm.type = SM_FINISHED;
+              strcpy(sm.id, playing->id);
+              speaker_send(1, &sm, 0);
+	      playing = 0;
+            }
+	    destroy(t);
+	  } else
+	    error(0, "SM_CANCEL for unknown track %s", sm.id);
+          report();
+	  break;
+	case SM_RELOAD:
+          D(("SM_RELOAD"));
+	  if(config_read()) error(0, "cannot read configuration");
+          info("reloaded configuration");
+	  break;
+	default:
+	  error(0, "unknown message type %d", sm.type);
+        }
+    }
+    /* Read in any buffered data */
+    for(t = tracks; t; t = t->next)
+      if(t->slot != -1 && (fds[t->slot].revents & POLLIN))
+         fill(t);
+    /* We might be able to play now */
+    if(pcm && forceplay && playing && !paused)
+      play(forceplay);
+    /* Maybe we finished playing a track somewhere in the above */
+    maybe_finished();
+    /* If we don't need the sound device for now then close it for the benefit
+     * of anyone else who wants it. */
+    if((!playing || paused) && pcm)
+      idle();
+    /* If we've not reported out state for a second do so now. */
+    if(time(0) > last_report)
+      report();
+  }
+  info("stopped (parent terminated)");
+  exit(0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:HQ4ayCGCjeBF97RuRnvcyg */
diff --git a/server/state.c b/server/state.c
new file mode 100644
index 0000000..60fea26
--- /dev/null
+++ b/server/state.c
@@ -0,0 +1,171 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "play.h"
+#include "trackdb.h"
+#include "state.h"
+#include "configuration.h"
+#include "log.h"
+#include "queue.h"
+#include "server.h"
+#include "printf.h"
+#include "addr.h"
+
+static const char *current_unix;
+static int current_unix_fd;
+
+static struct addrinfo *current_listen_addrinfo;
+static int current_listen_fd;
+
+void quit(ev_source *ev) {
+  quitting(ev);
+  trackdb_close();
+  trackdb_deinit();
+  info("terminating");
+  _exit(0);
+}
+
+static void reset_socket(ev_source *ev) {
+  const char *new_unix;
+  struct addrinfo *res;
+  struct sockaddr_un sun;
+  char *name;
+  
+  static const struct addrinfo pref = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_STREAM,
+    IPPROTO_TCP,
+    0,
+    0,
+    0,
+    0
+  };
+
+  /* unix first */
+  new_unix = config_get_file("socket");
+  if(!current_unix || strcmp(current_unix, new_unix)) {
+    /* either there was no socket, or there was but a different path */
+    if(current_unix) {
+      /* stop the old one and remove it from the filesystem */
+      server_stop(ev, current_unix_fd);
+      if(unlink(current_unix) < 0)
+	fatal(errno, "unlink %s", current_unix);
+    }
+    /* start the new one */
+    if(strlen(new_unix) >= sizeof sun.sun_path)
+      fatal(0, "socket path %s is too long", new_unix);
+    memset(&sun, 0, sizeof sun);
+    sun.sun_family = AF_UNIX;
+    strcpy(sun.sun_path, new_unix);
+    if(unlink(new_unix) < 0 && errno != ENOENT)
+      fatal(errno, "unlink %s", new_unix);
+    if((current_unix_fd = server_start(ev, PF_UNIX, sizeof sun,
+				       (const struct sockaddr *)&sun,
+				       new_unix)) >= 0) {
+      current_unix = new_unix;
+      if(chmod(new_unix, 0777) < 0)
+	fatal(errno, "error calling chmod %s", new_unix);
+    } else
+      current_unix = 0;
+  }
+
+  /* get the new listen config */
+  if(config->listen.n)
+    res = get_address(&config->listen, &pref, &name);
+  else
+    res = 0;
+
+  if((res && !current_listen_addrinfo)
+     || (current_listen_addrinfo
+	 && (!res
+	     || addrinfocmp(res, current_listen_addrinfo)))) {
+    /* something has to change */
+    if(current_listen_addrinfo) {
+      /* delete the old listener */
+      server_stop(ev, current_listen_fd);
+      freeaddrinfo(current_listen_addrinfo);
+      current_listen_addrinfo = 0;
+    }
+    if(res) {
+      /* start the new listener */
+      if((current_listen_fd = server_start(ev, res->ai_family, res->ai_addrlen,
+					   res->ai_addr, name)) >= 0) {
+	current_listen_addrinfo = res;
+	res = 0;
+      }
+    }
+  }
+  /* if res is still set it needs freeing */
+  if(res)
+    freeaddrinfo(res);
+}
+
+int reconfigure(ev_source *ev, int reload) {
+  int need_another_rescan = 0;
+  int ret = 0;
+
+  D(("reconfigure(%d)", reload));
+  if(reload) {
+    need_another_rescan = trackdb_rescan_cancel();
+    trackdb_close();
+    if(config_read())
+      ret = -1;
+    else {
+      /* Tell the speaker it needs to reload its config too. */
+      speaker_reload();
+      info("%s: installed new configuration", configfile);
+    }
+  }
+  trackdb_open();
+  if(need_another_rescan)
+    trackdb_rescan(ev);
+  if(!ret) {
+    queue_read();
+    recent_read();
+    reset_socket(ev);
+  }
+  return ret;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:94e23a75c2ebdf8a11e17ed7b0fd8cb6 */
diff --git a/server/state.h b/server/state.h
new file mode 100644
index 0000000..ecfdc8e
--- /dev/null
+++ b/server/state.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef STATE_H
+#define STATE_H
+
+void quit(ev_source *ev) attribute((noreturn));
+/* terminate the daemon */
+
+int reconfigure(ev_source *ev, int reload);
+/* reconfigure.  If @reload@ is nonzero, update the configuration. */
+
+#endif /* QUIT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a119be84b4ca00f632dad62837a8f965 */
diff --git a/server/trackdb-int.h b/server/trackdb-int.h
new file mode 100644
index 0000000..7e14fb7
--- /dev/null
+++ b/server/trackdb-int.h
@@ -0,0 +1,124 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef TRACKDB_INT_H
+#define TRACKDB_INT_H
+
+extern DB_ENV *trackdb_env;
+
+extern DB *trackdb_tracksdb;
+extern DB *trackdb_prefsdb;
+extern DB *trackdb_searchdb;
+
+DBC *trackdb_opencursor(DB *db, DB_TXN *tid);
+/* open a transaction */
+
+int trackdb_closecursor(DBC *c);
+/* close transaction, returns 0 or DB_LOCK_DEADLOCK */
+
+int trackdb_notice(const char *track,
+                   const char *path);
+int trackdb_notice_tid(const char *track,
+                       const char *path,
+                       DB_TXN *tid);
+/* notice a track; return DB_NOTFOUND if new, else 0.  _tid can return
+ * DB_LOCK_DEADLOCK too. */
+
+int trackdb_obsolete(const char *track, DB_TXN *tid);
+/* obsolete a track */
+
+DB_TXN *trackdb_begin_transaction(void);
+void trackdb_abort_transaction(DB_TXN *tid);
+void trackdb_commit_transaction(DB_TXN *tid);
+/* begin, abort or commit a transaction */
+
+int trackdb_getdata(DB *db,
+                    const char *track,
+                    struct kvp **kp,
+                    DB_TXN *tid);
+/* fetch and decode a database entry.  Returns 0, DB_NOTFOUND or
+ * DB_LOCK_DEADLOCK. */
+
+int trackdb_putdata(DB *db,
+                    const char *track,
+                    const struct kvp *k,
+                    DB_TXN *tid,
+                    u_int32_t flags);
+/* encode and store a database entry.  Returns 0, DB_KEYEXIST or
+ * DB_LOCK_DEADLOCK. */
+
+int trackdb_delkey(DB *db,
+                   const char *track,
+                   DB_TXN *tid);
+/* delete a database entry.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+
+int trackdb_delkeydata(DB *db,
+                       const char *word,
+                       const char *track,
+                       DB_TXN *tid);
+/* delete a (key,data) pair.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+
+int trackdb_scan(const char *root,
+                 int (*callback)(const char *track,
+                                 struct kvp *data,
+                                 void *u,
+                                 DB_TXN *tid),
+                 void *u,
+                 DB_TXN *tid);
+/* Call CALLBACK for each non-alias track below ROOT.  Return 0 or
+ * DB_LOCK_DEADLOCK.  CALLBACK should return 0 on success or EINTR to cancel
+ * the scan. */
+
+/* fill KEY in with S, returns KEY */
+static inline DBT *make_key(DBT *key, const char *s) {
+  memset(key, 0, sizeof *key);
+  key->data = (void *)s;
+  key->size = strlen(s);
+  return key;
+}
+
+/* set DATA up to receive data, returns DATA */
+static inline DBT *prepare_data(DBT *data) {
+  memset(data, 0, sizeof *data);
+  data->flags = DB_DBT_MALLOC;
+  return data;
+}
+
+/* encode K and store in DATA, returns DATA */
+static inline DBT *encode_data(DBT *data, const struct kvp *k) {
+  size_t size;
+  
+  memset(data, 0, sizeof *data);
+  data->data = kvp_urlencode(k, &size);
+  data->size = size;
+  return data;
+}
+
+#endif /* TRACKDB_INT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:BJ2D2N4ftvK2bQRnaLuFIg */
diff --git a/server/trackdb.c b/server/trackdb.c
new file mode 100644
index 0000000..223dc80
--- /dev/null
+++ b/server/trackdb.c
@@ -0,0 +1,1833 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "mem.h"
+#include "kvp.h"
+#include "log.h"
+#include "vector.h"
+#include "trackdb.h"
+#include "configuration.h"
+#include "syscalls.h"
+#include "wstat.h"
+#include "words.h"
+#include "printf.h"
+#include "filepart.h"
+#include "trackname.h"
+#include "trackdb-int.h"
+#include "logfd.h"
+#include "cache.h"
+#include "eventlog.h"
+#include "hash.h"
+
+#define RESCAN "disorder-rescan"
+#define DEADLOCK "disorder-deadlock"
+
+static const char *getpart(const char *track,
+                           const char *context,
+                           const char *part,
+                           const struct kvp *p,
+                           int *used_db);
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp);
+static int trackdb_get_global_tid(const char *name,
+                                  DB_TXN *tid,
+                                  const char **rp);
+
+const struct cache_type cache_files_type = { 86400 };
+unsigned long cache_files_hits, cache_files_misses;
+
+/* setup and teardown ********************************************************/
+
+static const char *home;                /* home had better not change */
+DB_ENV *trackdb_env;			/* db environment */
+DB *trackdb_tracksdb;			/* the db itself */
+DB *trackdb_prefsdb;			/* preferences */
+DB *trackdb_searchdb;			/* the search database */
+DB *trackdb_tagsdb;			/* the tags database */
+DB *trackdb_globaldb;                   /* global preferences */
+static pid_t db_deadlock_pid = -1;      /* deadlock manager PID */
+static pid_t rescan_pid = -1;           /* rescanner PID */
+static int initialized, opened;         /* state */
+
+/* tracks matched by required_tags */
+static char **reqtracks;
+static size_t nreqtracks;
+
+/* comparison function for keys */
+static int compare(DB attribute((unused)) *db_,
+		   const DBT *a, const DBT *b) {
+  return compare_path_raw(a->data, a->size, b->data, b->size);
+}
+
+/* open environment */
+void trackdb_init(int recover) {
+  int err;
+  static int recover_type[] = { 0, DB_RECOVER, DB_RECOVER_FATAL };
+
+  /* sanity checks */
+  assert(initialized == 0);
+  ++initialized;
+  if(home) {
+    if(strcmp(home, config->home))
+      fatal(0, "cannot change db home without server restart");
+    home = config->home;
+  }
+
+  /* create environment */
+  if((err = db_env_create(&trackdb_env, 0))) fatal(0, "db_env_create: %s",
+                                                   db_strerror(err));
+  if((err = trackdb_env->set_alloc(trackdb_env,
+                                   xmalloc_noptr, xrealloc_noptr, xfree)))
+    fatal(0, "trackdb_env->set_alloc: %s", db_strerror(err));
+  if((err = trackdb_env->set_lk_max_locks(trackdb_env, 10000)))
+    fatal(0, "trackdb_env->set_lk_max_locks: %s", db_strerror(err));
+  if((err = trackdb_env->set_lk_max_objects(trackdb_env, 10000)))
+    fatal(0, "trackdb_env->set_lk_max_objects: %s", db_strerror(err));
+  if((err = trackdb_env->open(trackdb_env, config->home,
+                              DB_INIT_LOG
+                              |DB_INIT_LOCK
+                              |DB_INIT_MPOOL
+                              |DB_INIT_TXN
+                              |DB_CREATE
+                              |recover_type[recover],
+                              0666)))
+    fatal(0, "trackdb_env->open: %s", db_strerror(err));
+  trackdb_env->set_errpfx(trackdb_env, "DB");
+  trackdb_env->set_errfile(trackdb_env, stderr);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_DEADLOCK, 1);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_RECOVERY, 1);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_REPLICATION, 1);
+  D(("initialized database environment"));
+}
+
+/* called when deadlock manager terminates */
+static int reap_db_deadlock(ev_source attribute((unused)) *ev,
+                            pid_t attribute((unused)) pid,
+                            int status,
+                            const struct rusage attribute((unused)) *rusage,
+                            void attribute((unused)) *u) {
+  db_deadlock_pid = -1;
+  if(initialized)
+    fatal(0, "deadlock manager unexpectedly terminated: %s",
+          wstat(status));
+  else
+    D(("deadlock manager terminated: %s", wstat(status)));
+  return 0;
+}
+
+static pid_t subprogram(ev_source *ev, const char *prog) {
+  pid_t pid;
+  int lfd;
+
+  /* If we're in the background then trap subprocess stdout/stderr */
+  if(!isatty(2))
+    lfd = logfd(ev, prog);
+  else
+    lfd = -1;
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    signal(SIGPIPE, SIG_DFL);
+    if(lfd != -1) {
+      xdup2(lfd, 1);
+      xdup2(lfd, 2);
+    }
+    /* If we were negatively niced, undo it.  We don't bother checking for
+     * error, it's not that important. */
+    setpriority(PRIO_PROCESS, 0, 0);
+    execlp(prog, prog, "--config", configfile,
+           debugging ? "--debug" : "--no-debug",
+           (char *)0);
+    fatal(errno, "error invoking %s", prog);
+  }
+  if(lfd != -1) xclose(lfd);
+  return pid;
+}
+
+/* start deadlock manager */
+void trackdb_master(ev_source *ev) {
+  assert(db_deadlock_pid == -1);
+  db_deadlock_pid = subprogram(ev, DEADLOCK);
+  ev_child(ev, db_deadlock_pid, 0, reap_db_deadlock, 0);
+  D(("started deadlock manager"));
+}
+
+/* close environment */
+void trackdb_deinit(void) {
+  int err;
+
+  /* sanity checks */
+  assert(initialized == 1);
+  --initialized;
+
+  /* close the environment */
+  if((err = trackdb_env->close(trackdb_env, 0)))
+    fatal(0, "trackdb_env->close: %s", db_strerror(err));
+
+  if(rescan_pid != -1 && kill(rescan_pid, SIGTERM) < 0)
+    fatal(errno, "error killing rescanner");
+
+  /* terminate the deadlock manager */
+  if(db_deadlock_pid != -1 && kill(db_deadlock_pid, SIGTERM) < 0)
+    fatal(errno, "error killing deadlock manager");
+  db_deadlock_pid = -1;
+
+  D(("deinitialized database environment"));
+}
+
+/* open a specific database */
+static DB *open_db(const char *path,
+                   u_int32_t dbflags,
+                   DBTYPE dbtype,
+                   u_int32_t openflags,
+                   int mode) {
+  int err;
+  DB *db;
+
+  D(("open %s", path));
+  path = config_get_file(path);
+  if((err = db_create(&db, trackdb_env, 0)))
+    fatal(0, "db_create %s: %s", path, db_strerror(err));
+  if(dbflags)
+    if((err = db->set_flags(db, dbflags)))
+      fatal(0, "db->set_flags %s: %s", path, db_strerror(err));
+  if(dbtype == DB_BTREE)
+    if((err = db->set_bt_compare(db, compare)))
+      fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err));
+  if((err = db->open(db, 0, path, 0, dbtype,
+                     openflags | DB_AUTO_COMMIT, mode)))
+    fatal(0, "db->open %s: %s", path, db_strerror(err));
+  return db;
+}
+
+/* open track databases */
+void trackdb_open(void) {
+  /* sanity checks */
+  assert(opened == 0);
+  ++opened;
+  /* open the databases */
+  trackdb_tracksdb = open_db("tracks.db",
+                             DB_RECNUM, DB_BTREE, DB_CREATE, 0666);
+  trackdb_searchdb = open_db("search.db",
+                             DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+  trackdb_tagsdb = open_db("tags.db",
+                           DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+  trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, DB_CREATE, 0666);
+  trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666);
+  D(("opened databases"));
+}
+
+/* close track databases */
+void trackdb_close(void) {
+  int err;
+  
+  /* sanity checks */
+  assert(opened == 1);
+  --opened;
+  if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0)))
+    fatal(0, "error closing tracks.db: %s", db_strerror(err));
+  if((err = trackdb_searchdb->close(trackdb_searchdb, 0)))
+    fatal(0, "error closing search.db: %s", db_strerror(err));
+  if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0)))
+    fatal(0, "error closing tags.db: %s", db_strerror(err));
+  if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0)))
+    fatal(0, "error closing prefs.db: %s", db_strerror(err));
+  if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
+    fatal(0, "error closing global.db: %s", db_strerror(err));
+  trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
+  trackdb_tagsdb = trackdb_globaldb = 0;
+  D(("closed databases"));
+}
+
+/* generic db routines *******************************************************/
+
+/* fetch and decode a database entry.  Returns 0, DB_NOTFOUND or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_getdata(DB *db,
+                    const char *track,
+                    struct kvp **kp,
+                    DB_TXN *tid) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->get(db, tid, make_key(&key, track),
+                       prepare_data(&data), 0)) {
+  case 0:
+    *kp = kvp_urldecode(data.data, data.size);
+    return 0;
+  case DB_NOTFOUND:
+    *kp = 0;
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error querying database: %s", db_strerror(err));
+  }
+}
+
+/* encode and store a database entry.  Returns 0, DB_KEYEXIST or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_putdata(DB *db,
+                    const char *track,
+                    const struct kvp *k,
+                    DB_TXN *tid,
+                    u_int32_t flags) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->put(db, tid, make_key(&key, track),
+                       encode_data(&data, k), flags)) {
+  case 0:
+  case DB_KEYEXIST:
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* delete a database entry */
+int trackdb_delkey(DB *db,
+                   const char *track,
+                   DB_TXN *tid) {
+  int err;
+
+  DBT key;
+  switch(err = db->del(db, tid, make_key(&key, track), 0)) {
+  case 0:
+  case DB_NOTFOUND:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* open a database cursor */
+DBC *trackdb_opencursor(DB *db, DB_TXN *tid) {
+  int err;
+  DBC *c;
+
+  switch(err = db->cursor(db, tid, &c, 0)) {
+  case 0: break;
+  default: fatal(0, "error creating cursor: %s", db_strerror(err));
+  }
+  return c;
+}
+
+/* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */
+int trackdb_closecursor(DBC *c) {
+  int err;
+
+  if(!c) return 0;
+  switch(err = c->c_close(c)) {
+  case 0:
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error closing cursor: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error closing cursor: %s", db_strerror(err));
+  }
+}
+
+/* delete a (key,data) pair.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+int trackdb_delkeydata(DB *db,
+                       const char *word,
+                       const char *track,
+                       DB_TXN *tid) {
+  int err;
+  DBC *c;
+  DBT key, data;
+
+  c = trackdb_opencursor(db, tid);
+  switch(err = c->c_get(c, make_key(&key, word),
+                        make_key(&data, track), DB_GET_BOTH)) {
+  case 0:
+    switch(err = c->c_del(c, 0)) {
+    case 0:
+      break;
+    case DB_KEYEMPTY:
+      err = 0;
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error updating database: %s", db_strerror(err));
+      break;
+    default:
+      fatal(0, "c->c_del: %s", db_strerror(err));
+    }
+    break;
+  case DB_NOTFOUND:
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+  if(trackdb_closecursor(c)) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* start a transaction */
+DB_TXN *trackdb_begin_transaction(void) {
+  DB_TXN *tid;
+  int err;
+
+  if((err = trackdb_env->txn_begin(trackdb_env, 0, &tid, 0)))
+    fatal(0, "trackdb_env->txn_begin: %s", db_strerror(err));
+  return tid;
+}
+
+/* abort transaction */
+void trackdb_abort_transaction(DB_TXN *tid) {
+  int err;
+
+  if(tid)
+    if((err = tid->abort(tid)))
+      fatal(0, "tid->abort: %s", db_strerror(err));
+}
+
+/* commit transaction */
+void trackdb_commit_transaction(DB_TXN *tid) {
+  int err;
+
+  if((err = tid->commit(tid, 0)))
+    fatal(0, "tid->commit: %s", db_strerror(err));
+}
+
+/* search/tags shared code ***************************************************/
+
+/* comparison function used by dedupe() */
+static int wordcmp(const void *a, const void *b) {
+  return strcmp(*(const char **)a, *(const char **)b);
+}
+
+/* sort and de-dupe VEC */
+static char **dedupe(char **vec, int nvec) {
+  int m, n;
+
+  qsort(vec, nvec, sizeof (char *), wordcmp);
+  m = n = 0;
+  if(nvec) {
+    vec[m++] = vec[0];
+    for(n = 1; n < nvec; ++n)
+      if(strcmp(vec[n], vec[m - 1]))
+	vec[m++] = vec[n];
+  }
+  vec[m] = 0;
+  return vec;
+}
+
+/* update a key/track database.  Returns 0 or DB_DEADLOCK. */
+static int register_word(DB *db, const char *what,
+                         const char *track, const char *word,
+                         DB_TXN *tid) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->put(db, tid, make_key(&key, word),
+                       make_key(&data, track), DB_NODUPDATA)) {
+  case 0:
+  case DB_KEYEXIST:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating %s.db: %s", what, db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating %s.db: %s", what,  db_strerror(err));
+  }
+}
+
+/* search primitives *********************************************************/
+
+/* return true iff NAME is a trackname_display_ pref */
+static int is_display_pref(const char *name) {
+  static const char prefix[] = "trackname_display_";
+  return !strncmp(name, prefix, (sizeof prefix) - 1);
+}
+
+/* compute the words of a track name */
+static char **track_to_words(const char *track,
+                             const struct kvp *p) {
+  struct vector v;
+  char **w;
+  int nw;
+
+  vector_init(&v);
+  if((w = words(casefold(strip_extension(track_rootless(track))), &nw)))
+    vector_append_many(&v, w, nw);
+
+  for(; p; p = p->next)
+    if(is_display_pref(p->name))
+      if((w = words(casefold(p->value), &nw)))
+        vector_append_many(&v, w, nw);
+  vector_terminate(&v);
+  return dedupe(v.vec, v.nvec);
+}
+
+/* return nonzero iff WORD is a stopword */
+static int stopword(const char *word) {
+  int n;
+
+  for(n = 0; n < config->stopword.n
+        && strcmp(word, config->stopword.s[n]); ++n)
+    ;
+  return n < config->stopword.n;
+}
+
+/* record that WORD appears in TRACK.  Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_search_word(const char *track, const char *word,
+                                DB_TXN *tid) {
+  if(stopword(word)) return 0;
+  return register_word(trackdb_searchdb, "search", track, word, tid);
+}
+
+/* Tags **********************************************************************/
+
+/* Return nonzero if C is a valid tag character */
+static int tagchar(int c) {
+  switch(c) {
+  case ',':
+    return 0;
+  default:
+    return c >= ' ';
+  }
+}
+
+/* Parse and de-dupe a tag list.  If S=0 then assumes "". */
+static char **parsetags(const char *s) {
+  const char *t;
+  struct vector v;
+
+  vector_init(&v);
+  if(s) {
+    /* skip initial separators */
+    while(*s && (!tagchar(*s) || *s == ' '))
+      ++s;
+    while(*s) {
+      /* find the extent of the tag */
+      t = s;
+      while(*s && tagchar(*s))
+        ++s;
+      /* strip trailing spaces */
+      while(s > t && s[-1] == ' ')
+        --s;
+      vector_append(&v, xstrndup(t, s - t));
+      /* skip intermediate and trailing separators */
+      while(*s && (!tagchar(*s) || *s == ' '))
+        ++s;
+    }
+  }
+  vector_terminate(&v);
+  return dedupe(v.vec, v.nvec);
+}
+
+/* Record that TRACK has TAG.  Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_tag(const char *track, const char *tag, DB_TXN *tid) {
+  return register_word(trackdb_tagsdb, "tags", track, tag, tid);
+}
+
+/* aliases *******************************************************************/
+
+/* compute the alias and store at aliasp.  Returns 0 or DB_LOCK_DEADLOCK.  If
+ * there is no alias sets *aliasp to 0. */
+static int compute_alias(char **aliasp,
+                         const char *track,
+                         const struct kvp *p,
+                         DB_TXN *tid) {
+  struct dynstr d;
+  const char *s = config->alias, *t, *expansion, *part;
+  int c, used_db = 0, slash_prefix, err;
+  struct kvp *at;
+
+  if(strstr(track, "Troggs"))
+    D(("computing alias for %s", track));
+  dynstr_init(&d);
+  dynstr_append_string(&d, find_track_root(track));
+  while((c = (unsigned char)*s++)) {
+    if(c != '{') {
+      dynstr_append(&d, c);
+      continue;
+    }
+    if((slash_prefix = (*s == '/')))
+      s++;
+    t = strchr(s, '}');
+    assert(t != 0);			/* validated at startup */
+    part = xstrndup(s, t - s);
+    expansion = getpart(track, "display", part, p, &used_db);
+    if(*expansion) {
+      if(slash_prefix) dynstr_append(&d, '/');
+      dynstr_append_string(&d, expansion);
+    }
+    s = t + 1;				/* skip {part} */
+  }
+  /* only admit to the alias if we used the db... */
+  if(!used_db) {
+    *aliasp = 0;
+    return 0;
+  }
+  dynstr_terminate(&d);
+  /* ...and the answer differs from the original... */
+  if(!strcmp(track, d.vec)) {
+    *aliasp = 0;
+    return 0;
+  }
+  /* ...and there isn't already a different track with that name (including as
+   * an alias) */
+  switch(err = trackdb_getdata(trackdb_tracksdb, d.vec, &at, tid)) {
+  case 0:
+    if(strstr(track, "Troggs"))
+      D(("found a hit for alias"));
+    if((s = kvp_get(at, "_alias_for"))
+       && !strcmp(s, track)) {
+    case DB_NOTFOUND:
+      if(strstr(track, "Troggs"))
+        D(("accepting anyway"));
+      *aliasp = d.vec;
+    } else {
+      if(strstr(track, "Troggs")) {
+        D(("rejecting"));
+        D(("%s", track));
+        D(("%s", s ? s : "(null)"));
+      }
+      *aliasp = 0;
+    }
+    return 0;
+  default:
+    return err;
+  }
+}
+
+/* get track and prefs data (if tp/pp not null pointers).  Returns 0 on
+ * success, DB_NOTFOUND if the track does not exist or DB_LOCK_DEADLOCK.
+ * Always sets the return values, even if only to null pointers. */
+static int gettrackdata(const char *track,
+                        struct kvp **tp,
+                        struct kvp **pp,
+                        const char **actualp,
+                        unsigned flags,
+#define GTD_NOALIAS 0x0001
+                        DB_TXN *tid) {
+  int err;
+  const char *actual = track;
+  struct kvp *t = 0, *p = 0;
+  
+  if((err = trackdb_getdata(trackdb_tracksdb, track, &t, tid))) goto done;
+  if((actual = kvp_get(t, "_alias_for"))) {
+    if(flags & GTD_NOALIAS) {
+      error(0, "alias passed to gettrackdata where real path required");
+      abort();
+    }
+    if((err = trackdb_getdata(trackdb_tracksdb, actual, &t, tid))) goto done;
+  } else
+    actual = track;
+  assert(actual != 0);
+  if(pp) {
+    if((err = trackdb_getdata(trackdb_prefsdb, actual, &p, tid)) == DB_LOCK_DEADLOCK)
+      goto done;
+  }
+  err = 0;
+done:
+  if(actualp) *actualp = actual;
+  if(tp) *tp = t;
+  if(pp) *pp = p;
+  return err;
+}
+
+/* trackdb_notice() **********************************************************/
+
+/* notice a track */
+int trackdb_notice(const char *track,
+                   const char *path) {
+  int err;
+  DB_TXN *tid;
+  
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    err = trackdb_notice_tid(track, path, tid);
+    if((err == DB_LOCK_DEADLOCK)) goto fail;
+    break;
+  fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return err;
+}
+
+int trackdb_notice_tid(const char *track,
+                       const char *path,
+                       DB_TXN *tid) {
+  int err, n;
+  struct kvp *t, *a, *p;
+  int t_changed, ret;
+  char *alias, **w;
+  
+  /* notice whether the tracks.db entry changes */
+  t_changed = 0;
+  /* get any existing tracks entry */
+  if((err = gettrackdata(track, &t, &p, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+    return err;
+  ret = err;
+  /* this is a real track */
+  t_changed += kvp_set(&t, "_alias_for", 0);
+  t_changed += kvp_set(&t, "_path", path);
+  /* if we have an alias record it in the database */
+  if((err = compute_alias(&alias, track, p, tid))) return err;
+  if(alias) {
+    /* won't overwrite someone else's alias as compute_alias() checks */
+    D(("%s: alias %s", track, alias));
+    a = 0;
+    kvp_set(&a, "_alias_for", track);
+    if((err = trackdb_putdata(trackdb_tracksdb, alias, a, tid, 0))) return err;
+  }
+  /* update search.db */
+  w = track_to_words(track, p);
+  for(n = 0; w[n]; ++n)
+    if((err = register_search_word(track, w[n], tid)))
+      return err;
+  /* update tags.db */
+  w = parsetags(kvp_get(p, "tags"));
+  for(n = 0; w[n]; ++n)
+    if((err = register_tag(track, w[n], tid)))
+      return err;
+  reqtracks = 0;
+  /* only store the tracks.db entry if it has changed */
+  if(t_changed && (err = trackdb_putdata(trackdb_tracksdb, track, t, tid, 0)))
+    return err;
+  return ret;
+}
+
+/* trackdb_obsolete() ********************************************************/
+
+/* obsolete a track */
+int trackdb_obsolete(const char *track, DB_TXN *tid) {
+  int err, n;
+  struct kvp *p;
+  char *alias, **w;
+
+  if((err = gettrackdata(track, 0, &p, 0,
+                         GTD_NOALIAS, tid)) == DB_LOCK_DEADLOCK)
+    return err;
+  else if(err == DB_NOTFOUND) return 0;
+  /* compute the alias, if any, and delete it */
+  if(compute_alias(&alias, track, p, tid)) return err;
+  if(alias) {
+    /* if the alias points to some other track then compute_alias won't
+     * return it */
+    if(trackdb_delkey(trackdb_tracksdb, alias, tid))
+      return err;
+  }
+  /* update search.db */
+  w = track_to_words(track, p);
+  for(n = 0; w[n]; ++n)
+    if(trackdb_delkeydata(trackdb_searchdb,
+                          w[n], track, tid) == DB_LOCK_DEADLOCK)
+      return err;
+  /* update tags.db */
+  w = parsetags(kvp_get(p, "tags"));
+  for(n = 0; w[n]; ++n)
+    if(trackdb_delkeydata(trackdb_tagsdb,
+                          w[n], track, tid) == DB_LOCK_DEADLOCK)
+      return err;
+  reqtracks = 0;
+  /* update tracks.db */
+  if(trackdb_delkey(trackdb_tracksdb, track, tid) == DB_LOCK_DEADLOCK)
+    return err;
+  /* We don't delete the prefs, so they survive temporary outages of the
+   * (possibly virtual) track filesystem */
+  return 0;
+}
+
+/* trackdb_stats() ***********************************************************/
+
+#define H(name) { #name, offsetof(DB_HASH_STAT, name) }
+#define B(name) { #name, offsetof(DB_BTREE_STAT, name) }
+
+static const struct statinfo {
+  const char *name;
+  size_t offset;
+} statinfo_hash[] = {
+  H(hash_magic),
+  H(hash_version),
+  H(hash_nkeys),
+  H(hash_ndata),
+  H(hash_pagesize),
+  H(hash_ffactor),
+  H(hash_buckets),
+  H(hash_free),
+  H(hash_bfree),
+  H(hash_bigpages),
+  H(hash_big_bfree),
+  H(hash_overflows),
+  H(hash_ovfl_free),
+  H(hash_dup),
+  H(hash_dup_free),
+}, statinfo_btree[] = {
+  B(bt_magic),
+  B(bt_version),
+  B(bt_nkeys),
+  B(bt_ndata),
+  B(bt_pagesize),
+  B(bt_minkey),
+  B(bt_re_len),
+  B(bt_re_pad),
+  B(bt_levels),
+  B(bt_int_pg),
+  B(bt_leaf_pg),
+  B(bt_dup_pg),
+  B(bt_over_pg),
+  B(bt_free),
+  B(bt_int_pgfree),
+  B(bt_leaf_pgfree),
+  B(bt_dup_pgfree),
+  B(bt_over_pgfree),
+};
+
+/* look up stats for DB */
+static int get_stats(struct vector *v,
+                     DB *database,
+                     const struct statinfo *si,
+                     size_t nsi,
+                     DB_TXN *tid) {
+  void *sp;
+  size_t n;
+  char *str;
+  int err;
+
+  if(database) {
+    switch(err = database->stat(database, tid, &sp, 0)) {
+    case 0:
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error querying database: %s", db_strerror(err));
+      return err;
+    default:
+      fatal(0, "error querying database: %s", db_strerror(err));
+    }
+    for(n = 0; n < nsi; ++n) {
+      byte_xasprintf(&str, "%s=%"PRIuMAX, si[n].name,
+		     (uintmax_t)*(u_int32_t *)((char *)sp + si[n].offset));
+      vector_append(v, str);
+    }
+  }
+  return 0;
+}
+
+struct search_entry {
+  char *word;
+  int n;
+};
+
+/* find the top COUNT words in the search database */
+static int search_league(struct vector *v, int count, DB_TXN *tid) {
+  struct search_entry *se;
+  DBT k, d;
+  DBC *cursor;
+  int err, n = 0, nse = 0, i;
+  char *word = 0;
+  size_t wl = 0;
+  char *str;
+
+  cursor = trackdb_opencursor(trackdb_searchdb, tid);
+  se = xmalloc(count * sizeof *se);
+  while(!(err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                              DB_NEXT))) {
+    if(word && wl == k.size && !strncmp(word, k.data, wl))
+      ++n;
+    else {
+#define FINALIZE() do {						\
+  if(word && (nse < count || n > se[nse - 1].n)) {		\
+    if(nse == count)						\
+      i = nse - 1;						\
+    else							\
+      i = nse++;						\
+    while(i > 0 && n > se[i - 1].n)				\
+      --i;							\
+    memmove(&se[i + 1], &se[i], (nse - i) * sizeof *se);	\
+    se[i].word = word;						\
+    se[i].n = n;						\
+  }								\
+} while(0)
+      FINALIZE();
+      word = xstrndup(k.data, wl = k.size);
+      n = 1;
+    }
+  }
+  switch(err) {
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying search database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error querying search database: %s", db_strerror(err));
+  }
+  if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+  if(err) return err;
+  FINALIZE();
+  byte_xasprintf(&str, "Top %d search words:", nse);
+  vector_append(v, str);
+  for(i = 0; i < nse; ++i) {
+    byte_xasprintf(&str, "%4d: %5d %s", i + 1, se[i].n, se[i].word);
+    vector_append(v, str);
+  }
+  return 0;
+}
+
+#define SI(what) statinfo_##what, \
+                 sizeof statinfo_##what / sizeof (struct statinfo)
+
+/* return a list of database stats */
+char **trackdb_stats(int *nstatsp) {
+  DB_TXN *tid;
+  struct vector v;
+  char *s;
+  
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    v.nvec = 0;
+    vector_append(&v, (char *)"Tracks database stats:");
+    if(get_stats(&v, trackdb_tracksdb, SI(btree), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Search database stats:");
+    if(get_stats(&v, trackdb_searchdb, SI(hash), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Prefs database stats:");
+    if(get_stats(&v, trackdb_prefsdb, SI(hash), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    if(search_league(&v, 10, tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Server stats:");
+    byte_xasprintf(&s, "track lookup cache hits: %lu", cache_files_hits);
+    vector_append(&v, (char *)s);
+    byte_xasprintf(&s, "track lookup cache misses: %lu", cache_files_misses);
+    vector_append(&v, (char *)s);
+    vector_terminate(&v);
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(nstatsp) *nstatsp = v.nvec;
+  return v.vec;
+}
+
+/* set a pref (remove if value=0) */
+int trackdb_set(const char *track,
+                const char *name,
+                const char *value) {
+  struct kvp *t, *p, *a;
+  DB_TXN *tid;
+  int err, cmp;
+  char *oldalias, *newalias, **oldtags = 0, **newtags;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = gettrackdata(track, &t, &p, 0,
+                           0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    if(err == DB_NOTFOUND) break;
+    if(name[0] == '_') {
+      if(kvp_set(&t, name, value))
+        if(trackdb_putdata(trackdb_tracksdb, track, t, tid, 0))
+          goto fail;
+    } else {
+      /* get the old alias name */
+      if(compute_alias(&oldalias, track, p, tid)) goto fail;
+      /* get the old tags */
+      if(!strcmp(name, "tags"))
+        oldtags = parsetags(kvp_get(p, "tags"));
+      /* set the value */
+      if(kvp_set(&p, name, value))
+        if(trackdb_putdata(trackdb_prefsdb, track, p, tid, 0))
+          goto fail;
+      /* compute the new alias name */
+      if((err = compute_alias(&newalias, track, p, tid))) goto fail;
+      /* check whether alias has changed */
+      if(!(oldalias == newalias
+           || (oldalias && newalias && !strcmp(oldalias, newalias)))) {
+        /* adjust alias records to fit change */
+        if(oldalias
+           && trackdb_delkey(trackdb_tracksdb, oldalias, tid)) goto fail;
+        if(newalias) {
+          a = 0;
+          kvp_set(&a, "_alias_for", track);
+          if(trackdb_putdata(trackdb_tracksdb, newalias, a, tid, 0)) goto fail;
+        }
+      }
+      /* check whether tags have changed */
+      if(!strcmp(name, "tags")) {
+        newtags = parsetags(value);
+        while(*oldtags || *newtags) {
+          if(*oldtags && *newtags) {
+            cmp = strcmp(*oldtags, *newtags);
+            if(!cmp) {
+              /* keeping this tag */
+              ++oldtags;
+              ++newtags;
+            } else if(cmp < 0)
+              /* old tag fits into a gap in the new list, so delete old */
+              goto delete_old;
+            else
+              /* new tag fits into a gap in the old list, so insert new */
+              goto insert_new;
+          } else if(*oldtags) {
+            /* we've run out of new tags, so remaining old ones are to be
+             * deleted */
+          delete_old:
+            if(trackdb_delkeydata(trackdb_tagsdb,
+                                  *oldtags, track, tid) == DB_LOCK_DEADLOCK)
+              goto fail;
+            ++oldtags;
+          } else {
+            /* we've run out of old tags, so remainig new ones are to be
+             * inserted */
+          insert_new:
+            if(register_tag(track, *newtags, tid)) goto fail;
+            ++newtags;
+          }
+        }
+        reqtracks = 0;
+      }
+    }
+    err = 0;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return err == 0 ? 0 : -1;
+}
+
+/* get a pref */
+const char *trackdb_get(const char *track,
+                        const char *name) {
+  return kvp_get(trackdb_get_all(track), name);
+}
+
+/* get all prefs as a 0-terminated array */
+struct kvp *trackdb_get_all(const char *track) {
+  struct kvp *t, *p, **pp;
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  for(pp = &p; *pp; pp = &(*pp)->next)
+    ;
+  *pp = t;
+  return p;
+}
+
+/* resolve alias */
+const char *trackdb_resolve(const char *track) {
+  DB_TXN *tid;
+  const char *actual;
+  
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, 0, 0, &actual, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return actual;
+}
+
+int trackdb_isalias(const char *track) {
+  const char *actual = trackdb_resolve(track);
+
+  return strcmp(actual, track);
+}
+
+/* test whether a track exists (perhaps an alias) */
+int trackdb_exists(const char *track) {
+  DB_TXN *tid;
+  int err;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    /* unusually, here we want the return value */
+    if((err = gettrackdata(track, 0, 0, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return (err == 0);
+}
+
+/* return the list of tags */
+char **trackdb_alltags(void) {
+  DB_TXN *tid;
+  int err;
+  char **taglist;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    err = trackdb_alltags_tid(tid, &taglist);
+    if(!err) break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return taglist;
+}
+
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp) {
+  struct vector v;
+  DBC *c;
+  DBT k, d;
+  int err;
+
+  vector_init(&v);
+  c = trackdb_opencursor(trackdb_tagsdb, tid);
+  memset(&k, 0, sizeof k);
+  while(!(err = c->c_get(c, &k, prepare_data(&d), DB_NEXT_NODUP)))
+    vector_append(&v, xstrndup(k.data, k.size));
+  switch(err) {
+  case DB_NOTFOUND:
+    break;
+  case DB_LOCK_DEADLOCK:
+      return err;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+  if((err = trackdb_closecursor(c))) return err;
+  vector_terminate(&v);
+  *taglistp = v.vec;
+  return 0;
+}
+
+/* return 1 iff sorted tag lists A and B have at least one member in common */
+static int tag_intersection(char **a, char **b) {
+  int cmp;
+
+  /* Same sort of logic as trackdb_set() above */
+  while(*a && *b) {
+    if(!(cmp = strcmp(*a, *b))) return 1;
+    else if(cmp < 0) ++a;
+    else ++b;
+  }
+  return 0;
+}
+
+/* Check whether a track is suitable for random play.  Returns 0 if it is,
+ * DB_NOTFOUND if it or DB_LOCK_DEADLOCK. */
+static int check_suitable(const char *track,
+                          DB_TXN *tid,
+                          char **required_tags,
+                          char **prohibited_tags) {
+  char **track_tags;
+  time_t last, now;
+  struct kvp *p, *t;
+  const char *pick_at_random, *played_time;
+
+  /* don't pick aliases - only pick the canonical form */
+  if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+    return DB_LOCK_DEADLOCK;
+  if(kvp_get(t, "_alias_for"))
+    return DB_NOTFOUND;
+  /* check that random play is not suppressed for this track */
+  if((pick_at_random = kvp_get(p, "pick_at_random"))
+     && !strcmp(pick_at_random, "0"))
+    return DB_NOTFOUND;
+  /* don't pick a track that's been played in the last 8 hours */
+  if((played_time = kvp_get(p, "played_time"))) {
+    last = atoll(played_time);
+    now = time(0);
+    if(now < last + 8 * 3600)       /* TODO configurable */
+      return DB_NOTFOUND;
+  }
+  track_tags = parsetags(kvp_get(p, "tags"));
+  /* check that no prohibited tag is present for this track */
+  if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
+    return DB_NOTFOUND;
+  /* check that at least one required tags is present for this track */
+  if(*required_tags && !tag_intersection(track_tags, required_tags))
+    return DB_NOTFOUND;
+  return 0;
+}
+
+/* attempt to pick a random non-alias track */
+const char *trackdb_random(int tries) {
+  DBT key, data;
+  DB_BTREE_STAT *sp;
+  int err, n;
+  DB_TXN *tid;
+  const char *track, *candidate;
+  db_recno_t r;
+  const char *tags;
+  char **required_tags, **prohibited_tags, **tp;
+  hash *h;
+  DBC *c = 0;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = trackdb_get_global_tid("required-tags", tid, &tags)))
+      goto fail;
+    required_tags = parsetags(tags);
+    if((err = trackdb_get_global_tid("prohibited-tags", tid, &tags)))
+      goto fail;
+    prohibited_tags = parsetags(tags);
+    track = 0;
+    if(*required_tags) {
+      /* Bung all the suitable tracks into a hash and convert to a list of keys
+       * (to eliminate duplicates).  We cache this list since it is possible
+       * that it will be very large. */
+      if(!reqtracks) {
+        h = hash_new(0);
+        for(tp = required_tags; *tp; ++tp) {
+          c = trackdb_opencursor(trackdb_tagsdb, tid);
+          memset(&key, 0, sizeof key);
+          key.data = *tp;
+          key.size = strlen(*tp);
+          n = 0;
+          err = c->c_get(c, &key, prepare_data(&data), DB_SET);
+          while(err == 0) {
+            hash_add(h, xstrndup(data.data, data.size), 0,
+                     HASH_INSERT_OR_REPLACE);
+            ++n;
+            err = c->c_get(c, &key, prepare_data(&data), DB_NEXT_DUP);
+          }
+          switch(err) {
+          case 0:
+          case DB_NOTFOUND:
+            break;
+          case DB_LOCK_DEADLOCK:
+            goto fail;
+          default:
+            fatal(0, "error querying tags.db: %s", db_strerror(err));
+          }
+          trackdb_closecursor(c);
+          c = 0;
+          if(!n)
+            error(0, "required tag %s does not match any tracks", *tp);
+        }
+        nreqtracks = hash_count(h);
+        reqtracks = hash_keys(h);
+      }
+      while(nreqtracks && !track && tries-- > 0) {
+        r = (rand() * (double)nreqtracks / (RAND_MAX + 1.0));
+        candidate = reqtracks[r];
+        switch(check_suitable(candidate, tid,
+                              required_tags, prohibited_tags)) {
+        case 0:
+          track = candidate;
+          break;
+        case DB_NOTFOUND:
+          break;
+        case DB_LOCK_DEADLOCK:
+          goto fail;
+        }
+      }
+    } else {
+      /* No required tags.  We pick random record numbers in the database
+       * instead. */
+      switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp,
+                                          DB_RECORDCOUNT)) {
+      case 0:
+        break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "error querying tracks.db: %s", db_strerror(err));
+        goto fail;
+      default:
+        fatal(0, "error querying tracks.db: %s", db_strerror(err));
+      }
+      if(!sp->bt_nkeys)
+        error(0, "cannot pick tracks at random from an empty database");
+      while(sp->bt_nkeys && !track && tries-- > 0) {
+        /* record numbers count from 1 upwards */
+        r = 1 + (rand() * (double)sp->bt_nkeys / (RAND_MAX + 1.0));
+        memset(&key, sizeof key, 0);
+        key.flags = DB_DBT_MALLOC;
+        key.size = sizeof r;
+        key.data = &r;
+        switch(err = trackdb_tracksdb->get(trackdb_tracksdb, tid, &key, prepare_data(&data),
+                                           DB_SET_RECNO)) {
+        case 0:
+          break;
+        case DB_LOCK_DEADLOCK:
+          error(0, "error querying tracks.db: %s", db_strerror(err));
+          goto fail;
+        default:
+          fatal(0, "error querying tracks.db: %s", db_strerror(err));
+        }
+        candidate = xstrndup(key.data, key.size);
+        switch(check_suitable(candidate, tid,
+                              required_tags, prohibited_tags)) {
+        case 0:
+          track = candidate;
+          break;
+        case DB_NOTFOUND:
+          break;
+        case DB_LOCK_DEADLOCK:
+          goto fail;
+        }
+      }
+    }
+    break;
+fail:
+    trackdb_closecursor(c);
+    c = 0;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(!track)
+    error(0, "could not pick a random track");
+  return track;
+}
+
+/* get a track name given the prefs.  Set *used_db to 1 if we got the answer
+ * from the prefs. */
+static const char *getpart(const char *track,
+                           const char *context,
+                           const char *part,
+                           const struct kvp *p,
+                           int *used_db) {
+  const char *result;
+  char *pref;
+
+  byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+  if((result = kvp_get(p, pref)))
+    *used_db = 1;
+  else
+    result = trackname_part(track, context, part);
+  assert(result != 0);
+  return result;
+}
+
+/* get a track name part, like trackname_part(), but taking the database into
+ * account. */
+const char *trackdb_getpart(const char *track,
+                            const char *context,
+                            const char *part) {
+  struct kvp *p;
+  DB_TXN *tid;
+  char *pref;
+  const char *actual;
+  int used_db, err;
+
+  /* construct the full pref */
+  byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = gettrackdata(track, 0, &p, &actual, 0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return getpart(actual, context, part, p, &used_db);
+}
+
+/* get the raw path name for @track@ (might be an alias) */
+const char *trackdb_rawpath(const char *track) {
+  DB_TXN *tid;
+  struct kvp *t;
+  const char *path;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, &t, 0, 0, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(!(path = kvp_get(t, "_path"))) path = track;
+  return path;
+}
+
+/* trackdb_list **************************************************************/
+
+/* this is incredibly ugly, sorry, perhaps it will be rewritten to be actually
+ * readable at some point */
+
+/* return true if the basename of TRACK[0..TL-1], as defined by DL, matches RE.
+ * If RE is a null pointer then it matches everything. */
+static int track_matches(size_t dl, const char *track, size_t tl,
+			 const pcre *re) {
+  int ovec[3], rc;
+
+  if(!re)
+    return 1;
+  track += dl + 1;
+  tl -= (dl + 1);
+  switch(rc = pcre_exec(re, 0, track, tl, 0, 0, ovec, 3)) {
+  case PCRE_ERROR_NOMATCH: return 0;
+  default:
+    if(rc < 0) {
+      error(0, "pcre_exec returned %d, subject '%s'", rc, track);
+      return 0;
+    }
+    return 1;
+  }
+}
+
+static int do_list(struct vector *v, const char *dir,
+                   enum trackdb_listable what, const pcre *re, DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  size_t dl;
+  char *ptr;
+  int err;
+  size_t l, last_dir_len = 0;
+  char *last_dir = 0, *track, *alias;
+  struct kvp *p;
+  
+  dl = strlen(dir);
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  make_key(&k, dir);
+  prepare_data(&d);
+  /* find the first key >= dir */
+  err = cursor->c_get(cursor, &k, &d, DB_SET_RANGE);
+  /* keep going while we're dealing with  */
+  while(err == 0
+	&& k.size > dl
+	&& ((char *)k.data)[dl] == '/'
+	&& !memcmp(k.data, dir, dl)) {
+    ptr = memchr((char *)k.data + dl + 1, '/', k.size - (dl + 1));
+    if(ptr) {
+      /* we have , so  is a directory */
+      l = ptr - (char *)k.data;
+      if(what & trackdb_directories)
+	if(!(last_dir
+	     && l == last_dir_len
+	     && !memcmp(last_dir, k.data, l))) {
+	  last_dir = xstrndup(k.data, last_dir_len = l);
+	  if(track_matches(dl, k.data, l, re))
+	    vector_append(v, last_dir);
+	}
+    } else {
+      /* found a plain file */
+      if((what & trackdb_files)) {
+	track = xstrndup(k.data, k.size);
+        if((err = trackdb_getdata(trackdb_prefsdb,
+                                  track, &p, tid)) == DB_LOCK_DEADLOCK)
+          goto deadlocked;
+	/* if this file has an alias in the same directory then we skip it */
+        if((err = compute_alias(&alias, track, p, tid)))
+          goto deadlocked;
+        if(!(alias && !strcmp(d_dirname(alias), d_dirname(track))))
+	  if(track_matches(dl, k.data, k.size, re))
+	    vector_append(v, track);
+      }
+    }
+    err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+  }
+  switch(err) {
+  case 0:
+    break;
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error querying database: %s", db_strerror(err));
+  }
+deadlocked:
+  if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* return the directories or files below @dir@ */
+char **trackdb_list(const char *dir, int *np, enum trackdb_listable what,
+                    const pcre *re) {
+  DB_TXN *tid;
+  int n;
+  struct vector v;
+
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    v.nvec = 0;
+    if(dir) {
+      if(do_list(&v, dir, what, re, tid))
+        goto fail;
+    } else {
+      for(n = 0; n < config->collection.n; ++n)
+        if(do_list(&v, config->collection.s[n].root, what, re, tid))
+          goto fail;
+    }
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  vector_terminate(&v);
+  if(np)
+    *np = v.nvec;
+  return v.vec;
+}  
+
+/* If S is tag:something, return something.  Else return 0. */
+static const char *checktag(const char *s) {
+  if(!strncmp(s, "tag:", 4))
+    return s + 4;
+  else
+    return 0;
+}
+
+/* return a list of tracks containing all of the words given.  If you
+ * ask for only stopwords you get no tracks. */
+char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
+  const char **w, *best = 0, *tag;
+  char **twords, **tags;
+  int i, j, n, err, what;
+  DBC *cursor = 0;
+  DBT k, d;
+  struct vector u, v;
+  DB_TXN *tid;
+  struct kvp *p;
+  int ntags = 0;
+  DB *db;
+  const char *dbname;
+
+  *ntracks = 0;				/* for early returns */
+  /* casefold all the words */
+  w = xmalloc(nwordlist * sizeof (char *));
+  for(n = 0; n < nwordlist; ++n) {
+    w[n] = casefold(wordlist[n]);
+    if(checktag(w[n])) ++ntags;         /* count up tags */
+  }
+  /* find the longest non-stopword */
+  for(n = 0; n < nwordlist; ++n)
+    if(!stopword(w[n]) && !checktag(w[n]))
+      if(!best || strlen(w[n]) > strlen(best))
+	best = w[n];
+  /* TODO: we should at least in principal be able to identify the word or tag
+   * with the least matches in log time, and choose that as our primary search
+   * term. */
+  if(ntags && !best) {
+    /* Only tags are listed.  We limit to the first and narrow down with the
+     * rest. */
+    best = checktag(w[0]);
+    db = trackdb_tagsdb;
+    dbname = "tags";
+  } else if(best) {
+    /* We can limit to some word. */
+    db = trackdb_searchdb;
+    dbname = "search";
+  } else {
+    /* Only stopwords */
+    return 0;
+  }
+  vector_init(&u);
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    /* find all the tracks that have that word */
+    make_key(&k, best);
+    prepare_data(&d);
+    what = DB_SET;
+    v.nvec = 0;
+    cursor = trackdb_opencursor(db, tid);
+    while(!(err = cursor->c_get(cursor, &k, &d, what))) {
+      vector_append(&v, xstrndup(d.data, d.size));
+      what = DB_NEXT_DUP;
+    }
+    switch(err) {
+    case DB_NOTFOUND:
+      err = 0;
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error querying %s database: %s", dbname, db_strerror(err));
+      break;
+    default:
+      fatal(0, "error querying %s database: %s", dbname, db_strerror(err));
+    }
+    if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+    cursor = 0;
+    /* do a naive search over that (hopefuly fairly small) list of tracks */
+    u.nvec = 0;
+    for(n = 0; n < v.nvec; ++n) {
+      if((err = gettrackdata(v.vec[n], 0, &p, 0, 0, tid) == DB_LOCK_DEADLOCK))
+        goto fail;
+      else if(err) {
+        error(0, "track %s unexpected error: %s", v.vec[n], db_strerror(err));
+        continue;
+      }
+      twords = track_to_words(v.vec[n], p);
+      tags = parsetags(kvp_get(p, "tags"));
+      for(i = 0; i < nwordlist; ++i) {
+        if((tag = checktag(w[i]))) {
+          /* Track must have this tag */
+          for(j = 0; tags[j]; ++j)
+            if(!strcmp(tag, tags[j])) break; /* tag found */
+          if(!tags[j]) break;           /* tag not found */
+        } else {
+          /* Track must contain this word */
+          for(j = 0; twords[j]; ++j)
+            if(!strcmp(w[i], twords[j])) break; /* word found */
+          if(!twords[j]) break;		/* word not found */
+        }
+      }
+      if(i >= nwordlist)                /* all words found */
+        vector_append(&u, v.vec[n]);
+    }
+    break;
+  fail:
+    trackdb_closecursor(cursor);
+    cursor = 0;
+    trackdb_abort_transaction(tid);
+    info("retrying search");
+  }
+  trackdb_commit_transaction(tid);
+  vector_terminate(&u);
+  if(ntracks)
+    *ntracks = u.nvec;
+  return u.vec;
+}
+
+/* trackdb_scan **************************************************************/
+
+int trackdb_scan(const char *root,
+                 int (*callback)(const char *track,
+                                 struct kvp *data,
+                                 void *u,
+                                 DB_TXN *tid),
+                 void *u,
+                 DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  size_t root_len = strlen(root);
+  int err;
+  struct kvp *data;
+
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  err = cursor->c_get(cursor, make_key(&k, root), prepare_data(&d),
+                      DB_SET_RANGE);
+  while(!err) {
+    if(k.size > root_len
+       && !strncmp(k.data, root, root_len)
+       && ((char *)k.data)[root_len] == '/') {
+      data = kvp_urldecode(d.data, d.size);
+      if(kvp_get(data, "_path"))
+        if((err = callback(xstrndup(k.data, k.size), data, u, tid)))
+          break;
+      err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+    } else
+      break;
+  }
+  trackdb_closecursor(cursor);
+  switch(err) {
+  case EINTR:
+    return err;
+  case 0:
+  case DB_NOTFOUND:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "c->c_get: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+}
+
+/* trackdb_rescan ************************************************************/
+
+/* called when the rescanner terminates */
+static int reap_rescan(ev_source attribute((unused)) *ev,
+                       pid_t pid,
+                       int status,
+                       const struct rusage attribute((unused)) *rusage,
+                       void attribute((unused)) *u) {
+  if(pid == rescan_pid) rescan_pid = -1;
+  if(status)
+    error(0, "disorderd-rescan: %s", wstat(status));
+  else
+    D(("disorderd-rescan terminate: %s", wstat(status)));
+  /* Our cache of file lookups is out of date now */
+  cache_clean(&cache_files_type);
+  return 0;
+}
+
+void trackdb_rescan(ev_source *ev) {
+  if(rescan_pid != -1) {
+    error(0, "rescan already underway");
+    return;
+  }
+  rescan_pid = subprogram(ev, RESCAN);
+  ev_child(ev, rescan_pid, 0, reap_rescan, 0);
+  D(("started rescanner"));
+  
+}
+
+int trackdb_rescan_cancel(void) {
+  if(rescan_pid == -1) return 0;
+  if(kill(rescan_pid, SIGTERM) < 0)
+    fatal(errno, "error killing rescanner");
+  rescan_pid = -1;
+  return 1;
+}
+
+/* global prefs **************************************************************/
+
+void trackdb_set_global(const char *name,
+                        const char *value,
+                        const char *who) {
+  DB_TXN *tid;
+  DBT k, d;
+  int err;
+  int state;
+
+  memset(&k, 0, sizeof k);
+  memset(&d, 0, sizeof d);
+  k.data = (void *)name;
+  k.size = strlen(name);
+  if(value) {
+    d.data = (void *)value;
+    d.size = strlen(value);
+  }
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(value)
+      err = trackdb_globaldb->put(trackdb_globaldb, tid, &k, &d, 0);
+    else
+      err = trackdb_globaldb->del(trackdb_globaldb, tid, &k, 0);
+    if(!err || err == DB_NOTFOUND) break;
+    if(err != DB_LOCK_DEADLOCK)
+      fatal(0, "error updating database: %s", db_strerror(err));
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  /* log important state changes */
+  if(!strcmp(name, "playing")) {
+    state = !value || !strcmp(value, "yes");
+    info("playing %s by %s",
+         state ? "enabled" : "disabled",
+         who ? who : "-");
+    eventlog("state", state ? "enable_play" : "disable_play", (char *)0);
+  }
+  if(!strcmp(name, "random-play")) {
+    state = !value || !strcmp(value, "yes");
+    info("random play %s by %s",
+         state ? "enabled" : "disabled",
+         who ? who : "-");
+    eventlog("state", state ? "enable_random" : "disable_random", (char *)0);
+  }
+  if(!strcmp(name, "required-tags"))
+    reqtracks = 0;
+}
+
+const char *trackdb_get_global(const char *name) {
+  DB_TXN *tid;
+  int err;
+  const char *r;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(!(err = trackdb_get_global_tid(name, tid, &r)))
+      break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return r;
+}
+
+static int trackdb_get_global_tid(const char *name,
+                                  DB_TXN *tid,
+                                  const char **rp) {
+  DBT k, d;
+  int err;
+
+  memset(&k, 0, sizeof k);
+  k.data = (void *)name;
+  k.size = strlen(name);
+  switch(err = trackdb_globaldb->get(trackdb_globaldb, tid, &k,
+                                     prepare_data(&d), 0)) {
+  case 0:
+    *rp = xstrndup(d.data, d.size);
+    return 0;
+  case DB_NOTFOUND:
+    *rp = 0;
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* tidying up ****************************************************************/
+
+void trackdb_gc(void) {
+  int err;
+  char **logfiles;
+
+  if((err = trackdb_env->txn_checkpoint(trackdb_env,
+                                        config->checkpoint_kbyte,
+                                        config->checkpoint_min,
+                                        0)))
+    fatal(0, "trackdb_env->txn_checkpoint: %s", db_strerror(err));
+  if((err = trackdb_env->log_archive(trackdb_env, &logfiles, DB_ARCH_REMOVE)))
+    fatal(0, "trackdb_env->log_archive: %s", db_strerror(err));
+  /* This makes catastrophic recovery impossible.  However, the user can still
+   * preserve the important data by using disorder-dump to snapshot their
+   * prefs, and later to restore it.  This is likely to have much small
+   * long-term storage requirements than record the db logfiles. */
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:/CBQ6uebrasgkefvjc+fHQ */
diff --git a/server/trackdb.h b/server/trackdb.h
new file mode 100644
index 0000000..54e7214
--- /dev/null
+++ b/server/trackdb.h
@@ -0,0 +1,130 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef TRACKDB_H
+#define TRACKDB_H
+
+struct ev_source;
+
+extern const struct cache_type cache_files_type;
+extern unsigned long cache_files_hits, cache_files_misses;
+/* Cache entry type and tracking for regexp-based lookups */
+
+void trackdb_init(int recover);
+#define TRACKDB_NO_RECOVER 0
+#define TRACKDB_NORMAL_RECOVER 1
+#define TRACKDB_FATAL_RECOVER 2
+void trackdb_deinit(void);
+/* close/close environment */
+
+void trackdb_master(struct ev_source *ev);
+/* start deadlock manager */
+
+void trackdb_open(void);
+void trackdb_close(void);
+/* open/close track databases */
+
+char **trackdb_stats(int *nstatsp);
+/* return a list of database stats */
+
+int trackdb_set(const char *track,
+                const char *name,
+                const char *value);
+/* set a pref (remove if value=0).  Return 0 t */
+
+const char *trackdb_get(const char *track,
+                        const char *name);
+/* get a pref */
+
+struct kvp *trackdb_get_all(const char *track);
+/* get all prefs */
+
+const char *trackdb_resolve(const char *track);
+/* resolve alias - returns a null pointer if not found */
+
+int trackdb_isalias(const char *track);
+/* return true if TRACK is an alias */
+
+int trackdb_exists(const char *track);
+/* test whether a track exists (perhaps an alias) */
+
+const char *trackdb_random(int tries);
+/* Pick a random non-alias track, making at most TRIES attempts.  Returns a
+ * null pointer on failure. */
+
+char **trackdb_alltags(void);
+/* Return the list of all tags */
+
+const char *trackdb_getpart(const char *track,
+                            const char *context,
+                            const char *part);
+/* get a track name part, like trackname_part(), but taking the database into
+ * account. */
+
+const char *trackdb_rawpath(const char *track);
+/* get the raw path name for TRACK (might be an alias); returns a null pointer
+ * if not found. */
+
+enum trackdb_listable {
+  trackdb_files = 1,
+  trackdb_directories = 2
+};
+
+char **trackdb_list(const char *dir, int *np, enum trackdb_listable what,
+                    const pcre *rec);
+/* Return the directories and/or files below DIR.  If DIR is a null pointer
+ * then concatenate the listing of all collections.
+ *
+ * If REC is not a null pointer then only names where the basename matches the
+ * regexp are returned.
+ */
+
+char **trackdb_search(char **wordlist, int nwordlist, int *ntracks);
+/* return a list of tracks containing all of the words given.  If you
+ * ask for only stopwords you get no tracks. */
+
+void trackdb_rescan(struct ev_source *ev);
+/* Start a rescan, if one is not running already */
+
+int trackdb_rescan_cancel(void);
+/* interrupt any running rescan.  Return 1 if one was running, else 0. */
+
+void trackdb_gc(void);
+/* tidy up old database log files */
+
+void trackdb_set_global(const char *name,
+                        const char *value,
+                        const char *who);
+/* set a global pref (remove if value=0). */
+
+const char *trackdb_get_global(const char *name);
+/* get a global pref */
+
+#endif /* TRACKDB_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:Y8z+2jDRros3Nz67LFBlzA */
diff --git a/server/trackname.c b/server/trackname.c
new file mode 100644
index 0000000..a24e3bf
--- /dev/null
+++ b/server/trackname.c
@@ -0,0 +1,96 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "trackname.h"
+#include "mem.h"
+#include "charset.h"
+#include "defs.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  trackname [OPTIONS] TRACK CONTEXT PART\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  const char *s;
+
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:d", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(argc - optind < 3) fatal(0, "not enough arguments");
+  if(argc - optind > 3) fatal(0, "too many arguments");
+  if(config_read()) fatal(0, "cannot read configuration");
+  s = trackname_part(argv[optind], argv[optind+1], argv[optind+2]);
+  if(!s) fatal(0, "trackname_part returned NULL");
+  xprintf("%s\n", nullcheck(utf82mb(s)));
+  if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:DiPOUeRLYf0fgoUqzejUDA */
diff --git a/sounds/Makefile.am b/sounds/Makefile.am
new file mode 100644
index 0000000..cc07db8
--- /dev/null
+++ b/sounds/Makefile.am
@@ -0,0 +1,24 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+pkgdata_DATA=slap.ogg scratch.ogg
+
+EXTRA_DIST=${pkgdata_DATA}
+# arch-tag:cd42d893e1fce039edeb219309bb4796
diff --git a/sounds/scratch.ogg b/sounds/scratch.ogg
new file mode 100644
index 0000000..05c0913
Binary files /dev/null and b/sounds/scratch.ogg differ
diff --git a/sounds/slap.ogg b/sounds/slap.ogg
new file mode 100644
index 0000000..fc4038c
Binary files /dev/null and b/sounds/slap.ogg differ
diff --git a/templates/Makefile.am b/templates/Makefile.am
new file mode 100644
index 0000000..2614895
--- /dev/null
+++ b/templates/Makefile.am
@@ -0,0 +1,31 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005, 2006 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
+	     stdhead.html stylesheet.html search.html about.html volume.html \
+	     sidebar.html prefs.html help.html choosealpha.html topbar.html \
+	     sidebarend.html topbarend.html error.html \
+	     options options.labels \
+	     options.columns
+static_DATA=disorder.css
+staticdir=${pkgdatadir}/static
+
+EXTRA_DIST=${pkgdata_DATA} $(static_DATA)
+# arch-tag:c04ecae39c46f7a88463e746b857d913
diff --git a/templates/about.html b/templates/about.html
new file mode 100644
index 0000000..5688238
--- /dev/null
+++ b/templates/about.html
@@ -0,0 +1,75 @@
+
+
+
+ 
+@include:stdhead@
+  @label:about.title@
+ 
+ 
+@include{@label{menu}@}@
+   

@label:about.title@

+ +

Copyright

+ +

DisOrder + version @version@ - select and play digital + audio files

+ +

Copyright © 2003, 2004, 2005, 2006 Richard Kettlewell

+ +

Portions extracted from + MPG321, + Copyright © 2001 Joe Drew, + Copyright © 2000-2001 Robert Leslie

+ +

This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details.

+ +

You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + USA

+ +

Server Statistics

+ +@stats@ + +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/choose.html b/templates/choose.html new file mode 100644 index 0000000..0475be6 --- /dev/null +++ b/templates/choose.html @@ -0,0 +1,92 @@ + + + + +@include:stdhead@ + @label:choose.title@ + + +@include{@label{menu}@}@ +

@label:choose.title@

+ + @if{@ne{@arg:directory@}{}@}{ +

@navigate{@arg:directory@}{/@basename@}@:

+ }@ + + @if{@isdirectories@}{ +
+

+ @label:choose.directories@ +

+ @choose{directories}{ +

+ + @transform{@file@}{dir}{display}@ + +

+ }@ +
+ }@ + @if{@isfiles@}{ +
+

+ @label:choose.files@ +

+ @choose{files}{ +

+ + @label:choose.prefs@ + + + @transform{@file@}{track}{display}@ + + @if{@eq{@trackstate{@file@}@}{playing}@}{[playing]}@ + @if{@eq{@trackstate{@file@}@}{queued}@}{[queued]}@ +

+ }@ +
+

+ + @label:choose.allprefs@ + + + @label:choose.playall@ + +

+ }@ + +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/choosealpha.html b/templates/choosealpha.html new file mode 100644 index 0000000..be61fd9 --- /dev/null +++ b/templates/choosealpha.html @@ -0,0 +1,72 @@ + + + + +@include:stdhead@ + @label:choose.title@ + + +@include{@label{menu}@}@ +

@label:choose.title@

+ +

+ A | + B | + C | + D | + E | + F | + G | + H | + I | + J | + K | + L | + M | + N | + O | + P | + Q | + R | + S | + T | + U | + V | + W | + X | + Y | + Z | + * +

+ +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/credits.html b/templates/credits.html new file mode 100644 index 0000000..ee0fe08 --- /dev/null +++ b/templates/credits.html @@ -0,0 +1,25 @@ +

DisOrder +version @version@ © 2003, 2004, 2005, 2006 Richard Kettlewell

+@@ + + diff --git a/templates/disorder.css b/templates/disorder.css new file mode 100644 index 0000000..8a42b45 --- /dev/null +++ b/templates/disorder.css @@ -0,0 +1,473 @@ +/* default colors */ +body { + color: black; + background-color: white +} + +/* general link colors */ +a:link { + color: blue +} + +a:visited { + color: blue +} + +a:active { + color: red +} + +h1.title { + font-family: sans-serif; + font-weight: bold; + text-align: center; + font-size: 18pt +} + +/* playing and recent *********************************************************/ + +/* table of current and future tracks */ +table.playing { + width: 100%; /* use the full available width */ + border-spacing: 0 /* no unsightly gaps between cells */ +} + +/* table of recently played tracks */ +table.recent { + width: 100%; /* use the full available width */ + border-spacing: 0 /* no unsightly gaps between cells */ +} + +/* titles in tables */ +th { + text-align: left +} + +/* ordinary cells in tables */ +td { + vertical-align: center +} + +/* the headings of the table */ +tr.headings { + background-color: black; + color: white +} + +/* The 'now playing' heading */ +tr.nowplaying { +} + +td.nowplaying { + background-color: #d0d0d0; + font-weight: bold; + text-align: center +} + +/* the currently playing track */ +tr.playing { + background-color: #e0ffe0 /* pastel green */ +} + +/* the "next" heading */ +tr.next { +} + +td.next { + background-color: #d0d0d0; + font-weight: bold; + text-align: center +} + +/* even-numbered rows */ +tr.even { + background-color: #ffecec /* faint pastel red */ +} + +/* odd-numbered rows */ +tr.odd { + background-color: #ffffff /* white */ +} + +/* column titles */ +th.when { +} + +th.who { +} + +th.artist { +} + +th.album { +} + +th.title { +} + +th.length { + text-align: right +} + +th.button { +} + +/* individual cells */ + +td.when { +} + +td.who { +} + +td.artist { +} + +td.album { +} + +td.title { +} + +td.length { + text-align: right; + font-size: small /* because otherwise visually intrusive */ +} + +td.button { + text-align: center; + padding: 1px; + border-color: black; + border-width: 1px; + border-style: solid; + background-color: #c0c0c0; + color: #000000 +} + +p.mgmt,form.volume { + display: inline +} + +/* choose *********************************************************************/ + +/* first letter choice */ +p.choosealpha { + text-align: center +} + +/* containing directory */ +p.directoryname { + font-weight: bold +} + +/* directories */ +div.directories { +} + +/* heading for directories */ +p.directories { + font-weight: bold +} + +/* one directory */ +p.directory { + margin-left: 1em +} + +a.directory { +} + +a.directory:link { + color: black +} + +a.directory:visited { + color: black +} + +a.directory:active { + color: red +} + +/* files */ +div.files { +} + +/* heading for files */ +p.files { + font-weight: bold +} + +/* one file */ +p.file { + margin-left: 1em +} + +a.file { + text-decoration: none; +} + +a.file:link { + color: black +} + +a.file:visited { + color: black +} + +a.file:active { + color: red +} + +/* buttons ********************************************************************/ + +/* a.allfiles turns up in track choice + * button is used e.g. in searching + */ +a.allfiles,a.prefs,button,span.button { + padding: 1px; + border-color: #fefefe; + border-style: inset; + background-color: #c0c0c0; + color: #000000; + text-decoration: none; + font-family: sans-serif +} + +a.button { + text-decoration: none; + font-family: sans-serif +} + +a.button:link,a.button:visited,a.allfiles:link,a.allfiles:visited { + background-color: #c0c0c0; + color: #000000 +} + +a.button:active,a.allfiles:active,button:active { + background-color: #c0c0c0; + color: #ffffff +} + +img.button { + border-width: 0 +} + +/* searching ******************************************************************/ + +div.searchresults { +} + +div.search_artist { +} + +p.search_artist { +} + +span.search_artist { + font-weight: bold +} + +div.search_album { + margin-left: 1em +} + +p.search_album { +} + +span.search_album { +} + +div.search_title { + margin-left: 1em +} + +p.search_title { + margin-top: 0; + margin-bottom: 0 +} + +a.search_title { + text-decoration: underline +} + +a.search_title:link { + color: black +} + +a.search_title:visited { + color: black +} + +a.search_title:active { + color: red +} + +/* sidebar ********************************************************************/ + +div#sidebar { + margin: 1em; + position: absolute; + width: 10em; + top: 0; + right: auto; + left: 0; +} + +div#content { + position: absolute; + width: auto; + top: 0; + right: 1em; + left: 6em; +} + +.sidebarlink { + font-family: sans-serif +} + +a.sidebarlink { + text-decoration: none; + color: black +} + +a.sidebarlink:visited { + color: black +} + +a.sidebarlink:active { + color: red +} + +a.sidebarlink:visited { + color: black +} + +/* topbar *********************************************************************/ + +p.menubar { + word-spacing: 1em +} + +.activemenu { + font-family: sans-serif; + font-weight: bold; + font-size: 14pt +} + +.inactivemenu { + font-family: sans-serif; + font-weight: bold; + font-size: 14pt +} + +a.inactivemenu,a.inactivemenu:visited { + text-decoration: none; + color: black +} + +a.activemenu,a.activemenu:visited { + text-decoration: none; + color: red +} + +a.activemenu:active,a.inactivemenu:active { + text-decoration: none; + color: red +} + +/* prefs **********************************************************************/ + +p.prefs_new,p.prefs_head { + font-weight: bold +} + +table.prefs { + border-spacing: 0 +} + +tr.prefs_headings { + background-color: black; + color: white +} + +th.prefs_name { +} + +th.prefs_value { +} + +td.prefs_name { + vertical-align: top +} + +td.prefs_value { + vertical-align: top +} + +td.prefs_delete { + vertical-align: top +} + +input.prefs_name,input.prefs_value { + font-family: monospace +} + +/* help ***********************************************************************/ + +.helpbuttons,.helpprefs,.helpcontexts { + margin-left: 2em; + margin-right: 2em; + vertical-align: top +} + +.helpsection { + margin-left: 1em; +} + +.helppref { + font-family: monospace +} + +.helpprefbit { + font-family: monospace; + font-style: italic +} + +.helpcontext { + font-weight: bold +} + +/* volume *********************************************************************/ + +p.volume { + text-align: center +} + +/* miscelleanous **************************************************************/ + +/* credits */ +p.credits { + font-size: small; /* because visually intrusive */ + text-align: right +} +/* +This file is part of DisOrder. +Copyright (C) 2003, 2004, 2005 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 +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +*/ +/* arch-tag:tlWcgChjjqNVaC/UmG9Zaw */ diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..a2ed445 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,46 @@ + + + + +@include:stdhead@ + @label:error.title@ + + +@include{@label{menu}@}@ +

@label:error.title@

+ +

@label{error.@label:error@}@

+ +

@label:error.generic@

+ +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/help.html b/templates/help.html new file mode 100644 index 0000000..34d6e26 --- /dev/null +++ b/templates/help.html @@ -0,0 +1,355 @@ + + + + +@include{stdhead}@ + @label{help.title}@ + + +@include{@label{menu}@}@ +

@label{help.title}@

+ + + +
+ +

This screen displays the currently playing track (if there is one) and + lists all the tracks in the queue (the track that will be played soonest + being listed first.) Where possible, estimated start times are + given.

+ +

Each track has a @label:playing.scratch@ button next to it. For the + currently playing track this can be used to stop playing the + track before it has finished. For a track in the queue it + removes the track from the queue.

+ +

Depending on the server configuration, you may be able to do + this for any track, or only for tracks you submitted or that were + randomly picked. See the "restrict" option in disorder_config(5) for more + details.

+ +

Artist and album names are hyperlinks to the relevant locations + in the Choose screen (see below).

+ +
+ + + +
+ +

This screen is almost identical to Playing except that it includes extra + management features.

+ +

At the top of the screen are the following controls:

+ +
    +
  • Pause. This button can be used to pause playing (provided the + player supports it). indicates that playing is paused, + that it is not.
  • + +
  • Enable/disable random play. If disabled then queued tracks + will still be played but if the queue is empty nothing will be + picked at random. indicates that random play is + enabled, that it is disabled.
  • + +
  • Enable/disable play. If disabled then tracks in the queue + will not be played, but will remain in the queue instead. + indicates that play is enabled, that it is + disabled.
  • + +
  • Volume control. You can use the @label:volume.increase@ and @label:volume.reduce@ buttons to increase or + decrease the volume, or enter new volume settings for the left + and/or right speakers.
  • + +
+ +

Below this is the same table of current and queued tracks as for + the main playing screen, but with extra buttons for managing the + queue. + The @label:playing.up@ and @label:playing.down@ buttons on each track move that + track around in the queue. Similarly the @label:playing.upall@ and @label:playing.downall@ buttons move each track to the head or + tail of the queue. + Depending on server configuration, it may be that only trusted + users can move tracks around the queue.

+ +
+ + + +
+ +

This screen displays recently played tracks, most recent first. + The @label:choose.prefs@ + button can be used to edit the details for a track; see Editing Preferences below.

+ +

The number of tracks remembered is controlled by the server + configuration. See the "history" option in disorder_config(5) for more + details.

+ +
+ + + +
+ +

This screen allows you to choose a track to be played, by navigating + through the directory structure of the tracks filesystem. The following + buttons appear:

+ + + + + + + + + + + + +
@label:choose.prefs@This button can be used to edit the details for a + track; see Editing Preferences below.
@label{choose.playall}@This button plays all the tracks in a directory, + in order. This is used to efficiently play a whole album.
+ +

This screen has two forms: choose, which give + you all the top-level directories at once, and choosealpha, + which breaks them down by initial letter.

+ +
+ + + +
+ +

This screen, reached from Choose or Recent, is used to edit a track's preferences. + Preferences can be edited in two ways.

+ +

At the top appear "cooked" preferences. These can be used to + edit artist, album and title fields for the track as displayed, or + to set the tags for a track, or to enable or disable random play + for the track.

+ +

Tags are separated by commas and can contain any other printing + characters (including spaces). Leading and trailing spaces are + not significant.

+ +

Random play for any given track is enabled by default, but you + can use this screen to disable it for undesirable tracks.

+ +

Below this are "raw" preferences, which allow individual + database fields to be modified.

+ +

To change an existing preference, edit its value and press its + @label{prefs.set}@ button.

+ +

To delete an existing preference, press its + @label{prefs.delete}@ button.

+ +

To add a new preference, enter its name and value in the box at the + bottom and press the @label{prefs.new}@ button. + If the preference exists already it will be overwritten.

+ + +

Preferences can have any name or value but certain names have special + significance:

+ + + + + + + + + + + +
pick_at_randomIf this preference is present and set to "0" then + the track will not be picked for random play. Otherwise it may be.
trackname_context_partThese preferences can be used to override the + filename parsing rules to find a track name part. trackname_part will + be used if the full version is not present.
+ +

context can be anything but standard + values are:

+ + + + + + + + + + + + +
displayDisplayed in a web page
sortUsed when sorting track names
+ +

part can be anything too but standard + values are "artist", "album" and "title", with the obvious meanings.

+ +

See also disorder(1) and disorder_config(5) for further + details.

+ +
+ + + +
+ +

This screen allows you to search for keywords in track names. If you + specify more than one keyword then only tracks containing all of them are + listed. Results are grouped by artist, album and title.

+ +

It is possible to limit results to tracks with a particular + tag, by using tag:TAG among the search terms.

+ +

Some keywords, known as "stopwords", are excluded from the search, and + will never match. See the "stopword" option in disorder_config(5) for further + details about this.

+ +
+ + @if{@eq{@label:menu@}{sidebar}@} + { + + + +
+ +

This screen allows you to set the playback volume, if this is enabled in + the server configuration. See the "channel" and "mixer" options in disorder_config(5) for further + details about this.

+ +
+ + }{}@ + + + +
+ +

If you cannot play a track, or it does not appear in the + database even after a rescan, check the following things:

+ +
    + +
  • Are there any error messages in the system log? The server + logs to LOG_DAEMON, which typically ends up in + /var/log/daemon.log or /var/log/messages, though + this depends on local configuration. + +
  • Is the track in a known format? Have a look at the + configuration file for the formats recognized by the local + installation. The filename matching is case-sensitive. + +
  • Do permissions on the track allow the server to read it? + +
  • Do the permissions on the containing directories allow the + server to read and execute them? + +
+ +

The user the server runs as is determined by the user + directive in the configuration file. The README recommends using + jukebox for this purpose but it could be different + locally.

+ +
+ + + +
+ +

disorder_config(5) - + configuration

+ +

disorder(1) - command line + client

+ +

disobedience(1) - GTK+ + client

+ +

tkdisorder(1) - GUI + client

+ +

disorderd(8) - server

+ +

disorder-dump(8) - + dump/restore preferences database

+ +

disorder(3) - C API

+ +

disorder_protocol(5) - + DisOrder control protocol

+ +
+ +@include{@label{menu}@end}@ + + + +@@ + + diff --git a/templates/options b/templates/options new file mode 100644 index 0000000..c054770 --- /dev/null +++ b/templates/options @@ -0,0 +1,12 @@ +# default label values +include options.labels + +# default columns +include options.columns + +# trackname transformations - supply your own or keep the default +include options.transform + +# user overrides - you supply this +include options.user +# arch-tag:c3c0270b2c7e398b97c0d6a43961a4c3 diff --git a/templates/options.columns b/templates/options.columns new file mode 100644 index 0000000..52e744c --- /dev/null +++ b/templates/options.columns @@ -0,0 +1,4 @@ +columns playing when who artist album title length button +columns recent when who artist album title length +columns search artist album title +# arch-tag:e8a3ec1ad5b5e9d8eaf6b3391d55abaf diff --git a/templates/options.labels b/templates/options.labels new file mode 100644 index 0000000..f0d47c9 --- /dev/null +++ b/templates/options.labels @@ -0,0 +1,201 @@ +# Labels used by the web interface. +# +# Rather then editing this file, edit options.user instead. +# +# Where there is 'short and long' text this means that the short text +# will appear in the ALT attribute, and so appear in a text-only browser +# (or if images are disabled); usually this should be a single short word. +# The long text will appear in the TITLE attribute, and so appear for +# instance if the user hovers over whatever the widget is. + +# for the 'Playing' screen +label playing.title "Now Playing" + +label playing.randomtrack   +label queue.randomtrack random + +# Short and long text for scratch (remove playing track) button +label playing.scratch Scratch +label playing.scratchverbose "stop playing this track" + +# Short and long text for remove queued track button +label playing.remove Remove +label playing.removeverbose "remove track from queue" + +# Text for banner above currently playing track +label playing.now "Now playing" + +# Text for banner above queue +label playing.next "Next" + +# Short and long text for queue management buttons +label playing.up Up +label playing.down Down +label playing.upall Head +label playing.downall Tail +label playing.upverbose "move track earlier in queue" +label playing.downverbose "move track later in queue" +label playing.upallverbose "move track to head of queue" +label playing.downallverbose "move track to end of queue" + +# Short and long text for play control buttons +label playing.random "Random play" +label playing.playing "Playing" +label playing.pause Pause +label playing.randomdisableverbose "disable random play" +label playing.randomenableverbose "enable random play" +label playing.playingdisableverbose "disable playing" +label playing.playingenableverbose "enable playing" +label playing.pauseverbose "Pause the current track" +label playing.resumeverbose "Resume play" + +# Text for volume control +label playing.volume "Volume:" + +# <TITLE> for volume control page +label volume.title "Volume control" + +# Volume control set button +label volume.set Set + +# Text preceding left/right fields +label volume.left "" +label volume.right "" + +# Short and long text for volume down/up buttons +label volume.reduce Down +label volume.increase Up +label volume.reduceverbose "reduce volume" +label volume.increaseverbose "increase volume" + +# Amount to increase/reduce volume by +label volume.resolution 4 + +# Long text for linsk to album/artist +label playing.artistverbose "more tracks by this artist" +label playing.albumverbose "more tracks from this album" + +# <TITLE> for recently played page +label recent.title "Recently Played" + +# <TITLE> for choose track page +label choose.title "Pick track" + +# Text for play all button +label choose.playall "Play all" + +# Heading for directory list +label choose.directories Directories + +# Heading for track list +label choose.files Tracks + +# Short and long text for edit prefs button (both recent and choose pages) +label choose.prefs Edit +label choose.prefsverbose "edit track information" + +# Same, for edit all prefs +label choose.allprefs "Edit all" +label choose.allprefsverbose "edit all track information" + +# <TITLE> for search page +label search.title Search + +# Text for search button +label search.search Search + +# <TITLE> for about page +label about.title "About DisOrder" + +# <TITLE> for edit prefs page +label prefs.title "Edit Track Preferences" + +# Text for set/add/delete preference buttons +label prefs.set Change +label prefs.new Set +label prefs.delete Delete + +# Headings for preferences table +label prefs.name Name +label prefs.value Value + +# Legend for prefs controls that don't correspond to a heading +label prefs.random "Random play" +label prefs.tags "Tags" + +# <TITLE> for help page +label help.title "DisOrder help" + +# <TITLE> for error page. Note that in this page the 'error' label is set +# to a string indicating the type of error. +label error.title "DisOrder error" + +# Text used when cannot connect to server +label error.connect "Cannot connect to server." + +# Text used when cannot become right user +label error.become "Unauthorized user." + +# Text appended to all error pages +label error.generic "" + +# Displayed text for links in the sidebar (or other menu) +label sidebar.playing Playing +label sidebar.choose Choose +label sidebar.random Random +label sidebar.search Search +label sidebar.recent Recent +label sidebar.about About +label sidebar.volume Volume +label sidebar.help Help +label sidebar.manage Manage + +# Long (i.e. TITLE=) text for sidebar links +label sidebar.playingverbose "current and queued tracks" +label sidebar.chooseverbose "choose tracks" +label sidebar.searchverbose "word search among track names" +label sidebar.recentverbose "recently played tracks" +label sidebar.aboutverbose "about DisOrder" +label sidebar.volumeverbose "volume control" +label sidebar.helpverbose "basic user guide" +label sidebar.manageverbose "queue management and volume control" + +# This should be 'topbar' or 'sidebar'. If 'topbar' then the menu appears +# across the top of the screen, otherwise down the side. +label menu topbar + +# This should be 'choose' or 'choosealpha'. If 'choose' then all artists +# appear on the same page, otherwise they are broken up by initial letter +# (which can be more convenient if you have huge numbers). +label sidebar.choosewhich choose + +# Column headings for tables of tracks (playing, queue, recent) +label heading.when When +label heading.who Who +label heading.artist Artist +label heading.album Album +label heading.title Title +label heading.length Length + +# Images. These are (possibly relative) URLs. In the factory configuration +# DisOrder assumes that you have arranged for 'static' relative to the base +# URL (i.e. the URL of the CGI) to point somewhere useful, but it's not +# the only way. The .deb for instance uses /disorder instead. +label images.enabled static/tick.png +label images.disabled static/cross.png +label images.scratch static/cross.png +label images.noscratch static/nocross.png +label images.up static/up.png +label images.noup static/noup.png +label images.down static/down.png +label images.nodown static/nodown.png +label images.edit static/edit.png +label images.upall static/upup.png +label images.noupall static/noupup.png +label images.downall static/downdown.png +label images.nodownall static/nodowndown.png + +# Stylesheet. As above, a (possibly relative) URL. +label links.css static/disorder.css + +# arch-tag:1c90ecc78e3d1a4ceaca2f0c0e6ee558 diff --git a/templates/playing.html b/templates/playing.html new file mode 100644 index 0000000..c67ab31 --- /dev/null +++ b/templates/playing.html @@ -0,0 +1,240 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> +<!-- +This file is part of DisOrder. +Copyright (C) 2004, 2005 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 +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +--> +<html> + <head> +@include:stdhead@ + <title>@if{@isplaying@}{@playing{@part:title@}@}{@label:playing.title@}@ + + +@include{@label{menu}@}@ +

@label:playing.title@

+ + @#{extra control buttons for the management page}@ + @if{@arg:mgmt@}{ +

+ @if{@paused@}{ + + + @label:playing.pause@ + + + + }{ + + + @label:playing.pause@ + + + + }@ + @if{@random-enabled@}{ + + + @label:playing.random@ + + + + }{ + + + @label:playing.random@ + + + + }@ + @if{@enabled@}{ + + + @label:playing.playing@ + + + + }{ + + + @label:playing.playing@ + + + + }@ +

+ + @label:playing.volume@ + + @label:volume.reduce@ + + @label:volume.left@ + @label:volume.right@ + + + + + @label:volume.increase@ + + +
+ }@ + +@#{only display the table if there is something to put in it}@ +@if{@or{@isplaying@}{@isqueue@}@}{ + + + + + + + + + + @if{@arg:mgmt@}{ + + + + + }@ + + @if{@isplaying@}{ + + + + @playing{ + + + + + + + + + @if{@arg:mgmt@}{ + + + + + }@ + + }@}@ + @if{@isqueue@}{ + + + + @queue{ + + + + + + + + + @if{@arg:mgmt@}{ + @if{@isfirst@} + { + }@}@ +
@label:heading.when@@label:heading.who@@label:heading.artist@@label:heading.album@@label:heading.title@@label:heading.length@     
@label:playing.now@
@when@@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:playing.randomtrack@}{ }@}{@who@}@@part:artist@@part:album@@part:title@@length@@if{@scratchable@}{@label:playing.scratch@}{@label:playing.scratch@}@    
@label:playing.next@
@when@@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:queue.randomtrack@}{ }@}{@who@}@@part:artist@@part:album@@part:title@@length@@if{@removable@}{@label:playing.remove@}{ }@ + + + } + { + @label:playing.upall@ + + @label:playing.up@}@ + @if{@islast@} + { + + + } + { + @label:playing.down@ + + @label:playing.downall@}@ + }@ +
+}@ + +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/prefs.html b/templates/prefs.html new file mode 100644 index 0000000..ac7c8ce --- /dev/null +++ b/templates/prefs.html @@ -0,0 +1,86 @@ + + + + +@include:stdhead@ + @label:prefs.title@ + + +@include{@label{menu}@}@ +

@label:prefs.title@

+ +
+ + + + @files{ +

Preferences for @arg{@index@_file}@

+ + + + + + + + + + + + + + + + + + + + + + + + + +
@label:prefs.name@@label:prefs.value@
@label:heading.title@
@label:heading.album@
@label:heading.artist@
@label:prefs.tags@
@label:prefs.random@
+ }@ + +

+ +

+
+ +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/recent.html b/templates/recent.html new file mode 100644 index 0000000..320dd0d --- /dev/null +++ b/templates/recent.html @@ -0,0 +1,72 @@ + + + + +@include:stdhead@ + @label:recent.title@ + + +@include{@label{menu}@}@ +

@label:recent.title@

+ +@#{only display the table if there is something to put in it}@ +@if{@isrecent@}{ + + + + + + + + + + + @recent{ + + + + + + + + + + }@ +
@label:heading.when@@label:heading.who@@label:heading.artist@@label:heading.album@@label:heading.title@@label:heading.length@ 
@when@@who@@part:artist@@part:album@@part:title@@length@@label:choose.prefs@
+}@ + +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..6d153cf --- /dev/null +++ b/templates/search.html @@ -0,0 +1,78 @@ + + + + +@include:stdhead@ + @label:search.title@ + + +@include{@label{menu}@}@ +

@label:search.title@

+ + + +
+ @search{artist}{display}{ +
+

Artist: + @part:artist@

+ @search{album}{display}{ +
+

Album: + @part:album@

+ @search{title}{ +
+

Title: + @part:title@ + @if{@eq{@trackstate{@file@}@}{playing}@}{[playing]}@ + @if{@eq{@trackstate{@file@}@}{queued}@}{[queued]}@ +

+
+ }@ +
+ }@ +
+ }@ +
+ +@include{@label{menu}@end}@ + + +@@ + + diff --git a/templates/sidebar.html b/templates/sidebar.html new file mode 100644 index 0000000..d0d3a69 --- /dev/null +++ b/templates/sidebar.html @@ -0,0 +1,48 @@ + +
+@@ + + diff --git a/templates/sidebarend.html b/templates/sidebarend.html new file mode 100644 index 0000000..551fd41 --- /dev/null +++ b/templates/sidebarend.html @@ -0,0 +1,23 @@ +@include:credits@ +
+@@ + + diff --git a/templates/stdhead.html b/templates/stdhead.html new file mode 100644 index 0000000..7b0fbb4 --- /dev/null +++ b/templates/stdhead.html @@ -0,0 +1,23 @@ +@include:stylesheet@ +@@ +Anything that goes in all html HEAD elements goes here. + + diff --git a/templates/stylesheet.html b/templates/stylesheet.html new file mode 100644 index 0000000..6632216 --- /dev/null +++ b/templates/stylesheet.html @@ -0,0 +1,24 @@ + +@@ +This file is a standard place to put a link to a stylesheet, +or an embedded stylesheet. + + diff --git a/templates/topbar.html b/templates/topbar.html new file mode 100644 index 0000000..bb760f5 --- /dev/null +++ b/templates/topbar.html @@ -0,0 +1,53 @@ + +
+@@ + + diff --git a/templates/topbarend.html b/templates/topbarend.html new file mode 100644 index 0000000..fd888ef --- /dev/null +++ b/templates/topbarend.html @@ -0,0 +1,22 @@ +@include:credits@ +@@ + + diff --git a/templates/volume.html b/templates/volume.html new file mode 100644 index 0000000..abb3d63 --- /dev/null +++ b/templates/volume.html @@ -0,0 +1,63 @@ + + + + +@include:stdhead@ + @label:volume.title@ + + +@include{@label{menu}@}@ +

@label:volume.title@

+ +
+

+ + @label:volume.reduce@ + + @label:volume.left@ + @label:volume.right@ + + + + @label:volume.increase@ + +

+
+ +@include{@label{menu}@end}@ + + +@@ + +