chiark / gitweb /
Import from Arch revision:
authorrjk@greenend.org.uk <>
Tue, 23 Jan 2007 21:23:45 +0000 (21:23 +0000)
committerrjk@greenend.org.uk <>
Tue, 23 Jan 2007 21:23:45 +0000 (21:23 +0000)
rjk@greenend.org.uk--2004/disorder--mainline--0.1--patch-328

252 files changed:
BUGS [new file with mode: 0644]
CHANGES [new file with mode: 0644]
ChangeLog.d/cvs--ChangeLog [new file with mode: 0644]
ChangeLog.d/disorder--mainline--0.1 [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
README [new file with mode: 0644]
README.client [new file with mode: 0644]
README.raw [new file with mode: 0644]
README.streams [new file with mode: 0644]
README.upgrades [new file with mode: 0644]
TODO [new file with mode: 0644]
acinclude.m4 [new file with mode: 0644]
clients/Makefile.am [new file with mode: 0644]
clients/authorize.c [new file with mode: 0644]
clients/authorize.h [new file with mode: 0644]
clients/disorder.c [new file with mode: 0644]
clients/disorderfm.c [new file with mode: 0644]
clients/filename-bytes.c [new file with mode: 0644]
clients/test-eclient.c [new file with mode: 0644]
configure.ac [new file with mode: 0644]
debian/Makefile.am [new file with mode: 0644]
debian/README.Debian [new file with mode: 0644]
debian/autorules.m4 [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/conffiles [new file with mode: 0644]
debian/config [new file with mode: 0755]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/disorder.config [new file with mode: 0644]
debian/htaccess [new file with mode: 0644]
debian/options.debian [new file with mode: 0644]
debian/postinst [new file with mode: 0755]
debian/postrm [new file with mode: 0755]
debian/prerm [new file with mode: 0755]
debian/rules.m4 [new file with mode: 0644]
debian/templates [new file with mode: 0644]
disobedience/Makefile.am [new file with mode: 0644]
disobedience/TODO [new file with mode: 0644]
disobedience/choose.c [new file with mode: 0644]
disobedience/client.c [new file with mode: 0644]
disobedience/control.c [new file with mode: 0644]
disobedience/disobedience.c [new file with mode: 0644]
disobedience/disobedience.h [new file with mode: 0644]
disobedience/disobedience.rc [new file with mode: 0644]
disobedience/menu.c [new file with mode: 0644]
disobedience/misc.c [new file with mode: 0644]
disobedience/properties.c [new file with mode: 0644]
disobedience/queue.c [new file with mode: 0644]
doc/Makefile.am [new file with mode: 0644]
doc/checklist.txt [new file with mode: 0644]
doc/disobedience.1.in [new file with mode: 0644]
doc/disorder-deadlock.8.in [new file with mode: 0644]
doc/disorder-dump.8.in [new file with mode: 0644]
doc/disorder-rescan.8.in [new file with mode: 0644]
doc/disorder.1.in [new file with mode: 0644]
doc/disorder.3 [new file with mode: 0644]
doc/disorder_config.5.in [new file with mode: 0644]
doc/disorder_protocol.5.in [new file with mode: 0644]
doc/disorderd.8.in [new file with mode: 0644]
doc/disorderfm.1.in [new file with mode: 0644]
doc/tkdisorder.1 [new file with mode: 0644]
driver/Makefile.am [new file with mode: 0644]
driver/disorder.c [new file with mode: 0644]
examples/Makefile.am [new file with mode: 0644]
examples/config.sample.in [new file with mode: 0644]
examples/disorder-log [new file with mode: 0755]
examples/disorder.init.in [new file with mode: 0644]
images/Makefile.am [new file with mode: 0644]
images/cross.png [new file with mode: 0644]
images/down.png [new file with mode: 0644]
images/downdown.png [new file with mode: 0644]
images/edit.png [new file with mode: 0644]
images/nocross.png [new file with mode: 0644]
images/nodown.png [new file with mode: 0644]
images/nodowndown.png [new file with mode: 0644]
images/notes.png [new file with mode: 0644]
images/notescross.png [new file with mode: 0644]
images/noup.png [new file with mode: 0644]
images/noupup.png [new file with mode: 0644]
images/pause.png [new file with mode: 0644]
images/play.png [new file with mode: 0644]
images/random.png [new file with mode: 0644]
images/randomcross.png [new file with mode: 0644]
images/tick.png [new file with mode: 0644]
images/up.png [new file with mode: 0644]
images/upup.png [new file with mode: 0644]
lib/Makefile.am [new file with mode: 0644]
lib/addr.c [new file with mode: 0644]
lib/addr.h [new file with mode: 0644]
lib/asprintf.c [new file with mode: 0644]
lib/authhash.c [new file with mode: 0644]
lib/authhash.h [new file with mode: 0644]
lib/basen.c [new file with mode: 0644]
lib/basen.h [new file with mode: 0644]
lib/cache.c [new file with mode: 0644]
lib/cache.h [new file with mode: 0644]
lib/casefold.h [new file with mode: 0644]
lib/charset.c [new file with mode: 0644]
lib/charset.h [new file with mode: 0644]
lib/client-common.c [new file with mode: 0644]
lib/client-common.h [new file with mode: 0644]
lib/client.c [new file with mode: 0644]
lib/client.h [new file with mode: 0644]
lib/configuration.c [new file with mode: 0644]
lib/configuration.h [new file with mode: 0644]
lib/defs.c [new file with mode: 0644]
lib/defs.h [new file with mode: 0644]
lib/disorder.h [new file with mode: 0644]
lib/eclient.c [new file with mode: 0644]
lib/eclient.h [new file with mode: 0644]
lib/event.c [new file with mode: 0644]
lib/event.h [new file with mode: 0644]
lib/eventlog.c [new file with mode: 0644]
lib/eventlog.h [new file with mode: 0644]
lib/filepart.c [new file with mode: 0644]
lib/filepart.h [new file with mode: 0644]
lib/fprintf.c [new file with mode: 0644]
lib/hash.c [new file with mode: 0644]
lib/hash.h [new file with mode: 0644]
lib/hex.c [new file with mode: 0644]
lib/hex.h [new file with mode: 0644]
lib/inputline.c [new file with mode: 0644]
lib/inputline.h [new file with mode: 0644]
lib/kvp.c [new file with mode: 0644]
lib/kvp.h [new file with mode: 0644]
lib/log-impl.h [new file with mode: 0644]
lib/log.c [new file with mode: 0644]
lib/log.h [new file with mode: 0644]
lib/logfd.c [new file with mode: 0644]
lib/logfd.h [new file with mode: 0644]
lib/mem-impl.h [new file with mode: 0644]
lib/mem.c [new file with mode: 0644]
lib/mem.h [new file with mode: 0644]
lib/mime.c [new file with mode: 0644]
lib/mime.h [new file with mode: 0644]
lib/mixer.c [new file with mode: 0644]
lib/mixer.h [new file with mode: 0644]
lib/plugin.c [new file with mode: 0644]
lib/plugin.h [new file with mode: 0644]
lib/printf.c [new file with mode: 0644]
lib/printf.h [new file with mode: 0644]
lib/queue.c [new file with mode: 0644]
lib/queue.h [new file with mode: 0644]
lib/regsub.c [new file with mode: 0644]
lib/regsub.h [new file with mode: 0644]
lib/selection.c [new file with mode: 0644]
lib/selection.h [new file with mode: 0644]
lib/signame.c [new file with mode: 0644]
lib/signame.h [new file with mode: 0644]
lib/sink.c [new file with mode: 0644]
lib/sink.h [new file with mode: 0644]
lib/snprintf.c [new file with mode: 0644]
lib/speaker.c [new file with mode: 0644]
lib/speaker.h [new file with mode: 0644]
lib/split.c [new file with mode: 0644]
lib/split.h [new file with mode: 0644]
lib/syscalls.c [new file with mode: 0644]
lib/syscalls.h [new file with mode: 0644]
lib/table.c [new file with mode: 0644]
lib/table.h [new file with mode: 0644]
lib/test.c [new file with mode: 0644]
lib/trackname.c [new file with mode: 0644]
lib/trackname.h [new file with mode: 0644]
lib/types.h [new file with mode: 0644]
lib/unicodegc.h [new file with mode: 0644]
lib/user.c [new file with mode: 0644]
lib/user.h [new file with mode: 0644]
lib/utf8.c [new file with mode: 0644]
lib/utf8.h [new file with mode: 0644]
lib/vacopy.h [new file with mode: 0644]
lib/vector.c [new file with mode: 0644]
lib/vector.h [new file with mode: 0644]
lib/words.c [new file with mode: 0644]
lib/words.h [new file with mode: 0644]
lib/wstat.c [new file with mode: 0644]
lib/wstat.h [new file with mode: 0644]
plugins/Makefile.am [new file with mode: 0644]
plugins/exec.c [new file with mode: 0644]
plugins/execraw.c [new file with mode: 0644]
plugins/fs.c [new file with mode: 0644]
plugins/mad.c [new file with mode: 0644]
plugins/madshim.h [new file with mode: 0644]
plugins/notify.c [new file with mode: 0644]
plugins/shell.c [new file with mode: 0644]
plugins/tracklength.c [new file with mode: 0644]
prepare [new file with mode: 0755]
python/Makefile.am [new file with mode: 0644]
python/disorder.py.in [new file with mode: 0644]
python/tkdisorder [new file with mode: 0755]
scripts/Makefile.am [new file with mode: 0644]
scripts/check [new file with mode: 0755]
scripts/completion.bash [new file with mode: 0644]
scripts/copyright.exceptions [new file with mode: 0644]
scripts/dist [new file with mode: 0755]
scripts/htmlman [new file with mode: 0755]
scripts/inst [new file with mode: 0755]
scripts/makedeb [new file with mode: 0755]
scripts/oggrename [new file with mode: 0644]
scripts/sedfiles.make [new file with mode: 0644]
scripts/text2c [new file with mode: 0755]
server/Makefile.am [new file with mode: 0644]
server/api-client.c [new file with mode: 0644]
server/api-client.h [new file with mode: 0644]
server/api-server.c [new file with mode: 0644]
server/api.c [new file with mode: 0644]
server/cgi.c [new file with mode: 0644]
server/cgi.h [new file with mode: 0644]
server/cgimain.c [new file with mode: 0644]
server/daemonize.c [new file with mode: 0644]
server/daemonize.h [new file with mode: 0644]
server/dcgi.c [new file with mode: 0644]
server/dcgi.h [new file with mode: 0644]
server/deadlock.c [new file with mode: 0644]
server/disorderd.c [new file with mode: 0644]
server/dump.c [new file with mode: 0644]
server/play.c [new file with mode: 0644]
server/play.h [new file with mode: 0644]
server/rescan.c [new file with mode: 0644]
server/server.c [new file with mode: 0644]
server/server.h [new file with mode: 0644]
server/speaker.c [new file with mode: 0644]
server/state.c [new file with mode: 0644]
server/state.h [new file with mode: 0644]
server/trackdb-int.h [new file with mode: 0644]
server/trackdb.c [new file with mode: 0644]
server/trackdb.h [new file with mode: 0644]
server/trackname.c [new file with mode: 0644]
sounds/Makefile.am [new file with mode: 0644]
sounds/scratch.ogg [new file with mode: 0644]
sounds/slap.ogg [new file with mode: 0644]
templates/Makefile.am [new file with mode: 0644]
templates/about.html [new file with mode: 0644]
templates/choose.html [new file with mode: 0644]
templates/choosealpha.html [new file with mode: 0644]
templates/credits.html [new file with mode: 0644]
templates/disorder.css [new file with mode: 0644]
templates/error.html [new file with mode: 0644]
templates/help.html [new file with mode: 0644]
templates/options [new file with mode: 0644]
templates/options.columns [new file with mode: 0644]
templates/options.labels [new file with mode: 0644]
templates/playing.html [new file with mode: 0644]
templates/prefs.html [new file with mode: 0644]
templates/recent.html [new file with mode: 0644]
templates/search.html [new file with mode: 0644]
templates/sidebar.html [new file with mode: 0644]
templates/sidebarend.html [new file with mode: 0644]
templates/stdhead.html [new file with mode: 0644]
templates/stylesheet.html [new file with mode: 0644]
templates/topbar.html [new file with mode: 0644]
templates/topbarend.html [new file with mode: 0644]
templates/volume.html [new file with mode: 0644]

diff --git a/BUGS b/BUGS
new file mode 100644 (file)
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 (file)
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 (file)
index 0000000..050c274
--- /dev/null
@@ -0,0 +1,881 @@
+2004-10-04 00:59:42 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * Makefile.am, plugins/Makefile.am: tidy up to build in separate
+          object directory
+        
+2004-09-25 17:47:23 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * version 0.11
+        
+2004-09-25 17:41:21 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * BUGS: some known bugs
+        
+2004-09-25 17:25:49 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * tracks.c: chattier errors
+        
+2004-09-25 17:17:25 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * api-client.c, cgi.c, cgimain.c, disorder.c, server.c: EXIT_FAILURE
+          pedantry
+        
+2004-09-18 17:28:32 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * client.c: rework handling of commands that get lists back,
+          unbreaking 'stats' in the process.
+        
+2004-09-18 16:54:58 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * tracks.c: add a lockfile to prevent concurrent access to databases.
+        
+2004-07-31 19:05:38 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * debian/postinst, prerm: hopefuly better upgrade handling
+        
+2004-07-31 18:44:52 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * tkdisorder: primitive queue widget, simplify MonitorStateThread
+        
+2004-07-20 19:42:52 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * tkdisorder: python + tkinter gui, still under development
+        
+2004-07-18 14:21:12 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * debian/control: more detailed build deps
+        
+2004-07-18 13:03:56 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * README: dependency version notes
+        
+2004-07-18 12:57:55 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * cope with gcrypt version skew
+        
+2004-07-18 02:46:07 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * Increase/decrease volume buttons
+        
+2004-07-17 16:35:18 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * templates/help.html: document 'Manage'
+        
+        * templates/playing.html: extra empty buttons
+        
+2004-07-17 16:24:12 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * '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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * templates/help.html: mention choosealpha
+        
+2004-07-17 14:37:18 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * disorder.py.in: correct exception-to-string functions
+        
+2004-07-17 13:05:06 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * disorder.py.in: add another example
+        
+2004-07-17 12:57:57 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * disorder.py.in: better docs and error handling
+        
+2004-07-17 01:06:59 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * disorder.py.in: add a full set of methods
+        
+2004-07-17 01:06:31 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * plugins/tracklength.c: quieten compiler
+        
+2004-07-16 22:31:31 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * rudimentary Python client support
+        
+2004-07-15 00:55:08 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * configuration.c: stricter syntax check for 'url'
+        
+2004-07-15 00:41:22 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * plugins/tracklength.c: build fix
+        
+2004-07-11 19:39:40 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * add notify_queue to notify plugin
+        
+2004-07-11 19:01:26 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * tracks.c: always sync log after replay, so that upgrade works
+        
+2004-07-11 18:54:33 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * disorder.c: missing 'break', oops!
+        
+2004-07-10 19:43:42 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * plugins/tracklength.c: binary search over extensions
+                                 round up WAV duration
+        
+2004-07-10 19:22:50 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 'disorder --length' allows command-line access to tracklength plugin
+        
+2004-07-10 14:47:41 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * printf.c: correct number bases!
+        
+2004-07-10 00:00:33 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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  <rjk@greenend.org.uk>
+
+        * 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 &#64; not &#40;
+        
+2004-05-24 14:00:38 +0100  Richard Kettlewell  <rjk@greenend.org.uk>
+
+        * templates/playing.html: put track title in <TITLE> 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 (file)
index 0000000..2daf877
--- /dev/null
@@ -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 &nbsp;.  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 (file)
index 0000000..401a14c
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..9a2ff74
--- /dev/null
@@ -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 (file)
index 0000000..cf2ca0c
--- /dev/null
@@ -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 (file)
index 0000000..4e6e425
--- /dev/null
@@ -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 (file)
index 0000000..6e279e7
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..03fd6ca
--- /dev/null
@@ -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 (file)
index 0000000..5cdb0cb
--- /dev/null
@@ -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 (file)
index 0000000..e38aed4
--- /dev/null
@@ -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 (file)
index 0000000..0098b09
--- /dev/null
@@ -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 (file)
index 0000000..46e2c2d
--- /dev/null
@@ -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 (file)
index 0000000..0fd4fa0
--- /dev/null
@@ -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 (file)
index 0000000..67ce1fd
--- /dev/null
@@ -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 (file)
index 0000000..67900ad
--- /dev/null
@@ -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 (file)
index 0000000..4895914
--- /dev/null
@@ -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 (file)
index 0000000..896e39c
--- /dev/null
@@ -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 (file)
index 0000000..93b2c99
--- /dev/null
@@ -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 (file)
index 0000000..d04393b
--- /dev/null
@@ -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 (file)
index 0000000..3cb4a8a
--- /dev/null
@@ -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 (file)
index 0000000..7d1aa67
--- /dev/null
@@ -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 (executable)
index 0000000..3d95bc7
--- /dev/null
@@ -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 (file)
index 0000000..1ea9dad
--- /dev/null
@@ -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 (file)
index 0000000..6731eeb
--- /dev/null
@@ -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 (file)
index 0000000..2f44f50
--- /dev/null
@@ -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 (file)
index 0000000..1f0c3a3
--- /dev/null
@@ -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 (file)
index 0000000..ea164b2
--- /dev/null
@@ -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 (executable)
index 0000000..2ffe148
--- /dev/null
@@ -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 (executable)
index 0000000..1c09f1c
--- /dev/null
@@ -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 (executable)
index 0000000..a15ed80
--- /dev/null
@@ -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 (file)
index 0000000..df6a051
--- /dev/null
@@ -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 (file)
index 0000000..a6712d1
--- /dev/null
@@ -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 (file)
index 0000000..cfbba08
--- /dev/null
@@ -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 (file)
index 0000000..fa1dee6
--- /dev/null
@@ -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 (file)
index 0000000..369c601
--- /dev/null
@@ -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 **)&regexp, "^(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 (file)
index 0000000..d03e9de
--- /dev/null
@@ -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(&gtkclient_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 (file)
index 0000000..159ab5d
--- /dev/null
@@ -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 (file)
index 0000000..dc8c27e
--- /dev/null
@@ -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 (file)
index 0000000..a63cf44
--- /dev/null
@@ -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 (file)
index 0000000..ed2710c
--- /dev/null
@@ -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 (file)
index 0000000..6eaea06
--- /dev/null
@@ -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 (file)
index 0000000..26222a9
--- /dev/null
@@ -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 (file)
index 0000000..ae945be
--- /dev/null
@@ -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 (file)
index 0000000..2494505
--- /dev/null
@@ -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 (file)
index 0000000..743ace3
--- /dev/null
@@ -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 (file)
index 0000000..f62e7be
--- /dev/null
@@ -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 (file)
index 0000000..75fc867
--- /dev/null
@@ -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 (file)
index 0000000..a876bb0
--- /dev/null
@@ -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 (file)
index 0000000..601f0cc
--- /dev/null
@@ -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 (file)
index 0000000..e66894e
--- /dev/null
@@ -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 (file)
index 0000000..369f2ca
--- /dev/null
@@ -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 (file)
index 0000000..62508f5
--- /dev/null
@@ -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 (file)
index 0000000..d9bd260
--- /dev/null
@@ -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&eacute;\fR, or an SGML numeric
+character reference, e.g. \fB&#253;\fR.  Use \fB&#64;\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 (file)
index 0000000..0344ac8
--- /dev/null
@@ -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 (file)
index 0000000..4e68055
--- /dev/null
@@ -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 (file)
index 0000000..81b48f3
--- /dev/null
@@ -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 (file)
index 0000000..d5fb365
--- /dev/null
@@ -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 (file)
index 0000000..a43470d
--- /dev/null
@@ -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 (file)
index 0000000..8385ae8
--- /dev/null
@@ -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 (file)
index 0000000..74984f4
--- /dev/null
@@ -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 (file)
index 0000000..eb10fda
--- /dev/null
@@ -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 (executable)
index 0000000..5a966a8
--- /dev/null
@@ -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 (file)
index 0000000..5348597
--- /dev/null
@@ -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 (file)
index 0000000..1af87d2
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..91809f7
--- /dev/null
@@ -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 (file)
index 0000000..07224c6
--- /dev/null
@@ -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 (file)
index 0000000..c20a4c8
--- /dev/null
@@ -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 (file)
index 0000000..3abe0db
--- /dev/null
@@ -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 (file)
index 0000000..360d7ef
--- /dev/null
@@ -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 (file)
index 0000000..4ef57ae
--- /dev/null
@@ -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 (file)
index 0000000..157919a
--- /dev/null
@@ -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 (file)
index 0000000..aedd5cf
--- /dev/null
@@ -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 (file)
index 0000000..df8fa68
--- /dev/null
@@ -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 (file)
index 0000000..f1aeccd
--- /dev/null
@@ -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 (file)
index 0000000..5b00c2b
--- /dev/null
@@ -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 (file)
index 0000000..e8f577a
--- /dev/null
@@ -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 (file)
index 0000000..3dcd756
--- /dev/null
@@ -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 (file)
index 0000000..1e8fbac
--- /dev/null
@@ -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 (file)
index 0000000..0387eb8
--- /dev/null
@@ -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 (file)
index 0000000..ddf88bb
--- /dev/null
@@ -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 (file)
index 0000000..b3c18a5
--- /dev/null
@@ -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 (file)
index 0000000..164f374
--- /dev/null
@@ -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 (file)
index 0000000..d647219
--- /dev/null
@@ -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 (file)
index 0000000..2bc9296
--- /dev/null
@@ -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 (file)
index 0000000..52271b3
--- /dev/null
@@ -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 (file)
index 0000000..ded021f
--- /dev/null
@@ -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 (file)
index 0000000..5e6cd6f
--- /dev/null
@@ -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 (file)
index 0000000..caeb81c
--- /dev/null
@@ -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 (file)
index 0000000..b49b2e1
--- /dev/null
@@ -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 (file)
index 0000000..32bc1d4
--- /dev/null
@@ -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 (file)
index 0000000..c374e4a
--- /dev/null
@@ -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 (file)
index 0000000..102956e
--- /dev/null
@@ -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 (file)
index 0000000..575cbc5
--- /dev/null
@@ -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 (file)
index 0000000..3e616d4
--- /dev/null
@@ -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 (file)
index 0000000..f86ede3
--- /dev/null
@@ -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 (file)
index 0000000..2fc19c1
--- /dev/null
@@ -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 (file)
index 0000000..d8698f0
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..891bade
--- /dev/null
@@ -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 (file)
index 0000000..74a8cbc
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..27f0004
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..e644e6c
--- /dev/null
@@ -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 (file)
index 0000000..4815fa4
--- /dev/null
@@ -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 (file)
index 0000000..a1ad0d1
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..d80ae5d
--- /dev/null
@@ -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(&parametername);
+    ++s;
+    if(!(s = skipwhite(s))) return -1;
+    if(!*s) return -1;
+    while(*s && !tspecial(*s) && !whitespace(*s))
+      dynstr_append(&parametername, 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(&parametervalue);
+      while(*s && !tspecial(*s) && !whitespace(*s))
+       dynstr_append(&parametervalue, *s++);
+      dynstr_terminate(&parametervalue);
+      *parametervaluep = parametervalue.vec;
+    }
+    if(!(s = skipwhite(s))) return -1;
+    dynstr_terminate(&parametername);
+    *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(&parametername);
+    ++s;
+    if(!(s = skipwhite(s))) return -1;
+    if(!*s) return -1;
+    while(*s && !tspecial(*s) && !whitespace(*s))
+      dynstr_append(&parametername, 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(&parametervalue);
+      while(*s && !tspecial(*s) && !whitespace(*s))
+       dynstr_append(&parametervalue, *s++);
+      dynstr_terminate(&parametervalue);
+      *parametervaluep = parametervalue.vec;
+    }
+    if(!(s = skipwhite(s))) return -1;
+    dynstr_terminate(&parametername);
+    *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 (file)
index 0000000..14cb809
--- /dev/null
@@ -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 (file)
index 0000000..5293ffd
--- /dev/null
@@ -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 (file)
index 0000000..99fbef2
--- /dev/null
@@ -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 (file)
index 0000000..bf41797
--- /dev/null
@@ -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 (file)
index 0000000..59cc936
--- /dev/null
@@ -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 (file)
index 0000000..aafe7ce
--- /dev/null
@@ -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 = &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 (file)
index 0000000..209615d
--- /dev/null
@@ -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 (file)
index 0000000..c7d32a4
--- /dev/null
@@ -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 (file)
index 0000000..5bf1587
--- /dev/null
@@ -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 (file)
index 0000000..bad6034
--- /dev/null
@@ -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 (file)
index 0000000..683a85b
--- /dev/null
@@ -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 (file)
index 0000000..ad46cb3
--- /dev/null
@@ -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 (file)
index 0000000..b358eb3
--- /dev/null
@@ -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 (file)
index 0000000..19e0662
--- /dev/null
@@ -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 (file)
index 0000000..a4d74d4
--- /dev/null
@@ -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 (file)
index 0000000..e54533a
--- /dev/null
@@ -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 (file)
index 0000000..f19a0c6
--- /dev/null
@@ -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 (file)
index 0000000..f16b047
--- /dev/null
@@ -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 (file)
index 0000000..401bbe5
--- /dev/null
@@ -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 (file)
index 0000000..ef1e849
--- /dev/null
@@ -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 (file)
index 0000000..dbf7a48
--- /dev/null
@@ -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 (file)
index 0000000..2344f87
--- /dev/null
@@ -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 (file)
index 0000000..ddfcb46
--- /dev/null
@@ -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 (file)
index 0000000..2ebffd9
--- /dev/null
@@ -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 (file)
index 0000000..1f30980
--- /dev/null
@@ -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 (file)
index 0000000..476399d
--- /dev/null
@@ -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 (file)
index 0000000..6eee1c9
--- /dev/null
@@ -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 (file)
index 0000000..c439fa4
--- /dev/null
@@ -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 (file)
index 0000000..cb227e0
--- /dev/null
@@ -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 (file)
index 0000000..e683268
--- /dev/null
@@ -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 (file)
index 0000000..1f67f5d
--- /dev/null
@@ -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 (file)
index 0000000..7cbca98
--- /dev/null
@@ -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 (file)
index 0000000..b895c5a
--- /dev/null
@@ -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 (file)
index 0000000..d4dc472
--- /dev/null
@@ -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 (file)
index 0000000..a051019
--- /dev/null
@@ -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 (file)
index 0000000..844ec31
--- /dev/null
@@ -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 (file)
index 0000000..f416104
--- /dev/null
@@ -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 (file)
index 0000000..8fbcc96
--- /dev/null
@@ -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 (file)
index 0000000..2e4001d
--- /dev/null
@@ -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 (file)
index 0000000..52a6839
--- /dev/null
@@ -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 (file)
index 0000000..bd387ff
--- /dev/null
@@ -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 (file)
index 0000000..bf80e42
--- /dev/null
@@ -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 (file)
index 0000000..3c87817
--- /dev/null
@@ -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 (file)
index 0000000..283ebd3
--- /dev/null
@@ -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 (file)
index 0000000..3c3595a
--- /dev/null
@@ -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 (file)
index 0000000..3212a04
--- /dev/null
@@ -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 (file)
index 0000000..32cbded
--- /dev/null
@@ -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 (file)
index 0000000..20ec3dc
--- /dev/null
@@ -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 (file)
index 0000000..eede49f
--- /dev/null
@@ -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 (file)
index 0000000..0fd3333
--- /dev/null
@@ -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 (file)
index 0000000..8d21ad8
--- /dev/null
@@ -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 (executable)
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 (file)
index 0000000..5f858f8
--- /dev/null
@@ -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 (file)
index 0000000..dfaddbf
--- /dev/null
@@ -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 (executable)
index 0000000..243b15e
--- /dev/null
@@ -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 (file)
index 0000000..84ad902
--- /dev/null
@@ -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 (executable)
index 0000000..b105277
--- /dev/null
@@ -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 (file)
index 0000000..cf237c3
--- /dev/null
@@ -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 (file)
index 0000000..4d64d50
--- /dev/null
@@ -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 (executable)
index 0000000..5583849
--- /dev/null
@@ -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 (executable)
index 0000000..54b1402
--- /dev/null
@@ -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</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+EOF
+printf "   <pre class=manpage>"
+# this is kind of painful using only BREs
+nroff -man "$1" | sed 's/&/\&amp;/g;
+                       s/</\&lt;/g;
+                       s/>/\&gt;/g;
+                       s/@/\&#64;/g;
+                       s!\(.\)\b\1!<b>\1</b>!g;
+                       s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
+                       s!_\b\(.\)!<i>\1</i>!g;
+                       s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
+                       s!</\([bi]\)><\1>!!g'
+cat <<EOF
+</pre>
+@include{@label{menu}@end}@
+ </body>
+</html>
+EOF
+# arch-tag:c0096f33b8a8f7d88236043ed970ae83
diff --git a/scripts/inst b/scripts/inst
new file mode 100755 (executable)
index 0000000..e0a51f0
--- /dev/null
@@ -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 (executable)
index 0000000..0531460
--- /dev/null
@@ -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 (file)
index 0000000..458a32b
--- /dev/null
@@ -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 (file)
index 0000000..534c5de
--- /dev/null
@@ -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 (executable)
index 0000000..23079f3
--- /dev/null
@@ -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 (file)
index 0000000..d0e8087
--- /dev/null
@@ -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 (file)
index 0000000..a69ec7e
--- /dev/null
@@ -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 <config.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <locale.h>
+
+#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 (file)
index 0000000..174405d
--- /dev/null
@@ -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 (file)
index 0000000..18056f2
--- /dev/null
@@ -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 <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <pcre.h>
+
+#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 (file)
index 0000000..8d631ca
--- /dev/null
@@ -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 <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <syslog.h>
+
+#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 (file)
index 0000000..02bad31
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <stddef.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <limits.h>
+#include <fnmatch.h>
+#include <ctype.h>
+
+#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, "</%s>", 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,
+                         &parameter_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 (file)
index 0000000..e871052
--- /dev/null
@@ -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 (file)
index 0000000..9be0d1b
--- /dev/null
@@ -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 <config.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <locale.h>
+#include <string.h>
+#include <stdarg.h>
+
+#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 (file)
index 0000000..bddc167
--- /dev/null
@@ -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 <config.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <syslog.h>
+
+#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 (file)
index 0000000..97e9edd
--- /dev/null
@@ -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 (file)
index 0000000..1f2d044
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <pcre.h>
+#include <assert.h>
+
+#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 **)&current,
+                      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", "&nbsp;");
+}
+
+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, "&nbsp;");
+}
+
+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, "&nbsp;");
+}
+
+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 (file)
index 0000000..2846001
--- /dev/null
@@ -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 (file)
index 0000000..aa29b41
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <string.h>
+#include <syslog.h>
+
+#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 (file)
index 0000000..06788f8
--- /dev/null
@@ -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 <config.h>
+
+#include <stdio.h>
+#include <getopt.h>
+#include <pwd.h>
+#include <grp.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <locale.h>
+#include <syslog.h>
+#include <sys/time.h>
+#include <pcre.h>
+#include <fcntl.h>
+
+#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 (file)
index 0000000..39af755
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <pcre.h>
+#include <unistd.h>
+#include <db.h>
+
+#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 (file)
index 0000000..e70e116
--- /dev/null
@@ -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 <config.h>
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <time.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <sys/socket.h>
+#include <string.h>
+#include <stdio.h>
+#include <pcre.h>
+#include <ao/ao.h>
+
+#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 (file)
index 0000000..396ff56
--- /dev/null
@@ -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 (file)
index 0000000..0c1e6de
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <fnmatch.h>
+#include <sys/wait.h>
+#include <string.h>
+#include <syslog.h>
+
+#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 (file)
index 0000000..89ff5ab
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <gcrypt.h>
+#include <stddef.h>
+#include <time.h>
+#include <limits.h>
+#include <pcre.h>
+#include <netdb.h>
+#include <netinet/in.h>
+
+#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 (file)
index 0000000..012497c
--- /dev/null
@@ -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 (file)
index 0000000..ef31931
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <errno.h>
+#include <ao/ao.h>
+#include <string.h>
+#include <assert.h>
+#include <sys/select.h>
+#include <time.h>
+#include <alsa/asoundlib.h>
+
+#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 (file)
index 0000000..60fea26
--- /dev/null
@@ -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 <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <locale.h>
+#include <stdio.h>
+#include <pcre.h>
+#include <netdb.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+
+#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 (file)
index 0000000..ecfdc8e
--- /dev/null
@@ -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 (file)
index 0000000..7e14fb7
--- /dev/null
@@ -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 (file)
index 0000000..223dc80
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <db.h>
+#include <sys/socket.h>
+#include <pcre.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stddef.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <time.h>
+
+#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 <dir/anything> */
+  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 <dir/component/anything>, so <dir/component> 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 (file)
index 0000000..54e7214
--- /dev/null
@@ -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 (file)
index 0000000..a24e3bf
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <locale.h>
+#include <errno.h>
+#include <string.h>
+
+#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 (file)
index 0000000..cc07db8
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..2614895
--- /dev/null
@@ -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 (file)
index 0000000..5688238
--- /dev/null
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:about.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:about.title@</h1>
+
+   <h2>Copyright</h2>
+
+   <p><a
+   href="http://www.greenend.org.uk/rjk/disorder/">DisOrder
+   version @version@</a> - select and play digital
+   audio files</p>
+
+   <p>Copyright &copy; 2003, 2004, 2005, 2006 Richard Kettlewell</p>
+
+   <p>Portions extracted from
+   <a href="http://mpg321.sourceforge.net/">MPG321</a>,
+   Copyright &copy; 2001 Joe Drew,
+   Copyright &copy; 2000-2001 Robert Leslie</p>
+
+   <p>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.</p>
+
+   <p>This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.</p>
+
+   <p>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</p>
+
+   <h2>Server Statistics</h2>
+
+@stats@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:bc42c6969d7ea0909aa52460cfd633ae -->
diff --git a/templates/choose.html b/templates/choose.html
new file mode 100644 (file)
index 0000000..0475be6
--- /dev/null
@@ -0,0 +1,92 @@
+<!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>@label:choose.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:choose.title@</h1>
+
+   @if{@ne{@arg:directory@}{}@}{
+   <p class=directoryname>@navigate{@arg:directory@}{/<a
+   class=directory
+   href="@url@/?action=choose&#38;directory=@urlquote{@fullname@}@&#38;nonce=@nonce@">@basename@</a>}@:</p>
+   }@
+
+   @if{@isdirectories@}{
+   <div class=directories>
+    <p class=directories>
+     @label:choose.directories@
+    </p>
+    @choose{directories}{
+    <p class=directory>
+     <a class=directory href="@url@/?action=choose&#38;directory=@urlquote{@file@}@&#38;nonce=@nonce@">
+      @transform{@file@}{dir}{display}@
+     </a>
+    </p>
+    }@
+   </div>
+   }@
+   @if{@isfiles@}{
+   <div class=files>
+    <p class=files>
+     @label:choose.files@
+    </p>
+    @choose{files}{
+    <p class=file>
+     <a class=imgprefs href="@url@/?action=prefs&#38;0_file=@urlquote{@resolve{@file@}@}@&#38;nonce=@nonce@">
+      <img class=button src="@label:images.edit@"
+      title="@label:choose.prefsverbose@" alt="@label:choose.prefs@">
+     </a>
+     <a class=file href="@url@/?action=play&#38;file=@urlquote{@file@}@&#38;back=@urlquote{@thisurl@}@&#38;nonce=@nonce@">
+      @transform{@file@}{track}{display}@
+     </a>
+     @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
+     @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
+    </p>
+    }@
+   </div>
+   <p class=allfiles>
+    <a class=imgprefs href="@url@/?action=prefs&#38;directory=@urlquote{@arg:directory@}@&#38;nonce=@nonce@&#38;back=@urlquote{@thisurl@}@">
+      <img class=button src="@label:images.edit@"
+      title="@label:choose.allprefsverbose@" alt="@label:choose.allprefs@">
+    </a>
+    <a class=allfiles href="@url@/?action=play&#38;directory=@urlquote{@arg:directory@}@&#38;nonce=@nonce@&#38;back=@urlquote{@thisurl@}@">
+     @label:choose.playall@
+    </a>
+   </p>
+   }@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:f62f829c03df666f4587b87b08c5e3bf -->
diff --git a/templates/choosealpha.html b/templates/choosealpha.html
new file mode 100644 (file)
index 0000000..be61fd9
--- /dev/null
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:choose.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:choose.title@</h1>
+
+   <p class=choosealpha>
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?a">A</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?b">B</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?c">C</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?d">D</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?e">E</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?f">F</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?g">G</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?h">H</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?i">I</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?j">J</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?k">K</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?l">L</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?m">M</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?n">N</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?o">O</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?p">P</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?q">Q</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?r">R</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?s">S</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(?!the [^t])t">T</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?u">U</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?v">V</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?w">W</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?x">X</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?y">Y</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^(the )?z">Z</a> |
+    <a class=choosealpha href="@url@?action=choose&#38;regexp=^[^a-z]">*</a>
+   </p>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:be6e8dfa8452199ee4f5aa988881ec03 -->
diff --git a/templates/credits.html b/templates/credits.html
new file mode 100644 (file)
index 0000000..ee0fe08
--- /dev/null
@@ -0,0 +1,25 @@
+<p class=credits><a
+href="http://www.greenend.org.uk/rjk/disorder/"
+title="DisOrder web site">DisOrder
+version @version@</a> &copy; 2003, 2004, 2005, 2006 Richard Kettlewell</p>
+@@
+<!--
+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
+-->
+<!-- arch-tag:625eff5d601a737c8dfb1bf2ff4320d4 -->
diff --git a/templates/disorder.css b/templates/disorder.css
new file mode 100644 (file)
index 0000000..8a42b45
--- /dev/null
@@ -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 <tr> 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 (file)
index 0000000..a2ed445
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:error.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:error.title@</h1>
+    
+    <p>@label{error.@label:error@}@</p>
+
+    <p>@label:error.generic@</p>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:5UQWniMns+CWWMBBF0qoUg -->
diff --git a/templates/help.html b/templates/help.html
new file mode 100644 (file)
index 0000000..34d6e26
--- /dev/null
@@ -0,0 +1,355 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include{stdhead}@
+  <title>@label{help.title}@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label{help.title}@</h1>
+
+   <h2 class=sidebarlink><a name=playing>Playing</a></h2>
+
+   <div class=helpsection>
+
+    <p>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.</p>
+
+    <p>Each track has a <img class=button
+       src="@label:images.scratch@"
+       title="@label:playing.scratch@"
+       alt="@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.</p>
+
+    <p>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 <a
+    href="@url@?action=disorder_config.5">disorder_config(5)</a> for more
+    details.</p>
+
+   <p>Artist and album names are hyperlinks to the relevant locations
+   in the <a href="#choose">Choose</a> screen (see below).</p>
+
+   </div>
+
+   <h2 class=sidebarlink><a name=manage>Manage</a></h2>
+
+   <div class=helpsection>
+
+    <p>This screen is almost identical to <a
+    href="#playing">Playing</a> except that it includes extra
+    management features.</p>
+
+   <p>At the top of the screen are the following controls:</p>
+
+   <ul>
+    <li>Pause.  This button can be used to pause playing (provided the
+    player supports it).  <img width=16 height=16 class=imgbutton
+    src="@label:images.enabled@"> indicates that playing is paused,
+    <img width=16 height=16 class=imgbutton
+    src="@label:images.disabled@"> that it is not.</li>
+
+    <li>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.  <img width=16 height=16 class=imgbutton
+    src="@label:images.enabled@"> indicates that random play is
+    enabled, <img width=16 height=16 class=imgbutton
+    src="@label:images.disabled@"> that it is disabled.</li>
+
+    <li>Enable/disable play.  If disabled then tracks in the queue
+    will not be played, but will remain in the queue instead.  <img
+    width=16 height=16 class=imgbutton src="@label:images.enabled@">
+    indicates that play is enabled, <img width=16 height=16
+    class=imgbutton src="@label:images.disabled@"> that it is
+    disabled.</li>
+
+    <li>Volume control.  You can use the <img class=button
+       src="@label:images.up@"
+       title="@label:volume.increase@"
+       alt="@label:volume.increase@"> and <img
+       src="@label:images.down@"
+       title="@label:volume.reduce@"
+       alt="@label:volume.reduce@"> buttons to increase or
+    decrease the volume, or enter new volume settings for the left
+    and/or right speakers.</li>
+
+   </ul>
+
+   <p>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 <img class=button src="@label:images.up@"
+     title="@label:playing.up@" alt="@label:playing.up@"> and <img
+     src="@label:images.down@" title="@label:playing.down@"
+     alt="@label:playing.down@"> buttons on each track move that
+    track around in the queue.  Similarly the <img class=button
+     src="@label:images.upall@" title="@label:playing.upall@"
+     alt="@label:playing.upall@"> and <img
+     src="@label:images.downall@" title="@label:playing.downall@"
+     alt="@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.</p>
+
+   </div>
+
+   <h2 class=sidebarlink><a name=recent>Recent</a></h2>
+
+   <div class=helpsection>
+
+    <p>This screen displays recently played tracks, most recent first.
+    The <img class=button src="@label:images.edit@"
+    title="@label:choose.prefs@" alt="@label:choose.prefs@">
+    button can be used to edit the details for a track; see <a
+    href="#prefs">Editing Preferences</a> below.</p>
+
+    <p>The number of tracks remembered is controlled by the server
+    configuration.  See the "history" option in <a
+    href="@url@?action=disorder_config.5">disorder_config(5)</a> for more
+    details.</p>
+
+   </div>
+
+   <h2 class=sidebarlink><a name=choose>Choose</a></h2>
+
+   <div class=helpsection>
+
+    <p>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:</p>
+
+    <table class=helpbuttons>
+     <tbody>
+      <tr>
+       <td class=helpbuttons><img
+       class=button src="@label:images.edit@"
+       title="@label:choose.prefs@"
+       alt="@label:choose.prefs@"></td>
+       <td class=helpbuttons>This button can be used to edit the details for a
+       track; see <a href="#prefs">Editing Preferences</a> below.</td>
+      </tr>
+      <tr>
+       <td class=helpbuttons><span class=button>@label{choose.playall}@</span></td>
+       <td class=helpbuttons>This button plays all the tracks in a directory,
+       in order.  This is used to efficiently play a whole album.</td>
+      </tr>
+     </tbody>
+    </table>
+
+    <p>This screen has two forms: <a
+    href="@url@?action=choose&#38;nonce=@nonce@">choose</a>, which give
+    you all the top-level directories at once, and <a
+    href="@url@?action=choosealpha&#38;nonce=@nonce@">choosealpha</a>,
+    which breaks them down by initial letter.</p>
+
+   </div>
+
+   <h2 class=sidebarlink><a name=prefs>Editing Preferences</a></h2>
+
+   <div class=helpsection>
+
+    <p>This screen, reached from <a href="#choose">Choose</a> or <a
+    href="#recent">Recent</a>, is used to edit a track's preferences.
+     Preferences can be edited in two ways.</p>
+    
+    <p>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.</p>
+
+    <p>Tags are separated by commas and can contain any other printing
+    characters (including spaces).  Leading and trailing spaces are
+    not significant.</p>
+
+    <p>Random play for any given track is enabled by default, but you
+    can use this screen to disable it for undesirable tracks.</p>
+
+    <p>Below this are "raw" preferences, which allow individual
+    database fields to be modified.</p>
+
+    <p>To change an existing preference, edit its value and press its
+    <span class=button>@label{prefs.set}@</span> button.</p>
+
+    <p>To delete an existing preference, press its
+    <span class=button>@label{prefs.delete}@</span> button.</p>
+
+    <p>To add a new preference, enter its name and value in the box at the
+    bottom and press the <span class=button>@label{prefs.new}@</span> button.
+    If the preference exists already it will be overwritten.</p>
+
+
+    <p>Preferences can have any name or value but certain names have special
+    significance:</p>
+
+    <table class=helpprefs>
+     <tbody>
+      <tr>
+       <td class=helpprefs><span class=helppref>pick_at_random</span></td>
+       <td class=helpprefs>If this preference is present and set to "0" then
+       the track will not be picked for random play.  Otherwise it may be.</td>
+      </tr>
+      <tr>
+       <td class=helpprefs><span class=helppref>trackname_<span class=helpprefbit>context</span>_<span class=helpprefbit>part</span></span></td>
+       <td class=helpprefs>These preferences can be used to override the
+       filename parsing rules to find a track name part.  <span
+       class=helppref>trackname_<span class=helpprefbit>part</span></span> will
+       be used if the full version is not present.</td>
+     </tbody>
+    </table>
+
+    <p><span class=helpprefbit>context</span> can be anything but standard
+    values are:</p>
+
+    <table class=helpcontexts>
+     <tbody>
+      <tr>
+       <td class=helpcontexts><span class=helpcontext>display</span></td>
+       <td class=helpcontexts>Displayed in a web page</td>
+      </tr>
+      <tr>
+       <td class=helpcontexts><span class=helpcontext>sort</span></td>
+       <td class=helpcontexts>Used when sorting track names</td>
+      </tr>
+     </tbody>
+    </table>
+
+    <p><span class=helpprefbit>part</span> can be anything too but standard
+    values are "artist", "album" and "title", with the obvious meanings.</p>
+
+    <p>See also <a href="@url@?action=disorder.1">disorder(1)</a> and <a
+    href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+    details.</p>
+
+   </div>
+
+   <h2 class=sidebarlink>Search</h2>
+
+   <div class=helpsection>
+
+    <p>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.</p>
+
+    <p>It is possible to limit results to tracks with a particular
+    tag, by using <b>tag:</b><i>TAG</i> among the search terms.</p>
+
+    <p>Some keywords, known as "stopwords", are excluded from the search, and
+    will never match.  See the "stopword" option in <a
+    href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+    details about this.</p>
+
+   </div>
+
+  @if{@eq{@label:menu@}{sidebar}@}
+     {
+
+   <h2 class=sidebarlink>Volume</h2>
+
+   <div class=helpsection>
+
+    <p>This screen allows you to set the playback volume, if this is enabled in
+    the server configuration.  See the "channel" and "mixer" options in <a
+    href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+    details about this.</p>
+
+   </div>
+
+     }{<!-- volume currently only linked in sidebar menu -->}@ 
+
+  <h2 class=sidebarlink>Troubleshooting</h2>
+
+  <div class=helpsection>
+
+   <p>If you cannot play a track, or it does not appear in the
+   database even after a rescan, check the following things:</p>
+
+   <ul>
+
+    <li>Are there any error messages in the system log?  The server
+    logs to <tt>LOG_DAEMON</tt>, which typically ends up in
+    <i>/var/log/daemon.log</i> or <i>/var/log/messages</i>, though
+    this depends on local configuration.
+
+    <li>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.
+
+    <li>Do permissions on the track allow the server to read it?
+     
+    <li>Do the permissions on the containing directories allow the
+    server to read and execute them?
+
+   </ul>
+
+   <p>The user the server runs as is determined by the <tt>user</tt>
+   directive in the configuration file.  The README recommends using
+   <b>jukebox</b> for this purpose but it could be different
+   locally.</p>
+
+  </div>
+
+   <h2 class=sidebarlink>Man Pages</h2>
+
+   <div class=helpsection>
+
+    <p><a href="@url@?action=disorder_config.5">disorder_config(5)</a> -
+     configuration</p>
+
+    <p><a href="@url@?action=disorder.1">disorder(1)</a> - command line
+     client</p>
+
+    <p><a href="@url@?action=disobedience.1">disobedience(1)</a> - GTK+
+     client</p>
+
+    <p><a href="@url@?action=tkdisorder.1">tkdisorder(1)</a> - GUI
+     client</p>
+
+    <p><a href="@url@?action=disorderd.8">disorderd(8)</a> - server</p>
+
+    <p><a href="@url@?action=disorder-dump.8">disorder-dump(8)</a> -
+     dump/restore preferences database</p>
+
+    <p><a href="@url@?action=disorder.3">disorder(3)</a> - C API</p>
+
+    <p><a href="@url@?action=disorder_protocol.5">disorder_protocol(5)</a> -
+     DisOrder control protocol</p>
+
+   </div>
+
+@include{@label{menu}@end}@
+  </div>
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:eb6205dbb8596ff6fb3290e4625a2ada -->
diff --git a/templates/options b/templates/options
new file mode 100644 (file)
index 0000000..c054770
--- /dev/null
@@ -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 (file)
index 0000000..52e744c
--- /dev/null
@@ -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 (file)
index 0000000..f0d47c9
--- /dev/null
@@ -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.
+
+# <TITLE> for the 'Playing' screen
+label  playing.title           "Now Playing"
+
+label  playing.randomtrack     &nbsp;
+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 (file)
index 0000000..c67ab31
--- /dev/null
@@ -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@}@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:playing.title@</h1>
+
+   @#{extra control buttons for the management page}@
+   @if{@arg:mgmt@}{
+   <p class=mgmt>
+    @if{@paused@}{
+    <!-- paused -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=resume&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.resumeverbose@">@label:playing.pause@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+    }{
+    <!-- not paused -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=pause&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.pauseverbose@">@label:playing.pause@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+    }@
+    @if{@random-enabled@}{
+    <!-- random played enabled -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=random-disable&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.randomdisableverbose@">@label:playing.random@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+    }{
+    <!-- random played disabled -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=random-enable&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.randomenableverbose@">@label:playing.random@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+    }@
+    @if{@enabled@}{
+    <!-- playing enabled -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=disable&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.disableverbose@">@label:playing.playing@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+    }{
+    <!-- playing disabled -->
+    <span class=button>
+    <a class=button
+    href="@url@?action=enable&#38;nonce=@nonce@&#38;mgmt=true"
+     title="@label:playing.enableverbose@">@label:playing.playing@</a>
+    </a>
+    </span>
+    <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+    }@
+    <form class=volume action="@url@" method=POST
+     enctype="multipart/form-data" accept-charset=utf-8>
+    <span class=volume>
+     @label:playing.volume@
+     <a class=imgbutton
+      href="@url@?action=volume&#38;delta=-@label:volume.resolution@&#38;back=@urlquote{@thisurl@?mgmt=true}@">
+      <img class=button src="@label:images.down@"
+       alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
+     </a>
+     @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
+     @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
+     <input name=nonce type=hidden value="@nonce@">
+     <input name=back type=hidden value="@thisurl@?mgmt=true">
+     <button class=search name=action type=submit value=volume>
+      @label:volume.set@
+     </button>
+     <a class=imgbutton
+      href="@url@?action=volume&#38;delta=@label:volume.resolution@&#38;back=@urlquote{@thisurl@?mgmt=true}@">
+      <img class=button src="@label:images.up@"
+       alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
+     </a>
+    </form>
+    </span>
+   }@
+
+@#{only display the table if there is something to put in it}@
+@if{@or{@isplaying@}{@isqueue@}@}{
+   <table class=playing>
+     <tr class=headings>
+      <th class=when>@label:heading.when@</th>
+      <th class=who>@label:heading.who@</th>
+      <th class=artist>@label:heading.artist@</th>
+      <th class=album>@label:heading.album@</th>
+      <th class=title>@label:heading.title@</th>
+      <th class=length>@label:heading.length@</th>
+      <th class=button>&nbsp;</th>
+      @if{@arg:mgmt@}{
+      <th class=imgbutton>&nbsp;</th>
+      <th class=imgbutton>&nbsp;</th>
+      <th class=imgbutton>&nbsp;</th>
+      <th class=imgbutton>&nbsp;</th>
+      }@
+     </tr>
+     @if{@isplaying@}{
+     <tr class=nowplaying>
+      <td colspan=@if{@arg:mgmt@}{11}{7}@ class=nowplaying>@label:playing.now@</td>
+     </tr>
+     @playing{
+     <tr class=playing>
+      <td class=when>@when@</td>
+      <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:playing.randomtrack@}{&nbsp;}@}{@who@}@</td>
+      <td class=artist><a class=directory
+       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
+       title="@label:playing.artistverbose@">@part:artist@</a></td>
+      <td class=album><a class=directory
+       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@"
+       title="@label:playing.albumverbose@">@part:album@</a></td>
+      <td class=title>@part:title@</td>
+      <td class=length>@length@</td>
+      <td class=imgbutton>@if{@scratchable@}{<a class=imgbutton
+       href="@url@?action=scratch&#38;nonce=@nonce@&#38;id=@id@&#38;mgmt=@arg:mgmt@"><img
+       class=button src="@label:images.scratch@"
+       title="@label:playing.scratchverbose@"
+       alt="@label:playing.scratch@"></a>}{<img
+       class=button src="@label:images.noscratch@"
+       title="@label:playing.scratchverbose@"
+       alt="@label:playing.scratch@">}@</td>
+      @if{@arg:mgmt@}{
+      <td class=imgbutton>&nbsp;</td>
+      <td class=imgbutton>&nbsp;</td>
+      <td class=imgbutton>&nbsp;</td>
+      <td class=imgbutton>&nbsp;</td>
+      }@
+     </tr>
+     }@}@
+     @if{@isqueue@}{
+     <tr class=next>
+      <td colspan=@if{@arg:mgmt@}{11}{7}@ class=next>@label:playing.next@</td>
+     </tr>
+     @queue{
+     <tr class=@parity@>
+      <td class=when>@when@</td>
+      <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:queue.randomtrack@}{&nbsp;}@}{@who@}@</td>
+      <td class=artist><a class=directory href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@">@part:artist@</a></td>
+      <td class=album><a class=directory href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@">@part:album@</a></td>
+      <td class=title>@part:title@</td>
+      <td class=length>@length@</td>
+      <td class=imgbutton>@if{@removable@}{<a class=imgbutton
+       href="@url@?action=remove&#38;nonce=@nonce@&#38;id=@id@&#38;mgmt=@arg:mgmt@"><img
+       class=button src="@label:images.scratch@"
+       title="@label:playing.removeverbose@" 
+       alt="@label:playing.remove@"></a>}{&nbsp;}@</td>
+      @if{@arg:mgmt@}{
+      @if{@isfirst@}
+    {<td class=imgbutton>
+      <img
+       class=button src="@label:images.noupall@"
+       title="@label:playing.upallverbose@" alt="">
+     <td class=imgbutton>
+      <img
+       class=button src="@label:images.noup@"
+       title="@label:playing.upverbose@" alt="">}
+    {<td class=imgbutton>
+      <a class=imgbutton
+        href="@url@?action=move&#38;nonce=@nonce@&#38;id=@id@&#38;delta=2147483647&#38;mgmt=true"><img
+       class=button src="@label:images.upall@"
+       title="@label:playing.upallverbose@"
+       alt="@label:playing.upall@"></a>
+     <td class=imgbutton>
+     <a class=imgbutton
+        href="@url@?action=move&#38;nonce=@nonce@&#38;id=@id@&#38;delta=1&#38;mgmt=true"><img
+       class=button src="@label:images.up@"
+       title="@label:playing.upverbose@" alt="@label:playing.up@"></a>}@
+      @if{@islast@}
+    {<td class=imgbutton>
+      <img
+       class=button src="@label:images.nodown@"
+       title="@label:playing.downverbose@" alt="">
+     <td class=imgbutton>
+     <img
+       class=button src="@label:images.nodownall@"
+       title="@label:playing.downallverbose@" alt="">}
+    {<td class=imgbutton>
+      <a class=imgbutton href="@url@?action=move&#38;nonce=@nonce@&#38;id=@id@&#38;delta=-1&#38;mgmt=true"><img
+       class=button src="@label:images.down@"
+       title="@label:playing.downverbose@"
+       alt="@label:playing.down@">
+     <td class=imgbutton>
+     <a class=imgbutton href="@url@?action=move&#38;nonce=@nonce@&#38;id=@id@&#38;delta=-2147483647&#38;mgmt=true"><img
+       class=button src="@label:images.downall@"
+       title="@label:playing.downallverbose@"
+       alt="@label:playing.downall@">}@</a>
+      }@
+     </tr>
+     }@}@
+   </table>
+}@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:0a255d8605a49fdb5ca6213aefbb21f2 -->
diff --git a/templates/prefs.html b/templates/prefs.html
new file mode 100644 (file)
index 0000000..ac7c8ce
--- /dev/null
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:prefs.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:prefs.title@</h1>
+
+   <form class=prefs action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+    <input type=hidden name="files" value="@nfiles@">
+    <input type=hidden name=nonce value=@nonce@>
+    <input type=hidden name=parts value="artist album title">
+   @files{
+   <p class="prefs_head">Preferences for <span class="prefs_track">@arg{@index@_file}@</span></p>
+    <input type=hidden name="@index@_file" value="@arg{@index@_file}@">
+    <table class=prefs>
+      <tr class="prefs_headings">
+       <th class="prefs_name">@label:prefs.name@</th>
+       <th class="prefs_value">@label:prefs.value@</th>
+      </tr>
+      <tr class=even>
+       <td class="prefs_name">@label:heading.title@</td>
+       <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_title" value="@part{display}{title}{@arg{@index@_file}@}@"></td>
+      </tr>
+      <tr class=odd>
+       <td class="prefs_name">@label:heading.album@</td>
+       <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_album" value="@part{display}{album}{@arg{@index@_file}@}@"></td>
+      </tr>
+      <tr class=even>
+       <td class="prefs_name">@label:heading.artist@</td>
+       <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_artist" value="@part{display}{artist}{@arg{@index@_file}@}@"></td>
+      </tr>
+      <tr class=odd>
+       <td class="prefs_name">@label:prefs.tags@</td>
+       <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_tags" value="@pref{@arg{@index@_file}@}{tags}@"></td>
+      </tr>
+      <tr class=even>
+       <td class="prefs_name">@label:prefs.random@</td>
+       <td class="prefs_value"><input class="prefs_value" type=checkbox
+        name="@index@_random" value=true
+       @if{@ne{@pref{@arg{@index@_file}@}{pick_at_random}@}{0}@}{ checked}{}@></td>
+    </table>
+   }@
+    
+    <p>
+     <button class="pref_set" type=submit name=action value=prefs>
+      @label:prefs.set@
+     </button>
+    </p>
+   </form>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:bc3f0280181afcc8d930e2118e1a2e53 -->
diff --git a/templates/recent.html b/templates/recent.html
new file mode 100644 (file)
index 0000000..320dd0d
--- /dev/null
@@ -0,0 +1,72 @@
+<!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>@label:recent.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+  <h1 class=title>@label:recent.title@</h1>
+
+@#{only display the table if there is something to put in it}@
+@if{@isrecent@}{
+  <table class=recent>
+    <tr class=headings>
+     <th class=when>@label:heading.when@</th>
+     <th class=who>@label:heading.who@</th>
+     <th class=artist>@label:heading.artist@</th>
+     <th class=album>@label:heading.album@</th>
+     <th class=title>@label:heading.title@</th>
+     <th class=length>@label:heading.length@</th>
+      <th class=button>&nbsp;</th>
+    </tr>
+    @recent{
+    <tr class=@parity@>
+     <td class=when>@when@</td>
+     <td class=who>@who@</td>
+     <td class=artist><a class=directory href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@">@part:artist@</a></td>
+     <td class=album><a class=directory href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@">@part:album@</a></td>
+     <td class=title>@part:title@</td>
+     <td class=length>@length@</td>
+     <td class=imgbutton><a class=imgbutton
+      href="@url@?action=prefs&#38;nonce=@nonce@&#38;0_file=@urlquote{@file@}@"><img
+       class=button src="@label:images.edit@"
+       title="@label:choose.prefsverbose@"
+       alt="@label:choose.prefs@"></a></td>
+    </tr>
+    }@
+  </table>
+}@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:48fdc42a2503829d74876a84289a0eaa -->
diff --git a/templates/search.html b/templates/search.html
new file mode 100644 (file)
index 0000000..6d153cf
--- /dev/null
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:search.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:search.title@</h1>
+
+   <form class=search action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+    <div class=search>
+     <input class=query name=query type=text value="@arg:query@"
+      size=32>
+     <button class=search name=action type=submit value=search>
+      @label:search.search@
+     </button>
+     <input name=nonce type=hidden value="@nonce@">
+    </div>
+   </form>
+
+   <div class=searchresults>
+    @search{artist}{display}{
+    <div class="search_artist">
+     <p class="search_artist">Artist:
+      <span class="search_artist">@part:artist@</span></p>
+     @search{album}{display}{
+     <div class="search_album">
+      <p class="search_album">Album:
+       <span class="search_album">@part:album@</span></p>
+      @search{title}{
+      <div class="search_title">
+       <p class="search_title">Title:
+       <a href="@url@/?action=play&#38;file=@urlquote{@file@}@&#38;back=@urlquote{@thisurl@}@&#38;nonce=@nonce@">@part:title@</a>
+       @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
+       @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
+       </p>
+      </div>
+      }@
+     </div>
+     }@
+    </div>
+    }@
+   </div>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:a4f39935b0c2a424c601c796ebf1a023 -->
diff --git a/templates/sidebar.html b/templates/sidebar.html
new file mode 100644 (file)
index 0000000..d0d3a69
--- /dev/null
@@ -0,0 +1,48 @@
+<div id=sidebar>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@">@label:sidebar.playing@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=recent&amp;nonce=@nonce@">@label:sidebar.recent@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=@label:sidebar.choosewhich@&amp;nonce=@nonce@">@label:sidebar.choose@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=search&amp;nonce=@nonce@">@label:sidebar.search@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=volume&amp;nonce=@nonce@">@label:sidebar.volume@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?mgmt=true">@label:sidebar.manage@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=help&amp;nonce=@nonce@">@label:sidebar.help@</a>
+ </p>
+ <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=about&amp;nonce=@nonce@">@label:sidebar.about@</a>
+ </p>
+</div>
+<div id=content>
+@@
+<!--
+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
+-->
+<!-- arch-tag:4d13881dcc3c1c93ff42a9ed5ca0f737 -->
diff --git a/templates/sidebarend.html b/templates/sidebarend.html
new file mode 100644 (file)
index 0000000..551fd41
--- /dev/null
@@ -0,0 +1,23 @@
+@include:credits@
+</div>
+@@
+<!--
+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
+-->
+<!-- arch-tag:NDGZgbsXX8WFUdqrRBzhcQ -->
diff --git a/templates/stdhead.html b/templates/stdhead.html
new file mode 100644 (file)
index 0000000..7b0fbb4
--- /dev/null
@@ -0,0 +1,23 @@
+@include:stylesheet@
+@@
+Anything that goes in all html HEAD elements goes here.
+<!--
+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
+-->
+<!-- arch-tag:82672a6cf795696767ec187456e17c86 -->
diff --git a/templates/stylesheet.html b/templates/stylesheet.html
new file mode 100644 (file)
index 0000000..6632216
--- /dev/null
@@ -0,0 +1,24 @@
+  <link rel=stylesheet type="text/css" href="@label:links.css@">
+@@
+This file is a standard place to put a link to a stylesheet,
+or an embedded stylesheet.
+<!--
+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
+-->
+<!-- arch-tag:d243bbe6e40ef52213cf0f72d92a7c6a -->
diff --git a/templates/topbar.html b/templates/topbar.html
new file mode 100644 (file)
index 0000000..bb760f5
--- /dev/null
@@ -0,0 +1,53 @@
+<p class=menubar>
+  <a class=@if{@eq{@action@}{playing}@}{activemenu}{inactivemenu}@
+ href="@url@"
+ title="@label:sidebar.playingverbose@">@label:sidebar.playing@</a>
+  <a class=@if{@eq{@action@}{recent}@}{activemenu}{inactivemenu}@
+ href="@url@?action=recent&amp;nonce=@nonce@"
+ title="@label:sidebar.recentverbose@">@label:sidebar.recent@</a>
+  <a class=@if{@or{@eq{@action@}{choose}@}
+                  {@eq{@action@}{choosealpha}@}@}
+              {activemenu}
+              {inactivemenu}@
+ href="@url@?action=@label:sidebar.choosewhich@&amp;nonce=@nonce@"
+ title="@label:sidebar.chooseverbose@">@label:sidebar.choose@</a>
+  <a class=@if{@eq{@action@}{search}@}{activemenu}{inactivemenu}@
+ href="@url@?action=search&amp;nonce=@nonce@"
+ title="@label:sidebar.searchverbose@">@label:sidebar.search@</a>
+<!-- disabled by default since now available from 'manage'
+  <a class=@if{@eq{@action@}{volume}@}{activemenu}{inactivemenu}@
+ href="@url@?action=volume&amp;nonce=@nonce@"
+ title="@label:sidebar.volumeverbose@">@label:sidebar.volume@</a>
+-->
+  <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
+ href="@url@?mgmt=true"
+ title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
+  <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
+ href="@url@?action=help&amp;nonce=@nonce@"
+ title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>
+  <a class=@if{@eq{@action@}{about}@}{activemenu}{inactivemenu}@
+ href="@url@?action=about&amp;nonce=@nonce@"
+ title="@label:sidebar.aboutverbose@">@label:sidebar.about@</a>
+</p>
+<hr>
+@@
+<!--
+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
+-->
+<!-- arch-tag:zo3pu0olF7vPOu0nAx703g -->
diff --git a/templates/topbarend.html b/templates/topbarend.html
new file mode 100644 (file)
index 0000000..fd888ef
--- /dev/null
@@ -0,0 +1,22 @@
+@include:credits@
+@@
+<!--
+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
+-->
+<!-- arch-tag:8+NViAUIcjCGcp5rEhCQ6A -->
diff --git a/templates/volume.html b/templates/volume.html
new file mode 100644 (file)
index 0000000..abb3d63
--- /dev/null
@@ -0,0 +1,63 @@
+<!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>@label:volume.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:volume.title@</h1>
+
+   <form class=volume action="@url@" method=POST>
+    <p class=volume>
+     <a class=imgbutton
+      href="@url@?action=volume&#38;delta=-@label:volume.resolution@">
+      <img class=button src="@label:images.down@"
+       alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
+     </a>
+     @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
+     @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
+     <input name=nonce type=hidden value="@nonce@">
+     <button class=search name=action type=submit value=volume>
+      @label:volume.set@
+     </button>
+     <a class=imgbutton
+      href="@url@?action=volume&#38;delta=@label:volume.resolution@">
+      <img class=button src="@label:images.up@"
+       alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
+     </a>
+    </p>
+   </form>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:2aa86adb876245eb10d501168f30db07 -->