From 460b9539a7c15580e41a71bbc0f47ae776238915 Mon Sep 17 00:00:00 2001 Message-Id: <460b9539a7c15580e41a71bbc0f47ae776238915.1713483417.git.mdw@distorted.org.uk> From: Mark Wooding Date: Tue, 23 Jan 2007 21:23:45 +0000 Subject: [PATCH] Import from Arch revision: rjk@greenend.org.uk--2004/disorder--mainline--0.1--patch-328 Organization: Straylight/Edgeware From: rjk@greenend.org.uk <> --- BUGS | 42 + CHANGES | 321 ++ ChangeLog.d/cvs--ChangeLog | 881 ++++ ChangeLog.d/disorder--mainline--0.1 | 6487 +++++++++++++++++++++++++++ Makefile.am | 27 + README | 334 ++ README.client | 47 + README.raw | 55 + README.streams | 32 + README.upgrades | 112 + TODO | 26 + acinclude.m4 | 105 + clients/Makefile.am | 70 + clients/authorize.c | 104 + clients/authorize.h | 35 + clients/disorder.c | 554 +++ clients/disorderfm.c | 414 ++ clients/filename-bytes.c | 42 + clients/test-eclient.c | 189 + configure.ac | 383 ++ debian/Makefile.am | 35 + debian/README.Debian | 37 + debian/autorules.m4 | 119 + debian/changelog | 90 + debian/conffiles | 4 + debian/config | 55 + debian/control | 15 + debian/copyright | 25 + debian/disorder.config | 50 + debian/htaccess | 5 + debian/options.debian | 27 + debian/postinst | 98 + debian/postrm | 27 + debian/prerm | 23 + debian/rules.m4 | 73 + debian/templates | 28 + disobedience/Makefile.am | 47 + disobedience/TODO | 6 + disobedience/choose.c | 999 +++++ disobedience/client.c | 176 + disobedience/control.c | 316 ++ disobedience/disobedience.c | 379 ++ disobedience/disobedience.h | 180 + disobedience/disobedience.rc | 78 + disobedience/menu.c | 150 + disobedience/misc.c | 97 + disobedience/properties.c | 438 ++ disobedience/queue.c | 1279 ++++++ doc/Makefile.am | 47 + doc/checklist.txt | 80 + doc/disobedience.1.in | 223 + doc/disorder-deadlock.8.in | 47 + doc/disorder-dump.8.in | 116 + doc/disorder-rescan.8.in | 48 + doc/disorder.1.in | 346 ++ doc/disorder.3 | 430 ++ doc/disorder_config.5.in | 1022 +++++ doc/disorder_protocol.5.in | 444 ++ doc/disorderd.8.in | 164 + doc/disorderfm.1.in | 100 + doc/tkdisorder.1 | 60 + driver/Makefile.am | 29 + driver/disorder.c | 170 + examples/Makefile.am | 31 + examples/config.sample.in | 59 + examples/disorder-log | 70 + examples/disorder.init.in | 68 + images/Makefile.am | 30 + images/cross.png | Bin 0 -> 226 bytes images/down.png | Bin 0 -> 181 bytes images/downdown.png | Bin 0 -> 183 bytes images/edit.png | Bin 0 -> 559 bytes images/nocross.png | Bin 0 -> 341 bytes images/nodown.png | Bin 0 -> 254 bytes images/nodowndown.png | Bin 0 -> 246 bytes images/notes.png | Bin 0 -> 248 bytes images/notescross.png | Bin 0 -> 444 bytes images/noup.png | Bin 0 -> 234 bytes images/noupup.png | Bin 0 -> 235 bytes images/pause.png | Bin 0 -> 90 bytes images/play.png | Bin 0 -> 179 bytes images/random.png | Bin 0 -> 211 bytes images/randomcross.png | Bin 0 -> 440 bytes images/tick.png | Bin 0 -> 242 bytes images/up.png | Bin 0 -> 179 bytes images/upup.png | Bin 0 -> 176 bytes lib/Makefile.am | 93 + lib/addr.c | 101 + lib/addr.h | 39 + lib/asprintf.c | 90 + lib/authhash.c | 66 + lib/authhash.h | 35 + lib/basen.c | 81 + lib/basen.h | 42 + lib/cache.c | 108 + lib/cache.h | 54 + lib/casefold.h | 915 ++++ lib/charset.c | 146 + lib/charset.h | 68 + lib/client-common.c | 86 + lib/client-common.h | 40 + lib/client.c | 613 +++ lib/client.h | 192 + lib/configuration.c | 934 ++++ lib/configuration.h | 155 + lib/defs.c | 40 + lib/defs.h | 39 + lib/disorder.h | 215 + lib/eclient.c | 1234 +++++ lib/eclient.h | 271 ++ lib/event.c | 844 ++++ lib/event.h | 248 + lib/eventlog.c | 93 + lib/eventlog.h | 51 + lib/filepart.c | 71 + lib/filepart.h | 43 + lib/fprintf.c | 52 + lib/hash.c | 174 + lib/hash.h | 69 + lib/hex.c | 94 + lib/hex.h | 44 + lib/inputline.c | 61 + lib/inputline.h | 37 + lib/kvp.c | 195 + lib/kvp.h | 66 + lib/log-impl.h | 53 + lib/log.c | 180 + lib/log.h | 89 + lib/logfd.c | 96 + lib/logfd.h | 39 + lib/mem-impl.h | 37 + lib/mem.c | 124 + lib/mem.h | 66 + lib/mime.c | 374 ++ lib/mime.h | 66 + lib/mixer.c | 114 + lib/mixer.h | 43 + lib/plugin.c | 304 ++ lib/plugin.h | 132 + lib/printf.c | 494 ++ lib/printf.h | 74 + lib/queue.c | 512 +++ lib/queue.h | 137 + lib/regsub.c | 166 + lib/regsub.h | 46 + lib/selection.c | 87 + lib/selection.h | 56 + lib/signame.c | 154 + lib/signame.h | 37 + lib/sink.c | 105 + lib/sink.h | 67 + lib/snprintf.c | 97 + lib/speaker.c | 109 + lib/speaker.h | 61 + lib/split.c | 169 + lib/split.h | 31 + lib/syscalls.c | 151 + lib/syscalls.h | 74 + lib/table.c | 50 + lib/table.h | 46 + lib/test.c | 418 ++ lib/trackname.c | 144 + lib/trackname.h | 66 + lib/types.h | 115 + lib/unicodegc.h | 1614 +++++++ lib/user.c | 65 + lib/user.h | 36 + lib/utf8.c | 41 + lib/utf8.h | 66 + lib/vacopy.h | 40 + lib/vector.c | 49 + lib/vector.h | 69 + lib/words.c | 177 + lib/words.h | 40 + lib/wstat.c | 58 + lib/wstat.h | 36 + plugins/Makefile.am | 43 + plugins/exec.c | 58 + plugins/execraw.c | 12 + plugins/fs.c | 85 + plugins/mad.c | 232 + plugins/madshim.h | 41 + plugins/notify.c | 93 + plugins/shell.c | 69 + plugins/tracklength.c | 265 ++ prepare | 46 + python/Makefile.am | 33 + python/disorder.py.in | 1045 +++++ python/tkdisorder | 475 ++ scripts/Makefile.am | 24 + scripts/check | 78 + scripts/completion.bash | 63 + scripts/copyright.exceptions | 25 + scripts/dist | 38 + scripts/htmlman | 52 + scripts/inst | 29 + scripts/makedeb | 37 + scripts/oggrename | 58 + scripts/sedfiles.make | 36 + scripts/text2c | 15 + server/Makefile.am | 84 + server/api-client.c | 73 + server/api-client.h | 33 + server/api-server.c | 60 + server/api.c | 78 + server/cgi.c | 619 +++ server/cgi.h | 108 + server/cgimain.c | 80 + server/daemonize.c | 89 + server/daemonize.h | 39 + server/dcgi.c | 1482 ++++++ server/dcgi.h | 66 + server/deadlock.c | 124 + server/disorderd.c | 275 ++ server/dump.c | 458 ++ server/play.c | 725 +++ server/play.h | 91 + server/rescan.c | 331 ++ server/server.c | 1105 +++++ server/server.h | 48 + server/speaker.c | 634 +++ server/state.c | 171 + server/state.h | 38 + server/trackdb-int.h | 124 + server/trackdb.c | 1833 ++++++++ server/trackdb.h | 130 + server/trackname.c | 96 + sounds/Makefile.am | 24 + sounds/scratch.ogg | Bin 0 -> 10804 bytes sounds/slap.ogg | Bin 0 -> 8502 bytes templates/Makefile.am | 31 + templates/about.html | 75 + templates/choose.html | 92 + templates/choosealpha.html | 72 + templates/credits.html | 25 + templates/disorder.css | 473 ++ templates/error.html | 46 + templates/help.html | 355 ++ templates/options | 12 + templates/options.columns | 4 + templates/options.labels | 201 + templates/playing.html | 240 + templates/prefs.html | 86 + templates/recent.html | 72 + templates/search.html | 78 + templates/sidebar.html | 48 + templates/sidebarend.html | 23 + templates/stdhead.html | 23 + templates/stylesheet.html | 24 + templates/topbar.html | 53 + templates/topbarend.html | 22 + templates/volume.html | 63 + 252 files changed, 48061 insertions(+) create mode 100644 BUGS create mode 100644 CHANGES create mode 100644 ChangeLog.d/cvs--ChangeLog create mode 100644 ChangeLog.d/disorder--mainline--0.1 create mode 100644 Makefile.am create mode 100644 README create mode 100644 README.client create mode 100644 README.raw create mode 100644 README.streams create mode 100644 README.upgrades create mode 100644 TODO create mode 100644 acinclude.m4 create mode 100644 clients/Makefile.am create mode 100644 clients/authorize.c create mode 100644 clients/authorize.h create mode 100644 clients/disorder.c create mode 100644 clients/disorderfm.c create mode 100644 clients/filename-bytes.c create mode 100644 clients/test-eclient.c create mode 100644 configure.ac create mode 100644 debian/Makefile.am create mode 100644 debian/README.Debian create mode 100644 debian/autorules.m4 create mode 100644 debian/changelog create mode 100644 debian/conffiles create mode 100755 debian/config create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/disorder.config create mode 100644 debian/htaccess create mode 100644 debian/options.debian create mode 100755 debian/postinst create mode 100755 debian/postrm create mode 100755 debian/prerm create mode 100644 debian/rules.m4 create mode 100644 debian/templates create mode 100644 disobedience/Makefile.am create mode 100644 disobedience/TODO create mode 100644 disobedience/choose.c create mode 100644 disobedience/client.c create mode 100644 disobedience/control.c create mode 100644 disobedience/disobedience.c create mode 100644 disobedience/disobedience.h create mode 100644 disobedience/disobedience.rc create mode 100644 disobedience/menu.c create mode 100644 disobedience/misc.c create mode 100644 disobedience/properties.c create mode 100644 disobedience/queue.c create mode 100644 doc/Makefile.am create mode 100644 doc/checklist.txt create mode 100644 doc/disobedience.1.in create mode 100644 doc/disorder-deadlock.8.in create mode 100644 doc/disorder-dump.8.in create mode 100644 doc/disorder-rescan.8.in create mode 100644 doc/disorder.1.in create mode 100644 doc/disorder.3 create mode 100644 doc/disorder_config.5.in create mode 100644 doc/disorder_protocol.5.in create mode 100644 doc/disorderd.8.in create mode 100644 doc/disorderfm.1.in create mode 100644 doc/tkdisorder.1 create mode 100644 driver/Makefile.am create mode 100644 driver/disorder.c create mode 100644 examples/Makefile.am create mode 100644 examples/config.sample.in create mode 100755 examples/disorder-log create mode 100644 examples/disorder.init.in create mode 100644 images/Makefile.am create mode 100644 images/cross.png create mode 100644 images/down.png create mode 100644 images/downdown.png create mode 100644 images/edit.png create mode 100644 images/nocross.png create mode 100644 images/nodown.png create mode 100644 images/nodowndown.png create mode 100644 images/notes.png create mode 100644 images/notescross.png create mode 100644 images/noup.png create mode 100644 images/noupup.png create mode 100644 images/pause.png create mode 100644 images/play.png create mode 100644 images/random.png create mode 100644 images/randomcross.png create mode 100644 images/tick.png create mode 100644 images/up.png create mode 100644 images/upup.png create mode 100644 lib/Makefile.am create mode 100644 lib/addr.c create mode 100644 lib/addr.h create mode 100644 lib/asprintf.c create mode 100644 lib/authhash.c create mode 100644 lib/authhash.h create mode 100644 lib/basen.c create mode 100644 lib/basen.h create mode 100644 lib/cache.c create mode 100644 lib/cache.h create mode 100644 lib/casefold.h create mode 100644 lib/charset.c create mode 100644 lib/charset.h create mode 100644 lib/client-common.c create mode 100644 lib/client-common.h create mode 100644 lib/client.c create mode 100644 lib/client.h create mode 100644 lib/configuration.c create mode 100644 lib/configuration.h create mode 100644 lib/defs.c create mode 100644 lib/defs.h create mode 100644 lib/disorder.h create mode 100644 lib/eclient.c create mode 100644 lib/eclient.h create mode 100644 lib/event.c create mode 100644 lib/event.h create mode 100644 lib/eventlog.c create mode 100644 lib/eventlog.h create mode 100644 lib/filepart.c create mode 100644 lib/filepart.h create mode 100644 lib/fprintf.c create mode 100644 lib/hash.c create mode 100644 lib/hash.h create mode 100644 lib/hex.c create mode 100644 lib/hex.h create mode 100644 lib/inputline.c create mode 100644 lib/inputline.h create mode 100644 lib/kvp.c create mode 100644 lib/kvp.h create mode 100644 lib/log-impl.h create mode 100644 lib/log.c create mode 100644 lib/log.h create mode 100644 lib/logfd.c create mode 100644 lib/logfd.h create mode 100644 lib/mem-impl.h create mode 100644 lib/mem.c create mode 100644 lib/mem.h create mode 100644 lib/mime.c create mode 100644 lib/mime.h create mode 100644 lib/mixer.c create mode 100644 lib/mixer.h create mode 100644 lib/plugin.c create mode 100644 lib/plugin.h create mode 100644 lib/printf.c create mode 100644 lib/printf.h create mode 100644 lib/queue.c create mode 100644 lib/queue.h create mode 100644 lib/regsub.c create mode 100644 lib/regsub.h create mode 100644 lib/selection.c create mode 100644 lib/selection.h create mode 100644 lib/signame.c create mode 100644 lib/signame.h create mode 100644 lib/sink.c create mode 100644 lib/sink.h create mode 100644 lib/snprintf.c create mode 100644 lib/speaker.c create mode 100644 lib/speaker.h create mode 100644 lib/split.c create mode 100644 lib/split.h create mode 100644 lib/syscalls.c create mode 100644 lib/syscalls.h create mode 100644 lib/table.c create mode 100644 lib/table.h create mode 100644 lib/test.c create mode 100644 lib/trackname.c create mode 100644 lib/trackname.h create mode 100644 lib/types.h create mode 100644 lib/unicodegc.h create mode 100644 lib/user.c create mode 100644 lib/user.h create mode 100644 lib/utf8.c create mode 100644 lib/utf8.h create mode 100644 lib/vacopy.h create mode 100644 lib/vector.c create mode 100644 lib/vector.h create mode 100644 lib/words.c create mode 100644 lib/words.h create mode 100644 lib/wstat.c create mode 100644 lib/wstat.h create mode 100644 plugins/Makefile.am create mode 100644 plugins/exec.c create mode 100644 plugins/execraw.c create mode 100644 plugins/fs.c create mode 100644 plugins/mad.c create mode 100644 plugins/madshim.h create mode 100644 plugins/notify.c create mode 100644 plugins/shell.c create mode 100644 plugins/tracklength.c create mode 100755 prepare create mode 100644 python/Makefile.am create mode 100644 python/disorder.py.in create mode 100755 python/tkdisorder create mode 100644 scripts/Makefile.am create mode 100755 scripts/check create mode 100644 scripts/completion.bash create mode 100644 scripts/copyright.exceptions create mode 100755 scripts/dist create mode 100755 scripts/htmlman create mode 100755 scripts/inst create mode 100755 scripts/makedeb create mode 100644 scripts/oggrename create mode 100644 scripts/sedfiles.make create mode 100755 scripts/text2c create mode 100644 server/Makefile.am create mode 100644 server/api-client.c create mode 100644 server/api-client.h create mode 100644 server/api-server.c create mode 100644 server/api.c create mode 100644 server/cgi.c create mode 100644 server/cgi.h create mode 100644 server/cgimain.c create mode 100644 server/daemonize.c create mode 100644 server/daemonize.h create mode 100644 server/dcgi.c create mode 100644 server/dcgi.h create mode 100644 server/deadlock.c create mode 100644 server/disorderd.c create mode 100644 server/dump.c create mode 100644 server/play.c create mode 100644 server/play.h create mode 100644 server/rescan.c create mode 100644 server/server.c create mode 100644 server/server.h create mode 100644 server/speaker.c create mode 100644 server/state.c create mode 100644 server/state.h create mode 100644 server/trackdb-int.h create mode 100644 server/trackdb.c create mode 100644 server/trackdb.h create mode 100644 server/trackname.c create mode 100644 sounds/Makefile.am create mode 100644 sounds/scratch.ogg create mode 100644 sounds/slap.ogg create mode 100644 templates/Makefile.am create mode 100644 templates/about.html create mode 100644 templates/choose.html create mode 100644 templates/choosealpha.html create mode 100644 templates/credits.html create mode 100644 templates/disorder.css create mode 100644 templates/error.html create mode 100644 templates/help.html create mode 100644 templates/options create mode 100644 templates/options.columns create mode 100644 templates/options.labels create mode 100644 templates/playing.html create mode 100644 templates/prefs.html create mode 100644 templates/recent.html create mode 100644 templates/search.html create mode 100644 templates/sidebar.html create mode 100644 templates/sidebarend.html create mode 100644 templates/stdhead.html create mode 100644 templates/stylesheet.html create mode 100644 templates/topbar.html create mode 100644 templates/topbarend.html create mode 100644 templates/volume.html diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..78e4a86 --- /dev/null +++ b/BUGS @@ -0,0 +1,42 @@ +* Compilation Bugs + +The configure script make pointless checks for C++ and Fortran compilers. This +is a bug in Libtool not in DisOrder. See: http://bugs.debian.org/221873 + +* Server Bugs + +* Other Problems + +** Wrongly Specified Character Encoding + +A problem was reported where DisOrder was misconfigured to believe a UTF-8 +filesystem was actually an ISO-8859-1 filesystem. When it was reconfigured +correctly, the old (mangled) filenames remain in the database. + +This is not a bug as such, it is a configuration error. However recovering +from it can be painful if many filenames are involved. If this mistake is +widespread then it may be worth adding support for automatic recovery, but for +now the easiest answer is to stop the server, remove the databases and start +again. + +** Poor Sound Quality + +Poor sound quality may be a result of (hitherto unknown!) bugs in DisOrder, or +may be a problem with other software or hardware. Specific combinations that +have produced problems in the past are listed here. + +*** Stutter with VIA 82C686 + +Hardware: "VT82C686 AC97 Audio Controller" +Software: Linux via82cxxx_audio driver +Symptom: Stuttering playback with raw format play. +Solution: Use snd-via82xx driver instead (from ALSA). + +ALSA drivers can be found in Linux 2.6.x; for 2.4.x they must be built +separately. + +Local Variables: +mode:outline +fill-column:79 +End: +arch-tag:32a95f730a93f0073f62873ebae245f8 diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..04abd31 --- /dev/null +++ b/CHANGES @@ -0,0 +1,321 @@ +See ChangeLog.d/* for detailed revision history. + +* Changes up to version 1.6 + +** General + +There is a new client, 'Disobedience', that depends on the GTK+ library. +Feedback on the interface would be very welcome. + +Tracks can now have tags associated with them. See tags in disorder(1) +or the preferences documentation for the web interface or Disobedience. + +The search facility knows how to limit results by tag (see search +documentation for any interface) as well as by word search. It is +possible to limit random play by tag (see required-tags and +prohibited-tags in disorder_config(5)). + +** Server + +Cache slow file lookups in the server. Should help installations with +large collections and/or slow platforms. + +The communications protocol has changed, for the benefit of +Disobedience. + +The 'enabled' and 'random_enabled' configuration options are now gone. +Instead the state survives from one run of the server to the next. +'disable now' is gone as well - if you want to emulate it disable +playing and then scratch the current track. + +The 'pick' plugin has been abolished. All the logic formerly done there +is now built into the server, where it can be done much more +efficiently. + +** disorderfm + +There is a new command line tool called 'disorderfm' which is designed +for filename translation on (for instance) digital audio repositories. +It is not yet feature-complete. See its man page for additional +details. + +** Build And Configuration + +You can control which components are built with new --with options. See +README. + +options.transform and the 'transform' web option have gone, replaced +with a 'transform' configuration command. Both this and 'namepart' are +now optional. + +* Changes up to version 1.5.1 + +** Web Interface + +Correct regexp for non-alpha tracks. + +* Changes up to version 1.5 + +** Web Interface + +Regexp-based filtering of tracks (for instance as used by the initial +'Choose' page) now does the regexp matching in the server, limiting the +amount of data transferred to the web interface only to be discarded. + +** Client + +Regexp-base filtering of tracks is now available to the command line +client. + +** Server + +New server_nice, speaker_nice and rescan_nice configuration options +allow independent control of process priorities. + +Scratches are now attributed to the user who requested them. + +Bugs fixed: + A file descriptor was leaked for each track played. + The amount of a track played so far was not reported. + The speaker process could crash on underrun. + The server would crash if you paused a non-pause capable track. + Regexp matching in the file and directory list commands was not + reliable. + Handling of variable-argument commands in the client was broken. + +* Changes up to version 1.4 + +** General + +Raw format players are now supported. See README.upgrades and +README.raw for details. This allows pausing and eliminating the +inter-track gap. + +Pausing is also supported with suitably modified standalone player +plugins, though none of the supplied ones are capable of this. + +When random play is enabled the randomly picked track now appears in the +queue, and can be moved around the queue, removed from it, etc. + +** Web Interface + +Switches (random play, pause, ...) are now presented as a +fixed-appearance switch with an adjacent state indicator. + +The 'Manage' screen has new buttons to move tracks to the head or tail +of the queue. + +You can now edit the preferences for all the tracks in an album in a +single screen, rather than having to visit each separately. For the +time being the raw preferences editing has gone; it can be reintroduced +on some form if there is demand. (You can still edit raw preferences +from the command line.) + +Labels are now documented in options.labels rather than +disorder_config(5). + +** Server + +If you tried to start up on any empty database with random play enabled +the server would exit with an error. + +The server no longer risks failing if you strace its player +subprocesses. + +It was possible for the server to hang when a 'reconfigure' command was +issued. This should no longer be the case. + +The default signal to forcibly terminate players is now SIGKILL. + +** Plugins + +Plugins must now declare a type word. This allows them to document +whether they are a standalone player or a raw-format player, and whether +they support pausing. They can also arrange to get setup and cleanup +calls in the main server. See disorder(3) for more details. + +* Changes up to version 1.3 + +** Dependencies + +Berkeley DB 4.2 is no longer supported. Use 4.3. + +** Client + +There is a new 'authorize' command to simplify the addition of local +users. Please report successes as well as failures. + +There is a new 'resolve' command to return the real track name behind an +alias. + +The 'rescan' command no longer takes an argument. + +** Server + +The track database code has been largely rewritten to improve +maintainability. + +There is a new 'lock' directive. By default the server uses a lockfile +to prevent multiple copies of itself running simultaneously; this can be +inhibited e.g. if you are using a filesystem that does not support +locking and are confident you can prevent concurrent running yourself. + +Aliases for track names, constructed from trackname_display_ +preferences, now appear in the virtual filesystem. + +The server now executes a subprocess for the rescan operation. It also +runs a separate deadlock manager. + +Standard output and standard error from subprocesses are now logged. +This is handy if you need to figure out why a player failed unexpectedly +but might lead to huge log files if you have needlessly verbose players. + +** Web Interface + +Enable/disable buttons are now colored to reflect current state. + +Entering numeric volume values (rather than clicking on the arrows) now +works. + +Connection errors are reported more gracefuly. + +** Plugins + +Scanner plugins are now always invoked in a subprocess. + +disorder_track_count() and disorder_track_getn() are no longer +available. Instead use disorder_track_random(). + +Plugins are now opened with RTLD_NOW, so link errors are detected +immediately. + +** Tools + +disorder-dump now insists on the input/output file being a named regular +file, rather than using stdin or stdout. + +** Other + +Some missing files have been added, and some notes added regarding +getting text encoding right. + +* Changes up to version 1.2 + +See README.upgrades when upgrading to this version. + +** Bugs Fixed + +Avoid accumulating overlarge recently played list. + +When the server was stopped, the currently playing track would not be +added to the recently played list. This has been fixed. + +Reloading the 'volume' page no longer repeats the last volume-changing +action. + +The search facility now works properly for multiple hits within a single +artist or album. + +** Server + +New namepart directive replaces web interface's trackname-part. There +are associated changes to the protocol and clients. + +The number of database queries per candidate match required when +searching has been reduced. + +The operator can control the signal used to scratch playing tracks. The +default has been changed to SIGINT from SIGKILL. + +The 'log' command now provides a formalised event log, rather than raw +access to the server's ordinary log output. + +** Web Interface Changes + +*** Choosing Tracks + +When picking a track the client now stays on the same screen rather than +redirecting back to the 'Playing' screen. So that the user gets +feedback from their action, playing and queued tracks are now marked as +such in the track picking screen. + +It is possible to revert to the old behaviour by removing the back= +argument from the choose.html and search.html templates (and optionally +the trackstate lines). + +*** Search + +Non-ASCII characters are now properly supported in search terms. + +*** Syntax + +The template syntax has been changed slightly to ignore whitespace in +certain places. + +*** Miscellaneous + +Some formerly textual buttons are now replaced by images (with ALT text +reflecting the old value). The stylesheet is now a .css file (installed +in the same place as the images) rather than being embedded into every +template. + +Artist and album names in the playing and recently-played lists are now +links to the corresponding directory. + +More functions are now available from the 'manage' screen. + +The menus are now (by default) across the top of the screen instead of +down the side. Set the 'menu' label to 'sidebar' to restore the old +appearance. 'Volume' is not present in this new menu, use 'Manage' +instead (or edit the template). + +** tkdisorder + +tkdisorder now displays artist, album and title in the queue and +recently played widgets, rather than just the title (as formerly). + +* Changes up to version 1.1 + +** Bugs Fixed + +Corrected various problems with UTF-8 parsing. + +In the web interface, "The Beatles" (etc) are now grouped under 'B' not +'T' when grouping tracks by initial letter. + +** Server + +The list of recently played tracks is now preserved across server +restarts. + +Track IDs are more compact. + +Versions of libdb before 4.2 are no longer supported. 4.2 and 4.3 both +work now. 4.2 support will be removed in some future release. + +Prehistoric backwards-compatibility logic removed. Only affects people +upgrading from long before 1.0 (who should upgrade to 1.0 and then to +1.1.) + +** Command Line + +Tracks can be moved in the queue from the command line. + +'disorder queue' now reports track IDs. + +$pkgdatadir/completion.bash provides tab completion over commands and +options. + +** Web Interface + +New 'cooked' preferences interface saves users having to know arcane +details of trackname preferences and so on. Non-ASCII characters are +now properly supported in this context. + +CGI arguments to the web interface are now checked for UTF-8 compliance. + +Local Variables: +mode:outline +fill-column:72 +End: +arch-tag:9dfc21f4428056e647e3656822342956 diff --git a/ChangeLog.d/cvs--ChangeLog b/ChangeLog.d/cvs--ChangeLog new file mode 100644 index 0000000..050c274 --- /dev/null +++ b/ChangeLog.d/cvs--ChangeLog @@ -0,0 +1,881 @@ +2004-10-04 00:59:42 +0100 Richard Kettlewell + + * dcgi.c: proper track ordering in search. + + * regsub.c, tracks.c: chattier error messages + + * utf8.h: stricter UTF-8 parsing + +2004-09-26 10:47:45 +0100 Richard Kettlewell + + * Makefile.am, plugins/Makefile.am: tidy up to build in separate + object directory + +2004-09-25 17:47:23 +0100 Richard Kettlewell + + * version 0.11 + +2004-09-25 17:41:21 +0100 Richard Kettlewell + + * BUGS: some known bugs + +2004-09-25 17:25:49 +0100 Richard Kettlewell + + * tracks.c: chattier errors + +2004-09-25 17:17:25 +0100 Richard Kettlewell + + * api-client.c, cgi.c, cgimain.c, disorder.c, server.c: EXIT_FAILURE + pedantry + +2004-09-18 17:28:32 +0100 Richard Kettlewell + + * client.c: rework handling of commands that get lists back, + unbreaking 'stats' in the process. + +2004-09-18 16:54:58 +0100 Richard Kettlewell + + * tracks.c: add a lockfile to prevent concurrent access to databases. + +2004-07-31 19:05:38 +0100 Richard Kettlewell + + * debian/postinst, prerm: hopefuly better upgrade handling + +2004-07-31 18:44:52 +0100 Richard Kettlewell + + * queue.c: don't reverse the queue on restart + + * Document queue/recent ordering a bit better. + +2004-07-20 20:22:09 +0100 Richard Kettlewell + + * tkdisorder: primitive queue widget, simplify MonitorStateThread + +2004-07-20 19:42:52 +0100 Richard Kettlewell + + * tkdisorder: python + tkinter gui, still under development + +2004-07-18 14:21:12 +0100 Richard Kettlewell + + * Extra navigation links in 'Choose' screen allow easier navigation + back up the directory tree. It's not quite right yet because + track names are relative to the filesystem root rather than the root + of their collection. + + New @navigate@, @fullname@, @dirname@, @basename@, @ne@ and @eq@ + expansions. + +2004-07-18 13:22:53 +0100 Richard Kettlewell + + * use new label sidebar.choosewhich to determine which 'Choose' screen + to pick, rather than making operator edit sidebar.html + + * mention config.USERNAME in README + +2004-07-18 13:05:45 +0100 Richard Kettlewell + + * debian/control: more detailed build deps + +2004-07-18 13:03:56 +0100 Richard Kettlewell + + * README: dependency version notes + +2004-07-18 12:57:55 +0100 Richard Kettlewell + + * cope with gcrypt version skew + +2004-07-18 02:46:07 +0100 Richard Kettlewell + + * Control connections can go over the net (using anything that + getaddrinfo() knows about) using the new 'listen' and 'connect' + options. + +2004-07-17 19:01:25 +0100 Richard Kettlewell + + * Increase/decrease volume buttons + +2004-07-17 16:35:18 +0100 Richard Kettlewell + + * templates/help.html: document 'Manage' + + * templates/playing.html: extra empty buttons + +2004-07-17 16:24:12 +0100 Richard Kettlewell + + * Any track that had a filesystem encoding that differed from itself + when converted to UTF-8 track (that is to say, any non-ASCII track in + a non-UTF-8 filesystem) would not be played, as only the UTF-8 + version of the name was passed to the player. + + * The disorder_play_track plugin interface now takes both the path and + the track name (the former being the raw bytes from the filesystem, + the latter being the UTF-8 version). + + * Document that scratch names must be UTF-8 (or ASCII). + +2004-07-17 16:11:21 +0100 Richard Kettlewell + + * 'move' command in control protocol to move tracks around + + * New 'Manage' page allows tracks to be moved around in the queue. + +2004-07-17 14:56:04 +0100 Richard Kettlewell + + * disorder.c: teach 'disorder play' to take multile tracks. Hence for + instance you could do: + disorder play /path/to/some/album/*.ogg + +2004-07-17 14:45:55 +0100 Richard Kettlewell + + * templates/help.html: mention choosealpha + +2004-07-17 14:37:18 +0100 Richard Kettlewell + + * Use regexp-filtered file listings to implement choosealpha.html, + which split the top-level 'Choose' page up according to the initial + letter of the filenames. + +2004-07-17 14:19:33 +0100 Richard Kettlewell + + * files and directory listings can now be filtered using regexps (you + could do this in your client anyway but now the server does it for + you, thus causing less data to be transferred from server to + client). + + * correct disorder.py's quoting of empty strings + +2004-07-17 13:07:51 +0100 Richard Kettlewell + + * disorder.py.in: correct exception-to-string functions + +2004-07-17 13:05:06 +0100 Richard Kettlewell + + * disorder.py.in: add another example + +2004-07-17 12:57:57 +0100 Richard Kettlewell + + * disorder.py.in: better docs and error handling + +2004-07-17 01:06:59 +0100 Richard Kettlewell + + * disorder.py.in: add a full set of methods + +2004-07-17 01:06:31 +0100 Richard Kettlewell + + * plugins/tracklength.c: quieten compiler + +2004-07-16 22:31:31 +0100 Richard Kettlewell + + * rudimentary Python client support + +2004-07-15 00:55:08 +0100 Richard Kettlewell + + * configuration.c: stricter syntax check for 'url' + +2004-07-15 00:41:22 +0100 Richard Kettlewell + + * disorder_config.5.in: clarify 'url' syntax + + * templates/help.html: link to DisOrder control protocol page + +2004-07-14 19:46:38 +0100 Richard Kettlewell + + * config.sample.in, debian/disorder.config: play WAV files correctly + + * debian/control: depend on sox + + * scratches are now *.ogg files + +2004-07-11 19:45:24 +0100 Richard Kettlewell + + * plugins/tracklength.c: build fix + +2004-07-11 19:39:40 +0100 Richard Kettlewell + + * add notify_queue to notify plugin + +2004-07-11 19:01:26 +0100 Richard Kettlewell + + * tracks.c: always sync log after replay, so that upgrade works + +2004-07-11 18:54:33 +0100 Richard Kettlewell + + * new prefs.log file records all preferences in ASCII and guarantess + to be up to date. So provided you back up this file the rest of the + databases can be lost completely. + + * document the use and properties of the various database files + + * new prefsync configuration command controls interval between + minimization of prefs.log. + + * event.c, event.h: timeouts get an associated handle which can be + used to cancel them. + + * don't remove the search database at the drop of a hat as that would + require it to be rebuilt more frequently than is sensible. + +2004-07-11 18:38:52 +0100 Richard Kettlewell + + * disorder.c: missing 'break', oops! + +2004-07-10 19:43:42 +0100 Richard Kettlewell + + * plugins/tracklength.c: binary search over extensions + round up WAV duration + +2004-07-10 19:22:50 +0100 Richard Kettlewell + + * plugins/tracklength.c: tracklength plugin now knows about WAV files + remember to unmap files after parsing them! + + * sounds/slap.wav: correct broken encoding (was two WAVs concatenated, + is now a single WAV; it worked with 'cat > /dev/audio' but doesn't + work with things that expect a single correctly formatted WAV file.) + +2004-07-10 18:11:26 +0100 Richard Kettlewell + + * 'disorder --length' allows command-line access to tracklength plugin + +2004-07-10 14:47:41 +0100 Richard Kettlewell + + * server.c: use libgcrypt for random numbers + + * configure.ac: don't need dev/[u]random any more + +2004-07-10 13:33:16 +0100 Richard Kettlewell + + * add log command to stream logs to clients + + * disorder_protocol.5.in: new man page documenting internal + communications protocol + +2004-07-10 11:51:54 +0100 Richard Kettlewell + + * use URL-encoding in dumps because it's more convenient to duck + encoding issues that way. It also looks more consistent with the + use of URL-encoding elsewhere in DisOrder. + + * change logging interface so that messages are formatted only once. + +2004-07-10 11:40:13 +0100 Richard Kettlewell + + * printf.c: correct number bases! + +2004-07-10 00:00:33 +0100 Richard Kettlewell + + * send back our URL in Refresh HTTP header to work around weird Apache + behaviour + + * log to (in theory) multiple outputs + + * '#define NO_MEMORY_ALLOCATION' allows us to enforce rules about + files that shouldn't perform memory allocation + + * split *printf frontends into separate files since some need memory + allocation and some do not + +2004-06-12 11:37:37 +0100 Richard Kettlewell + + * Use a sink to handle writing to an ev_writer + + * disorder-dump --dump now works against a running server, so it is + not necessary to take the server down to back up the preference + data. + + * printf.c: correct handling of flags (which were completely broken) + +2004-05-24 15:56:30 +0100 Richard Kettlewell + + * new help page, plus HTML-ized versions of man pages + + * de-dupe code in Makefile + + * template names can't have / in and can't be dot-files + (previously they weren't allowed . in) + + * @ is @ not ( + +2004-05-24 14:00:38 +0100 Richard Kettlewell + + * templates/playing.html: put track title in element + +2004-05-24 13:58:29 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: remove dead code + +2004-05-23 19:29:16 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: distribute missing files + + * dcgi.c: quiten compiler + + * printf.c: missing base and padding settings + +2004-05-23 19:08:40 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.3: update for disorder_snprintf + +2004-05-23 19:01:31 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * New *printf implementation, which is guaranteed to behave reliable + in the face of strange encoding games (standard *printf insist on + MBC strings in the current encoding, which isn't great for us). + We don't support floating point yet. + + * Log output is always ASCII (non-ASCII characters are escaped) so we + don't have to rely on the encoding of stderr or syslog. + + * No longer depend on the target providing a working snprintf. + + * Corrected a few broken *printf calls + +2004-05-22 16:55:02 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * reach preferences edit from 'Recent' + +2004-05-22 16:30:56 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * @search@ converted to new template infrastructure + +2004-05-21 21:55:55 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * optionally restrict scratch and/or remove to submitting user + + * export 'become' command to command-line client + + * determine username from UID, not environment + +2004-05-21 21:13:30 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: minor improvements + +2004-05-21 21:00:05 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: rearrange into alpha order + +2004-05-18 23:14:11 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.c: add {enable,disable}-random as synonyms for + random-{enable,disable} + +2004-05-18 20:18:02 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: prevent rapid web refresh is all playing is disabled + +2004-05-18 00:30:28 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * support libdb 4.2 as well as 3.2. Versions inbetween might well work + but this hasn't been tested. + +2004-05-16 23:43:34 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * debian/control: list build depends + +2004-05-16 23:34:40 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * #include <db.h> when checking libdb, to cope with variants that + redefine all the symbols. + +2004-05-16 23:28:55 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * new disorder-dump program to read/write prefs in text format + (e.g. for backup, database upgrade, replication) + +2004-05-16 10:37:10 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: clean generated files + + * debian/autorules.m4: fix clean target + + * debian/rules.m4: man pages in /usr/share/man + +2004-04-26 23:28:18 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate dependency on <inttypes.h> + +2004-04-26 20:59:20 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * convert UTF-8 -> UCS-4 directly, rather than relying on iconv (which + doesn't always know how to do it). + +2004-04-26 20:47:12 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Do not expand dcgi-generated txet in expansion argument values (as + it'll get expanded properly later on anyway). + + * Add @urlquote@ expansion. + +2004-04-25 20:47:26 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * arg:directory, not arg:dir + Include containing directory name in choose.html + +2004-04-25 20:11:49 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: minor docs improvements. + +2004-04-25 19:53:19 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * unify transform- web options, and make them available via the + @transform@ expansion. + +2004-04-25 19:52:13 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: setting a trackname_ pref would cause a crash. + +2004-04-25 18:51:56 +0100 Richard Kettlewell <rjk@greenend.org.uk> + + * Bring documentation up to date a bit. + +2004-04-25 18:29:49 Richard Kettlewell <rjk@greenend.org.uk> + + * @choose@ converted to new template infrastructure. + +2004-04-25 17:14:56 Richard Kettlewell <rjk@greenend.org.uk> + + * @prefs@ now takes a template argument, shifting the preferences + table furniture into the template file. + +2004-04-25 15:44:38 Richard Kettlewell <rjk@greenend.org.uk> + + * Portability hacks of various degrees of nastiness in the interest of + building on FreeBSD. + +2004-04-18 19:00:52 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: add some missing bits. + +2004-04-18 18:05:17 Richard Kettlewell <rjk@greenend.org.uk> + + * @playing@ and @recent@ now take a template argument, allowing the + template file to contain the table furniture rather than having it + generated from inside dcgi. Templates updated accordingly. + Documentation updated. + + * Moved the calculation of expected start times into the server. + +2004-04-18 13:45:19 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: booleans for the expansion language + +2004-04-18 13:13:46 Richard Kettlewell <rjk@greenend.org.uk> + + * Expansions can now take multiple parameters using a new quoting + syntax. Parameters are (for all current expansions, but not for + some future one) recursively expanded before use. + + * sink.c: interface for things that accept output (current + implementations being stdio and dynstrs) + + * vacopy.h: find a va_copy somewhere. + +2004-04-17 16:48:00 Richard Kettlewell <rjk@greenend.org.uk> + + * version 0.10 + +2004-04-17 16:25:59 Richard Kettlewell <rjk@greenend.org.uk> + + * Typo fixes. + +2004-04-17 16:03:45 Richard Kettlewell <rjk@greenend.org.uk> + + * README: recommend basic authentication instead of digest auth, as + the latter seems too poorly supported. + + * configure.ac: don't check for things that are very standard + + * debian/control: add Section and Priority fields + +2004-04-04 17:45:34 Richard Kettlewell <rjk@greenend.org.uk> + + * Build GNU getopt for systems where libc doesn't have it + +2004-04-04 15:11:49 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: check that various things are available and work at + configure time, rather than build or (worse still!) run time. More + library checks. + + * charset.c: <wchar.h> no longer needed + +2004-04-04 12:06:16 Richard Kettlewell <rjk@greenend.org.uk> + + * server.c: attempt to play tracks when they are added to the queue. + Otherwise they wouldn't be played at all nothing was already + playing. + +2004-04-03 13:39:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Use libiconv if libc doesn't have iconv + +2004-04-03 12:42:05 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: check for both libdb and libdb3 + report all missing libs at once + +2004-04-02 23:42:13 Richard Kettlewell <rjk@greenend.org.uk> + + * Only use __attribute__ syntax under GNU C. + +2004-04-02 23:16:41 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: use whatever libdb the system defaults to. + +2004-04-02 23:06:50 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.c: add a --help-commands option giving a one-line summary + of each command. + + * Updated documentation a bit. + +2004-04-02 20:20:47 Richard Kettlewell <rjk@greenend.org.uk> + + * Ship another scratch sound. + +2004-04-02 20:10:10 Richard Kettlewell <rjk@greenend.org.uk> + + * Debianization fixes: + + distribute postrm + + call Libtool correctly + + fix shared library dependencies + + * Quieten compiler when optimization turned on. + +2004-04-02 19:27:59 Richard Kettlewell <rjk@greenend.org.uk> + + * Tidy up appearance of preferences screen + +2004-03-28 20:01:40 Richard Kettlewell <rjk@greenend.org.uk> + + * Track prefs editor in web interface. Rather rough-edged for now. + +2004-03-28 17:41:43 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: add @shell@ expansion + +2004-03-28 15:02:02 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: handle empty search result lists correctly. + +2004-03-28 14:38:01 Richard Kettlewell <rjk@greenend.org.uk> + + * split.c: quote empty strings properly + +2004-03-28 14:35:39 Richard Kettlewell <rjk@greenend.org.uk> + + * README.streams: how to play streams with DisOrder + +2004-03-28 14:10:51 Richard Kettlewell <rjk@greenend.org.uk> + + * play.c: unpick wait status of failed players + +2004-03-28 14:07:49 Richard Kettlewell <rjk@greenend.org.uk> + + * play.c: put players in their own process group and send SIGKILL + rather than SIGTERM (the latter because ogg123 doesn't seem to honor + SIGTERM when playing streams). + +2004-03-28 13:57:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Handle directories that contain both files and directories better + + dcgi.c: headings for directory/track lists + correct output for choose.playall label + + server.c: support file listing in the root + + Associated label and style sheet additions + +2004-03-28 12:35:20 Richard Kettlewell <rjk@greenend.org.uk> + + * New 'shell' player plugin executes a shell command with an environment + variable identifying the track. Optionally, the user can control what + shell is used. + + * The sample config file knows how to play .wav files now. + + * Ship a default scratch sound. + +2004-03-27 20:48:13 Richard Kettlewell <rjk@greenend.org.uk> + + * Play tracks via a plugin. 'exec' plugin module provides previous + behaviour. + +2004-03-27 20:28:57 Richard Kettlewell <rjk@greenend.org.uk> + + * log interesting events if a username is attached (see previous + change) + +2004-03-27 19:30:44 Richard Kettlewell <rjk@greenend.org.uk> + + * server.c: don't log auth data any more - with the Refresh: HTTP + header causing disorder.cgi to run every few seconds you end up with + vast amounts of useless information. It would be more sensible to + log actions instead. + +2004-03-27 18:41:22 Richard Kettlewell <rjk@greenend.org.uk> + + * Add a sidebar to the web interface, making everything available from + everywhere. Tidy up the default stylesheet. + +2004-03-27 18:19:40 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: eliminate a spurious </div> + +2004-03-27 17:17:16 Richard Kettlewell <rjk@greenend.org.uk> + + * When updating the track list, only process one track per select(). + This makes the server much more responsive to clients during + rescans. + +2004-03-27 16:20:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Don't need file extensions in stopword list by default any more. + +2004-03-27 16:07:53 Richard Kettlewell <rjk@greenend.org.uk> + + * Common code now lives in a shared library. + This library is not intended for other programs to link against, + so no ABI guarantees exist for it. + + * The disorder(3) API is now implemented by shim functions rather than + aliases. These are always part of the executable that imports the + plugin, never the shared library. Varargs functions are a bit messy + as you can't just write a forwarding function for them. + +2004-03-27 14:22:22 Richard Kettlewell <rjk@greenend.org.uk> + + * Volume control support. + + new 'mixer' and 'channel' configuration commands + + 'volume' server command + + disorder_set_volume and disorder_get_volume client functions + + get-volume and set-volume command-line client commands + + @volume:SPEAKER@ expansion to get current volumes + + 'volume' action to set volume + + volume.html template (a bit primitive still) + +2004-03-26 19:15:09 Richard Kettlewell <rjk@greenend.org.uk> + + * Bring back 'Play all' button in @choose@, and fix track selection + + * Scratch by ID, so that late scratches don't get the wrong track + +2004-03-25 00:40:12 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: report top 10 search words in server stats + +2004-03-24 23:24:45 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: de-dupe track/directory code in @choose@ + +2004-03-22 23:31:55 Richard Kettlewell <rjk@greenend.org.uk> + + * dcgi.c: fine-tune sort order. Now we try the 'sort' transformation + or part context first - once case-folded, once raw; then we try the + 'display' version, again case-folded then raw; and finally we use + the unprocessed track name as as tie-breaker. + + Repeated calls to casefold aren't very efficient, this should be + fixed. + + * templates/options.transform: correct transform-track replacement + string + +2004-03-22 19:58:42 Richard Kettlewell <rjk@greenend.org.uk> + + * Global and case-independent regexp replacement. + + * Strip out punctuation for sorting directories in @choose@. + +2004-03-21 19:30:39 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: remove search database in track_sync(). It'll be removed + when we re-open anyway, and there's no point occupying the disk + space longer than necessary. + + By the same token, we don't bother syncing it in track_sync(). + +2004-03-21 18:47:31 Richard Kettlewell <rjk@greenend.org.uk> + + * Sort strings are now case-folded before comparison. + + * words.c: casefold now returns the original string if it was + malformed, rather than a null pointer. + +2004-03-21 17:04:48 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.h: new function disorder_track_set_data allows plugins to + set preferences as well as query them. + + * disorder.3: document the above and add some general notes + + * plugins/notify.c: record time and count tracks are played at + + * plugins/pick.c: don't pick recently played tracks + +2004-03-21 16:06:21 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: server stats had the database types all wrong, leading to + a crash. Fixed. + +2004-03-21 15:14:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Separate sorting and display with new context arguments in + configuration and trackname_ preferences. + + * By default track numbers take part in sorting, but are not displayed + + * By default "The" is display unmodified but moved to the end for + sorting + +2004-03-21 14:37:36 Richard Kettlewell <rjk@greenend.org.uk> + + * Group search results according to 'columns search' configuration. + +2004-03-21 14:25:35 Richard Kettlewell <rjk@greenend.org.uk> + + cvs -Q up -j mergepoint-0-9-bugfixes-1 -j mergepoint-0-9-bugfixes-2 + +2004-03-21 14:21:34 Richard Kettlewell <rjk@greenend.org.uk> + + * templates/search.html: fix broken HTML. + +2004-03-20 21:11:30 Richard Kettlewell <rjk@greenend.org.uk> + + * Include trackname_ preferences in searches, and rebuild search + database from scratch on startup (to completely eliminate any + contamination due to prior configuration). + +2004-03-20 18:50:36 Richard Kettlewell <rjk@greenend.org.uk> + + * words.c: more characters are separators + +2004-03-20 18:44:45 Richard Kettlewell <rjk@greenend.org.uk> + + * Split preferences into a separate database, and automatically + convert the old one. + +2004-03-18 00:11:26 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: correct scanning for directories, to correctly handle + cases where you have <path>/<prefix> and <path>/<prefix><suffix>. + +2004-03-17 00:20:55 Richard Kettlewell <rjk@greenend.org.uk> + + * config.sample.in: prefer mpg321 to mpg123 + +2004-03-16 23:45:54 Richard Kettlewell <rjk@greenend.org.uk> + + * Server statistics support: + o track_stats() reports stats from databases + o disorder_stats() reports stats in client + o stats command to command line interface + o @stats@ expansion in web interface + + * about.html: provides information about DisOrder + + * client.c: correct list parsing in client interface + + * disorder_config.5.in: add missing docs for @arg@ and @search@ + +2004-03-16 23:16:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Do all database writes synchronously and add some missing + transaction IDs. In fact the missing transaction IDs probably + ameliorated problems caused by the asynchronous writes (which leave + the database actually unusable, rather than merely with bits + missing). + +2004-03-16 20:30:00 Richard Kettlewell <rjk@greenend.org.uk> + + * Improved track search: + o Instead of encoding all track names into a single db item, we use + sorted duplicate data items. libdb does all the work for us. + o Strip collection root and extensions from filenames + o Delete stopwords from search db at startup (in case new ones have + been added) + o Rename search db to search2.db so it doesn't conflict with the old + one + o Always update search db even if we've seen the track, so that + database format/name changes don't trash searching + + * Add a few missing transaction ID arguments. + +2004-03-16 20:24:25 Richard Kettlewell <rjk@greenend.org.uk> + + * rescan.c: rescan would crash on shut down + +2004-03-16 12:43:54 Richard Kettlewell <rjk@greenend.org.uk> + + * quieten compiler when optimization turned on + +2004-03-16 12:12:40 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder_config.5.in: complete renaming of trackinfo.* to heading.* + +2004-03-15 23:18:02 Richard Kettlewell <rjk@greenend.org.uk> + + * version 0.9 + +2004-03-15 19:56:50 Richard Kettlewell <rjk@greenend.org.uk> + + * Define table columns with a new 'columns' web option, rather than by + relying on the ordering of trackinfo.* labels. trackinfo.* labels + are thus only used as headings and are renamed accordingly. The + button column is no longer magical. + + * Always search the config directory and data directory for templates + and web options (after any explicitly configured directories). + +2004-03-15 19:17:02 Richard Kettlewell <rjk@greenend.org.uk> + + * make table classes more consistent + +2004-03-15 18:13:39 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: log closing databases. Checkpoint database at shutdown. + +2004-03-15 18:03:35 Richard Kettlewell <rjk@greenend.org.uk> + + * tracks.c: issue log messages from database checkpointing, since we + don't know how long they'll take. + +2004-03-15 17:31:35 Richard Kettlewell <rjk@greenend.org.uk> + + * disorder.init.in: make 'stop' work even if server not running. + +2004-03-15 14:37:59 Richard Kettlewell <rjk@greenend.org.uk> + + Added debianization files. + +2004-03-15 13:51:12 Richard Kettlewell <rjk@greenend.org.uk> + + * cgi.c: avoid UB if template path is empty + +2004-03-14 16:33:12 Richard Kettlewell <rjk@greenend.org.uk> + + * Makefile.am: correct dependencies on version script + distribute version script + +2004-03-14 16:30:56 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate zombie rescans. + +2004-03-14 16:25:37 Richard Kettlewell <rjk@greenend.org.uk> + + * eliminate clash with glibc error(). + +2004-03-14 16:19:55 Richard Kettlewell <rjk@greenend.org.uk> + + * Use a version script to limit the exported symbols. There's a glibc + naming clash with error() which should work around. Also this + should be conditional on the linker actually supporting this + feature. + +2004-03-14 15:37:56 Richard Kettlewell <rjk@greenend.org.uk> + + * configure.ac: correct wrong handling of <gc.h> + +2004-03-14 15:04:44 Richard Kettlewell <rjk@greenend.org.uk> + + * DisOrder has been rewritten in C. + +arch-tag:b64f2a539d1621207b46b5bd202244e9 diff --git a/ChangeLog.d/disorder--mainline--0.1 b/ChangeLog.d/disorder--mainline--0.1 new file mode 100644 index 0000000..2daf877 --- /dev/null +++ b/ChangeLog.d/disorder--mainline--0.1 @@ -0,0 +1,6487 @@ +# do not edit -- automatically generated by arch changelog +# arch-tag: automatic-ChangeLog--rjk@greenend.org.uk--2004/disorder--mainline--0.1 +# + +2006-11-11 18:04:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-328 + + Summary: + Missing ship + Revision: + disorder--mainline--0.1--patch-328 + + * scripts/Makefile.am: Remember to ship oggrename. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/Makefile.am + + +2006-11-11 13:13:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-327 + + Summary: + oggrename + Revision: + disorder--mainline--0.1--patch-327 + + * scripts/oggrename: Script to rename OGG files according to embedded + title information. + + new files: + scripts/oggrename + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2006-11-04 15:56:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-326 + + Summary: + disorderfm filename converter + Revision: + disorder--mainline--0.1--patch-326 + + * clients/disorderfm.c: New filename management tool. + * doc/disorderfm.1.in: Documentation. + * lib/charset.c: New entry points required for disorderfm. + * lib/eclient.c: Quieten compiler. + * clients/filename-bytes.c: Grotty utility for examining byte strings in + filenames. + + new files: + clients/disorderfm.c clients/filename-bytes.c + doc/disorderfm.1.in + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + clients/Makefile.am doc/Makefile.am lib/charset.c + lib/charset.h lib/eclient.c + + +2006-10-08 21:26:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-325 + + Summary: + Update CHANGES + Revision: + disorder--mainline--0.1--patch-325 + + * CHANGES: Update change description + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-10-08 21:20:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-324 + + Summary: + Copyright dates. + Revision: + disorder--mainline--0.1--patch-324 + + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/trackname.c + templates/prefs.html + + +2006-10-08 21:12:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-323 + + Summary: + Search by tag + Revision: + disorder--mainline--0.1--patch-323 + + * server/trackdb.c: Search for tags using tag: syntax. + + * templates/help.html: Mention tag: syntax. + * doc/disobedience.1.in: Mention tag: syntax. + * doc/disorder.1.in: Mention tag: syntax. + * doc/disorder_protocol.5.in: Mention tag: syntax. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + doc/disorder.1.in doc/disorder_protocol.5.in server/trackdb.c + templates/help.html + + +2006-10-08 19:17:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-322 + + Summary: + Avoid needless redraws. + Revision: + disorder--mainline--0.1--patch-322 + + * disobedience/choose.c: Don't issue a redraw on an empty search result + if we were already displaying the right thing. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 19:01:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-321 + + Summary: + Docs + window title. + Revision: + disorder--mainline--0.1--patch-321 + + * disobedience/disobedience.c: Correct window title. + * doc/disobedience.1.in: Document search box. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c doc/disobedience.1.in + + +2006-10-08 18:56:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-320 + + Summary: + Better search responsiveness. + Revision: + disorder--mainline--0.1--patch-320 + + * disobedience/choose.c: More efficient initial construction of search + results tree. The display of the tree is now the expensive bit (e.g + 0.2s for 300 hits on my Athlon). The old logic remains for expanding + individual items in the tree, but it does much less work in that + context and so isn't a performance problem. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 18:35:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-319 + + Summary: + Cancel search button + Revision: + disorder--mainline--0.1--patch-319 + + * disobedience/choose.c: Add a cancel button to clear the current search. + You can do this by deleting all the text but having an obvious button + for it seems friendlier. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + + +2006-10-08 17:58:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-318 + + Summary: + Search cleanup + Revision: + disorder--mainline--0.1--patch-318 + + 'search-parse' makes much more sense as it means we guarantee a uniform + interpretation of search strings across all clients. So we make 'search' + do that. + + * server/server.c: 'search' takes on the meaning of 'search-parse' now. + * lib/eclient.c: Keep up with changed 'search' names. + * lib/client.c: Use new search interface. + * server/dcgi.c: Use new search interface. + * clients/disorder.c: Use new search interface. + + * doc/disorder_protocol.5.in: Document resolved search semantics + + modified files: + ChangeLog.d/disorder--mainline--0.1 clients/disorder.c + disobedience/TODO doc/disorder_protocol.5.in lib/client.c + lib/client.h lib/eclient.c server/dcgi.c server/server.c + + +2006-10-08 17:45:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-317 + + Summary: + Search in Disobedience + Revision: + disorder--mainline--0.1--patch-317 + + Lots of cleanup and documentation to do. + + * disobedience/choose.c: Track searching. This is implemented as a text + entry in the choose window. Whenever contains a valid search string + then the choose tree is replaced with the search results. + * disobedience/choose.c: Disable breakdown by initial letter. The code + is still there and available via --choosealpha. + * disobedience/disobedience.c: Make report window available earlier. + * disobedience/disobedience.c: --choosealpha option to re-enable initial + letter breakup of choose tree. + + * server/server.c: search-parse parses search string instead of expecting + caller to do so. + + * lib/eclient.c: Implement search. + * lib/hash.c: Constness. + * lib/split.c: Cope with an absent error handler. + * server/trackdb.c: Directory-tree-order comparison moved to lib/trackname.c. + * lib/trackname.c: Export directory-tree-order comparison as compare_path(). + + * doc/disorder_protocol.5.in: Document search-parse. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + disobedience/disobedience.c disobedience/disobedience.h + disobedience/disobedience.rc doc/disobedience.1.in + doc/disorder_protocol.5.in lib/eclient.c lib/eclient.h + lib/hash.c lib/hash.h lib/split.c lib/trackname.c + lib/trackname.h server/server.c server/trackdb.c + server/trackname.c + + +2006-09-17 14:53:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-316 + + Summary: + Support for obsolete GCC. + Revision: + disorder--mainline--0.1--patch-316 + + * configure.ac: Turn off -Werror for GCC 2.95. + + Not sure how worthwhile this is... + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + disobedience/TODO + + +2006-09-17 10:24:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-315 + + Summary: + Fix popup menu behaviour. + Revision: + disorder--mainline--0.1--patch-315 + + * disobedience/queue.c: Pop up menu on button press, not release, giving + more traditional behaviour. + * disobedience/choose.c: Pop up menu on button press. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + disobedience/queue.c + + +2006-09-17 10:21:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-314 + + Summary: + Document Disobedience tag support. + Revision: + disorder--mainline--0.1--patch-314 + + * doc/disobedience.1.in: Document tag support. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + + +2006-09-17 10:18:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-313 + + Summary: + Tags in Disobedience + Revision: + disorder--mainline--0.1--patch-313 + + * disobedience/properties.c: Edit tags. Also fix boolean prefs to work + for prefs other than pick_at_random, not that there are any yet. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-09-17 10:04:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-312 + + Summary: + Web editing for tags + Revision: + disorder--mainline--0.1--patch-312 + + * server/dcgi.c: Accept tags in a prefs response. + * templates/prefs.html: Edit tags in web prefs screen. + * templates/options.labels: Label for tags field. + * templates/help.html: Mention tags in html help. + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/dcgi.c + templates/help.html templates/options.labels + templates/prefs.html + + +2006-09-17 09:50:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-311 + + Summary: + Tags fiddling and documntation. + Revision: + disorder--mainline--0.1--patch-311 + + * server/trackdb.c: Tags are now separated by commas and can contain + spaces. + * doc/disorder.1.in: Mention tags in track preferences. + * doc/disorder_config.5.in; Mention tag list syntax. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_config.5.in server/trackdb.c + + +2006-09-17 09:28:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-310 + + Summary: + Update copyright dates + Revision: + disorder--mainline--0.1--patch-310 + + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/client.h lib/disorder.h lib/plugin.c lib/plugin.h + plugins/Makefile.am server/play.h + + +2006-09-17 09:22:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-309 + + Summary: + Fix missing events bugs (hooray) + Revision: + disorder--mainline--0.1--patch-309 + + * disobedience/queue.c: Only create drag target widgets when actually + dragging, as otherwise they sometimes(!) steal events from the widgets + they overlap. + The padding cell at the RHS of every row is now sensitive to input. + Add a comment describing widget hierarchy. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-09-15 21:46:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-308 + + Summary: + Minor fixups + Revision: + disorder--mainline--0.1--patch-308 + + * server/play.c: Close the spare writing end of the player's log pipe - + it only needs to be visible as stdout/err. + + * disobedience/queue.c: Conditioned out diagnostic code for lost clicks. + + * scripts/completion.bash: Update completeions for current command set. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + disobedience/queue.c scripts/completion.bash server/play.c + + +2006-05-14 16:49:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-307 + + Summary: + Heartbeat + Revision: + disorder--mainline--0.1--patch-307 + + * disobedience/disobedience.c: Add a (conditioned-out) heartbeat in + pursuit of unresponsiveness. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c + + +2006-05-03 23:11:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-306 + + Summary: + Tags and global preferences. + Revision: + disorder--mainline--0.1--patch-306 + + Higher-level user interfaces have yet to be written and not much testing + has been done but the basics seem to be working. + + * server/trackdb.c: Tags. Global preferences for recording long-term + server state. This is relatively involved; although the logic for + maintaining tags is very simple, being similar to search, picking a + track at random when it must have particular tags is more annoying and + we don't use the database to help us much, but instead keep a cache and + remember to blow it in various places. + * server/play.c: playing_enable and random_enabled are now database + entries. + * server/disorderd.c: Need to do initial setup more directly now. + * server/server.c: Track protocol changes. + + * server/dcgi.c: Abolish disable-now. + + * lib/client.c: Track protocol changes. + * lib/configuration.c: enabled/random_enabled config options abolished in + favour of new global prefs. + * lib/hash.c: Start with a 256-slot hash. Cope with null values. New + hash_keys() returns a list of keys in no particular order. + + * clients/disorder.c: Kill disable-now. + Add tags, get-global, set-global, unset-global. + + * plugins/pick.c: Removed. + * lib/plugin.c: Pick plugin abolished. + + * doc/disorder.1.in: Document new command line options. + * doc/disorder.3: Document removal of pick plugin. + * doc/disorder_config.5.in: Document global prefs and removal of + enabled/random_enabled config options and disable-now CGI action. + * doc/disorder_protocol.5.in: Document get-global, set-global, + unset-global, tags. + + removed files: + plugins/pick.c + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.upgrades + clients/disorder.c doc/disorder.1.in doc/disorder.3 + doc/disorder_config.5.in doc/disorder_protocol.5.in + lib/client.c lib/client.h lib/configuration.c + lib/configuration.h lib/disorder.h lib/hash.c lib/hash.h + lib/plugin.c lib/plugin.h plugins/Makefile.am server/dcgi.c + server/disorderd.c server/play.c server/play.h server/server.c + server/trackdb.c server/trackdb.h + + +2006-05-01 17:38:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-305 + + Summary: + Drag+drop fixing. + Revision: + disorder--mainline--0.1--patch-305 + + * disobedience/queue.c: Cope with dragging to the head of the queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-05-01 17:32:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-304 + + Summary: + Quieten compiler + Revision: + disorder--mainline--0.1--patch-304 + + * disobedience/queue.c: Rename 'time' args to keep gcc happy. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-05-01 17:31:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-303 + + Summary: + Drag+drop queue rearrangement + Revision: + disorder--mainline--0.1--patch-303 + + * disobedience/queue.c: Drag+drop queue rearrangement. Use button + release events, not press, as the latter get confused with drag starts. + * disobedience/choose.c: Use button release events, not press. + * disobedience/disobedience.c: New log_moved() signature. + * disobedience/disobedience.rc: Colors for drag target zones + + * doc/disorder_protocol.5.in: Document moveafter and 'moved' log change. + + * lib/queue.c: queue_moveafter() is the underlying implementation of the + 'moveafter' command. + * server/server.c: New 'moveafter' command moves a bunch of tracks to a + single ocation in the queue. + * lib/eclient.c: Support moveafter and 'moved' log change. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/choose.c disobedience/disobedience.c + disobedience/disobedience.rc disobedience/queue.c + doc/disorder_protocol.5.in lib/eclient.c lib/eclient.h + lib/queue.c lib/queue.h server/server.c + + +2006-05-01 14:33:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-302 + + Summary: + Reduce accidental scratching. + Revision: + disorder--mainline--0.1--patch-302 + + * disobedience/queue.c: Only make scratch item in popup sensitive if the + playing track is selected, to cut down on accidental scratching. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/queue.c + + +2006-05-01 12:15:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-301 + + Summary: + Cache fixes. + Revision: + disorder--mainline--0.1--patch-301 + + * lib/hash.c: Remember to actually save value. + * lib/cache.c: Pass correct time when expiring. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + lib/cache.c lib/hash.c + + +2006-05-01 12:01:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-300 + + Summary: + More administrivia + Revision: + disorder--mainline--0.1--patch-300 + + More copyright dates and exceptions. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/check + scripts/completion.bash scripts/copyright.exceptions + + +2006-05-01 11:57:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-299 + + Summary: + Default transform/namepart. + Revision: + disorder--mainline--0.1--patch-299 + + * lib/configuration.c: Default transform/namepart. + + * debian/disorder.config: Commment out transform/namepart. + * examples/config.sample.in: Commment out transform/namepart. + + * README.client: more notes. + * README.upgrades: Mention that transform/namepart are optional now. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.client + README.upgrades debian/disorder.config + doc/disorder_config.5.in examples/config.sample.in + lib/configuration.c + + +2006-05-01 11:21:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-298 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-298 + + * scripts/check: Easier invocation. + * scripts/completion.bash: Option completion for Disobedience. + + Also updated copyright dates on a bunch of files. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/rules.m4 + disobedience/disobedience.rc doc/Makefile.am + images/Makefile.am lib/asprintf.c lib/authhash.c + lib/authhash.h lib/hash.c lib/hash.h lib/log.c lib/mem.c + lib/mem.h lib/printf.h lib/queue.c lib/queue.h lib/split.c + lib/trackname.c lib/trackname.h prepare scripts/Makefile.am + scripts/check scripts/completion.bash + scripts/copyright.exceptions server/cgi.c server/server.h + server/trackdb.h templates/Makefile.am templates/help.html + + +2006-05-01 11:07:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-297 + + Summary: + Fix queue column width + Revision: + disorder--mainline--0.1--patch-297 + + * disobedience/queue.c: Columns should shrink to fit, not stay at their + maximum extent indefinitely. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/queue.c + + +2006-04-30 23:22:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-296 + + Summary: + Memory optimization. + Revision: + disorder--mainline--0.1--patch-296 + + * lib/hash.c: Less memory-heavy hash implementation. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/hash.c + + +2006-04-30 19:53:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-295 + + Summary: + More documentation. + Revision: + disorder--mainline--0.1--patch-295 + + * README: mention --without-* options. + * README.client: how to set up a standalone client install. + + new files: + README.client + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am README + disobedience/TODO + + +2006-04-30 19:42:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-294 + + Summary: + Documentation updates. + Revision: + disorder--mainline--0.1--patch-294 + + * doc/disorder.1.in: Mention the automatic rescan. + Add a troubleshooting section. + + * doc/disobedience.1.in: Hide --sync. Document keyboard shortcuts and + recent changes to 'Choose'. + + * templates/help.html: Add a troubleshooting section. Possibly this + should just be a link to the equivalent disorder(1) section. + + * CHANGES: Updated. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disobedience.1.in doc/disorder.1.in templates/help.html + + +2006-04-30 19:05:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-293 + + Summary: + debian policy fixup + Revision: + disorder--mainline--0.1--patch-293 + + * debian/autorules.m4: Make binary targets depend on build target. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/autorules.m4 + + +2006-04-30 18:55:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-292 + + Summary: + Don't forget queue selection on update + Revision: + disorder--mainline--0.1--patch-292 + + * disobedience/queue.c: Use new selection_*() functions to record + selection so that it survives updates to the queue reliably. + + * lib/selection.c: Selection management functions using a hash. + + * lib/hash.c: hash_count() to count the number of items in a hash. + + * lib/queue.h: queue_entry.selected is gone. + + new files: + lib/selection.c lib/selection.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.h disobedience/queue.c + lib/Makefile.am lib/hash.c lib/hash.h lib/queue.c lib/queue.h + + +2006-04-30 16:42:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-291 + + Summary: + Faster startup + Revision: + disorder--mainline--0.1--patch-291 + + * lib/eclient.c: Batch up command writes once authenticated. This + improves Disobedience performance, in particular it fills in the track + names faster at startup if the server is over a network, by reducing + the number of round trip times. + * lib/log.c: DISORDER_DEBUG_ONLY allows you to limit debug output to that + from a single file. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/eclient.c lib/log.c + + +2006-04-30 15:02:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-290 + + Summary: + Fix playing indicator for aliased tracks. + Revision: + disorder--mainline--0.1--patch-290 + + * lib/choose.c: Resolve filenames (so that the playing indicator and + properties window work). + + * lib/eclient.c: disorder_eclient_resolve(). + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/choose.c + lib/eclient.c lib/eclient.h + + +2006-04-30 14:40:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-289 + + Summary: + Popup menu and selection in choose tab + Revision: + disorder--mainline--0.1--patch-289 + + * disobedience/choose.c: Abolish buttons and just use labels and do our + own click parsing. Maintain a selection in the same way as queue.c. + Popup menu to play/edit tracks, middle click to play straight away. + + * disobedience/disobedience.rc: Supply bg whenever we supply fg. + + * disobedience/menu.c: Edit menu uses callbacks to deal with different + kinds of tabs rather than explicit knowledge. + + * disobedience/queue.c: Callbacks for edit menu. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/TODO + disobedience/choose.c disobedience/disobedience.h + disobedience/disobedience.rc disobedience/menu.c + disobedience/queue.c + + +2006-04-30 11:59:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-288 + + Summary: + Set sensitivity of main menu items. + Revision: + disorder--mainline--0.1--patch-288 + + * disobedience/menu.c: Move out main menu code. Set sensitivity of + Properties and Select All appropriately from menu_update(). + + * disobedience/queue.c: Call menu_update() when queue/recent changes. + Provide queue-counting functions queue_count_*() to set sensitivity of + main menu items. Fix 'remove' option. + + * disobedience/disobedience.c: Move main menu out to menu.c. Call + menu_update() when the user switches tabs. + + * disobedience/disobedience.h: Include almost all headers from here. + Organize function prototypes into logical groups. + + new files: + disobedience/menu.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/Makefile.am + disobedience/choose.c disobedience/client.c + disobedience/control.c disobedience/disobedience.c + disobedience/disobedience.h disobedience/misc.c + disobedience/properties.c disobedience/queue.c + + +2006-04-30 11:21:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-287 + + Summary: + Edit>Properties starts working. + Revision: + disorder--mainline--0.1--patch-287 + + * disobedience/disobedience.c: Edit>Properties menu item now works for + queues. + + * disobedience/queue.c: queue_properties() entry point for the above. + + modified files: + ChangeLog.d/disorder--mainline--0.1 + disobedience/disobedience.c disobedience/disobedience.h + disobedience/queue.c + + +2006-04-29 18:00:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-286 + + Summary: + Document properties window. + Revision: + disorder--mainline--0.1--patch-286 + + * doc/disobedience.1.in: Document properties window. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disobedience.1.in + + +2006-04-29 16:04:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-285 + + Summary: + Stock buttons in properties window + Revision: + disorder--mainline--0.1--patch-285 + + * disobedience/properties.c: Use stock items for properties window buttons. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-04-29 13:46:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-284 + + Summary: + Stop badness if user closes progress bar window + Revision: + disorder--mainline--0.1--patch-284 + + * disobedience/properties.c: Cope with progress bar window being + destroyed part way through. + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/properties.c + + +2006-04-29 13:38:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-283 + + Summary: + Properties window + Revision: + disorder--mainline--0.1--patch-283 + + Only usable from queue/recent so far - need to do select and the menu bar + properties item too. Also search when that is done. + + * disobedience/properties.c: New properties popup window. + * disobedience/queue.c: Pass queue definition as well as item to menu + item activation. + namepart_update() notifies that a namepart might have changed. + If we right click away from any selected item, select just the hovered + item. + Make properties menu item in popup sensitive if anything is selected, + and call properties() with the selected tracks when it is activated. + Include formerly missing backlink from first queued track to playing + track. + * lib/eclient.c: get, set and unset. + * disobedience/client.c: Split out popup_error(). + * disobedience/misc.c: popup_error(). + * disobedience/disobedience.c: Rename function to avoid collision. + + new files: + disobedience/properties.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 disobedience/Makefile.am + disobedience/client.c disobedience/disobedience.c + disobedience/disobedience.h disobedience/misc.c + disobedience/queue.c lib/eclient.c lib/eclient.h + + +2006-04-26 20:24:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-282 + + Summary: + Rename gdisorder to 'Disobedience' + Revision: + disorder--mainline--0.1--patch-282 + + Name suggested by Owen Dunn. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + disobedience/Makefile.am disobedience/choose.c + disobedience/client.c disobedience/control.c + disobedience/disobedience.c disobedience/disobedience.h + disobedience/disobedience.rc disobedience/misc.c + disobedience/queue.c doc/Makefile.am doc/disobedience.1.in + + renamed files: + doc/gdisorder.1.in + ==> doc/disobedience.1.in + gdisorder/.arch-ids/=id + ==> disobedience/.arch-ids/=id + gdisorder/gdisorder.c + ==> disobedience/disobedience.c + gdisorder/gdisorder.h + ==> disobedience/disobedience.h + gdisorder/gdisorder.rc + ==> disobedience/disobedience.rc + + new directories: + disobedience/.arch-ids + + removed directories: + gdisorder/.arch-ids + + renamed directories: + gdisorder + ==> disobedience + + +2006-04-17 18:29:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-281 + + Summary: + Build fixes + Revision: + disorder--mainline--0.1--patch-281 + + * gdisorder/Makefile.am: ship gdisorder.c + * server/server.c: Quieten compiler. + * lib/eclient.c: Quieten stupid compiler. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + gdisorder/Makefile.am lib/eclient.c server/server.c + + +2006-04-17 11:18:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-280 + + Summary: + Install gdisorder + Revision: + disorder--mainline--0.1--patch-280 + + * gdisorder/Makefile.am: Install gdisorder. + * doc/gdisorder.1.in: Update man page. + * doc/Makefile.am: Install gdisorder man page + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + doc/gdisorder.1.in gdisorder/Makefile.am + + +2006-04-17 11:02:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-279 + + Summary: + Popup improvements, multi-track remove. + Revision: + disorder--mainline--0.1--patch-279 + + * gdisorder/queue.c: Table driven popup menus. The menu items are now + fixed but the sensitivity changes according to context. Multi-track + remove now works. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/queue.c + + +2006-04-16 23:39:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-278 + + Summary: + gdisorder tidying + Revision: + disorder--mainline--0.1--patch-278 + + * gdisorder/gdisorder.c: Remove bogus underlines. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + + +2006-04-16 23:37:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-277 + + Summary: + Selection in queue/recent. + Revision: + disorder--mainline--0.1--patch-277 + + * gdisorder/queue.c: Show and maintain the selection. You can't do + anything useful with the selection yet, however. + * lib/eclient.c: Fill in backlinks in queue lists. + * gdisorder/gdisorder.c: New Edit menu. Just a placeholder right now. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/queue.c lib/eclient.c + + +2006-04-16 22:47:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-276 + + Summary: + Popup menu in queue/recent. + Revision: + disorder--mainline--0.1--patch-276 + + * lib/eclient.c: disorder_eclient_scratch() now takes an ID. + disorder_eclient_scratch_playing() provides the old scratch-anything + interface for the benefit of control.c. + * lib/queue.h: Add a 'ql' field to the queue so gdisorder can remember + which queue each entry belongs to. + * gdisorder/queue.c: Popup menus on right button in queues. Currently + only scratch and remove work, though 'properties' appears in the menu + for the sake of show. + * gdisorder/control.c: Keep up with eclient.c. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/control.c + gdisorder/queue.c lib/eclient.c lib/eclient.h lib/queue.c + lib/queue.h + + +2006-04-16 18:28:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-275 + + Summary: + Show how much of the currently playing track has played. + Revision: + disorder--mainline--0.1--patch-275 + + * gdisorder/queue.c: Show how much of the currently playing track has + been played. Destroy queue label widgets as well as their containing + eventbox. + + * gdisorder/gdisorder.c: Refetch currently playing track data whenever + the track is paused or resumed. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/gdisorder.c gdisorder/queue.c + + +2006-04-16 17:17:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-274 + + Summary: + New gdisorder buttons to enable/disable play/random play + Revision: + disorder--mainline--0.1--patch-274 + + * gdisorder/control.c: New buttons to enable/disable play/random play. + + * lib/eclient.c: New calls to enable/disable play/random play. + + * images/random.png: Question-mark icon to enable random play. + * images/randomcross.png: Crossed question-mark icon to disable random play + * images/notescross.png: Notes icon to enable play. We use the existing + notes.png to disable play. + + new files: + images/.arch-ids/notescross.png.id + images/.arch-ids/random.png.id + images/.arch-ids/randomcross.png.id images/notescross.png + images/random.png images/randomcross.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/control.c + images/Makefile.am lib/eclient.c lib/eclient.h + + +2006-04-14 17:06:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-273 + + Summary: + Volume control for gdisorder + Revision: + disorder--mainline--0.1--patch-273 + + * gdisorder/control.c: Volume control. Visually rather ugly but the + feature is now there. + * gdisorder/gdisorder.c: Monitor volume. + * lib/eclient.c: Volume support. + * server/disorderd.c: Check the current volume from time to time in case + it's changed outside the server's control. + * server/server.c: Remember the (believed) current volume. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/TODO + gdisorder/control.c gdisorder/gdisorder.c + gdisorder/gdisorder.h lib/eclient.c lib/eclient.h + server/disorderd.c server/server.c server/server.h + + +2006-04-14 10:45:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-272 + + Summary: + Split out icon code to control.c + Revision: + disorder--mainline--0.1--patch-272 + + * gdisorder/control.c: Split icon code out to a control.c. + * gdisorder/gdisorder.c: Split icon code out to a control.c. + * gdisorder/queue.c: Rename 'playing' to 'playing_track' to avoid + conflict with now-global playing boolean. + + new files: + gdisorder/control.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h gdisorder/queue.c + + +2006-04-12 19:45:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-271 + + Summary: + Fix queue/recent title truncation. + Revision: + disorder--mainline--0.1--patch-271 + + * gdisorder/queue.c: Determine title cell width each time round rather + than stashing it. Eliminates truncation of rightmost title. + + new files: + gdisorder/TODO + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + + +2006-04-12 18:36:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-270 + + Summary: + Pause and scratch buttons for gdisorder + Revision: + disorder--mainline--0.1--patch-270 + + The scratch button doesn't currently use the same trick as the GUI to be + sure it's scratching exactly the right thing. Since the display is much + more likely to be up to date that's less of an issue here. Still, it + would be good to fix it sometime. + + * gdisorder/gdisorder.c: Icon bar between menu bar and tabs containing + pause/resume buttons and a scratch button. Pause and resume are + actually separate buttons but exactly one is ever visible at any given + time. + + * lib/eclient.c: Implement _pause/_resume/_scratch commands. + protocol_error() takes an operation not a client, since the operation + pointer in the client may be the wrong one by the point it gets called. + Support the 'state' log entry. + + * server/server.c: The 'log' command now issues some initial lines to + synchronize the current state. + + * images/pause.png: Pause icon. + + * images/play.png: Play icon. + + new files: + images/.arch-ids/pause.png.id images/.arch-ids/play.png.id + images/pause.png images/play.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + images/Makefile.am lib/eclient.c lib/eclient.h server/server.c + + +2006-04-11 19:30:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-269 + + Summary: + Display track lengths + Revision: + disorder--mainline--0.1--patch-269 + + * lib/eclient.c: disorder_eclient_length() + * gdisorder/queue.c: Include track lengths in queue/recent. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + lib/eclient.c lib/eclient.h + + +2006-04-11 19:10:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-268 + + Summary: + Minor gdisorder fixes. + Revision: + disorder--mainline--0.1--patch-268 + + * lib/mem.c: Reverse test in xcalloc. You can ask for count=0 but size=0 + will give silly results. + + * gdisorder/choose.c: Build fixes for Mac. + + * gdisorder/queue.c: Cope with completely empty queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/choose.c + gdisorder/queue.c lib/mem.c + + +2006-04-09 22:12:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-267 + + Summary: + More concise logging + Revision: + disorder--mainline--0.1--patch-267 + + * server/server.c: Don't log boring errors (EPIPE when talking to a + client in particular). When we do log an error make sure it's the + correct one (though it usually was anyway). + + modified files: + ChangeLog.d/disorder--mainline--0.1 server/server.c + + +2006-04-09 22:11:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-266 + + Summary: + Kill a recently introduced crash... + Revision: + disorder--mainline--0.1--patch-266 + + * lib/cache.c: Don't try to clean the cache if it does not exist yet! + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/cache.c + + +2006-04-09 22:03:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-265 + + Summary: + Title bar for gdisorder queue/recent listing + Revision: + disorder--mainline--0.1--patch-265 + + * gdisorder/queue.c: Rewrite in terms of layouts. This proved to be the + least painful way of getting a title bar which panned in synch. + * gdisorder/gdisorder.rc: gdisorder-title style is (by default) white on + black in a bold font. *.row-title is bound to it. + * lib/mem.c: xcalloc(). + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.rc + gdisorder/queue.c lib/mem.c lib/mem.h + + +2006-04-08 11:44:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-264 + + Summary: + Update CHANGES. + Revision: + disorder--mainline--0.1--patch-264 + + * CHANGES: updated. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-04-08 11:42:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-263 + + Summary: + Cache track lookups that use regexps. + Revision: + disorder--mainline--0.1--patch-263 + + Top-level track lookups generally involve scanning over the whole + database and filtering. This is significantly slower than any other + lookup - noticably slow in one slightly underpowered installation - so + well worthwhile caching the results. + + Note that we do not have a call to cache_expire anywhere in the server + yet. It doesn't matter: there is a daily automatic rescan, and all the + cached file lookups are junked at the end of any rescan, so a cache entry + never has a lifetime much greater than a day anyway. + + * lib/cache.c: cache_clean() allows selective or total elimination of + cache elements. + * server/trackdb.c: Clean out track lookup cache when a rescan completes. + * server/server.c: Cache track lookups that use regexps. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/cache.c lib/cache.h + server/server.c server/trackdb.c server/trackdb.h + + +2006-04-05 22:37:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-262 + + Summary: + Better reporting. + Revision: + disorder--mainline--0.1--patch-262 + + * lib/eclient.c: 'report' callback to signal what's going on to the + application. + * gdisorder/choose.c, gdisorder/queue.c: Set report line when we start a + command. + * lib/client.c: Clear the report line when the client goes idle. + + modified files: + ChangeLog.d/disorder--mainline--0.1 clients/test-eclient.c + gdisorder/choose.c gdisorder/client.c gdisorder/queue.c + lib/eclient.c lib/eclient.h + + +2006-04-05 22:24:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-261 + + Summary: + Tidying up + Revision: + disorder--mainline--0.1--patch-261 + + * templates/options.transform: Removed because now obsolete. + * CHANGES: Note the move here too. + + removed files: + templates/options.transform + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/Makefile.am + + +2006-04-05 21:34:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-260 + + Summary: + Correct text and sorting in track choice tab + Revision: + disorder--mainline--0.1--patch-260 + + * lib/configuration.c: 'transform' directive moved here from web options. + * lib/trackname.c: trackname_transform() and compare_tracks() moved from + CGI code. + * server/cgi.c: Don't parse 'transform' directive. cgi_transform() moved + to trackname.c. + * server/dcgi.c: compare_multi() moved to trackname.c. + + * gdisorder/choose.c: Display and sort directory and track names using + 'transform' directive, as the web interface. + + * README.upgrades: Mention move of 'transform'. + * doc/disorder_config.5.in: Move documentation for 'transform' to its new + section. + + * examples/config.sample.in: 'transform' directives moved to config file. + * debian/disorder.config: 'transform' directives moved to config file. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in gdisorder/choose.c + lib/configuration.c lib/configuration.h lib/trackname.c + lib/trackname.h server/cgi.c server/dcgi.c + + +2006-04-05 20:23:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-259 + + Summary: + Queue fixes + Revision: + disorder--mainline--0.1--patch-259 + + * gdisorder/queue.c: Queue now expands to fill horizontal space + available. Kill a GTK+ error message when there is nothing in the + queue. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/queue.c + + +2006-04-02 18:58:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-258 + + Summary: + Memory leak + Revision: + disorder--mainline--0.1--patch-258 + + * gdisorder/choose.c: Don't leak tree widgets. It seems GTK+ remembers + pointers to them somewhere even after thay have been deparented, + frustrating the garbage collector. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/choose.c + + +2006-04-02 18:50:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-257 + + Summary: + Prettier colors + Revision: + disorder--mainline--0.1--patch-257 + + * gdisorder/choose.c: Don't set colors explicitly, just set widget names + instead. + + * gdisorder/gdisorder.c: Apply a default style. + + * gdisorder/misc.c: scroll_widget() adds a GtkViewport to non-GtkLayout + widgets and sets the name of the scrolled window's child (i.e. the + GtkViewport or the GtkLayout). + + * gdisorder/queue.c: Use a GtkTable instead of a list store in order to + colorize rows conveniently. Still not happy with the queue views but + they look better than they did. + + * doc/gdisorder.1.in: Start of a man page for gdisorder. + + * gdisorder/gdisorder.rc: Default style information for gdisorder. + + * scripts/text2c: Script to convert gdisorder.rc (or other files) into a + variable in C. + + new files: + doc/gdisorder.1.in gdisorder/gdisorder.rc scripts/text2c + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + gdisorder/Makefile.am gdisorder/choose.c gdisorder/gdisorder.c + gdisorder/gdisorder.h gdisorder/misc.c gdisorder/queue.c + scripts/Makefile.am + + +2006-04-02 14:55:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-256 + + Summary: + State reporting to event log. + Revision: + disorder--mainline--0.1--patch-256 + + * server/play.c: Report state changes to event log. + * server/server.c: Report volume changes to event log. + + * doc/disorder_protocol.5.in: Document the above. + + * scripts/inst: Correct path to CGI. + * debian/rules.m4: Correct path to CGI. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/rules.m4 + doc/disorder_protocol.5.in scripts/inst server/play.c + server/server.c + + +2006-04-02 14:43:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-255 + + Summary: + Tidying up + Revision: + disorder--mainline--0.1--patch-255 + + * gdisorder/Makefile.am: Lose 'gtk' prefix from source files. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + + renamed files: + gdisorder/gtkchoose.c + ==> gdisorder/choose.c + gdisorder/gtkclient.c + ==> gdisorder/client.c + gdisorder/gtkmisc.c + ==> gdisorder/misc.c + gdisorder/gtkqueue.c + ==> gdisorder/queue.c + + +2006-04-02 14:42:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-254 + + Summary: + Make scroll bar arrows work. + Revision: + disorder--mainline--0.1--patch-254 + + * gdisorder/gtkmisc.c: Fix up scroll step increments for layouts, which + for some reason default to 0. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gtkmisc.c + + +2006-04-02 11:52:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-253 + + Summary: + Mark currently playing tracks in the choose track widget. + Revision: + disorder--mainline--0.1--patch-253 + + * gdisorder/gtkchoose.c: Display the notes icon next to tracks that are + queued or playing. + * gdisorder/gtkqueue.c: Notify Choose tab when then the queue or playing + track change. New queued() function to tell whether a track is queued + or playing. + * gdisorder/gtkmisc.c: find_image() loads images into the cache as + pixbufs. + * lib/cache.c: More careful checking for cache expiry, since we may have + very large lifetimes. + * images/notes.png: New notes icon to mark currently playing tracks. + * configure.ac: Build and install images/ for GTK+ builds as well as + server builds. We share the images between the two. + + * gdisorder/gtkchoose.c: If the Choose layout shrinks then invalidate the + regions outside it, since they are not redrawn otherwise. + + new files: + images/.arch-ids/notes.png.id images/notes.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + gdisorder/gdisorder.h gdisorder/gtkchoose.c + gdisorder/gtkmisc.c gdisorder/gtkqueue.c images/Makefile.am + lib/cache.c + + +2006-04-01 19:00:06 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-252 + + Summary: + Client-only (and Mac) fixups + Revision: + disorder--mainline--0.1--patch-252 + + The result of this works on my Mac with a minimal configuration file, + connecting to the server on a Linux box using TCP/IP. + + * configure.ac: Don't search fink db4 includes if not building server. + Fix bad test for want_server when checking db version. + * lib/log.c: Use explicit casts and wide types when printing timestamps + in debug messages. + * lib/configuration.c: Removed server-specific configuration checks. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/configuration.c lib/log.c + + +2006-04-01 18:20:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-251 + + Summary: + Split up clients from server and allow configure-time selection. + Revision: + disorder--mainline--0.1--patch-251 + + The command line clients are split into their own directory, clients/ and + everything else that used to be in progs/ is now in server. You can + control what is built with --without-python, --without-gtk and + --without-server. + + * configure.ac: Tell the top-level makefile what subdirectories to build + based on --with/--without options. We also only ask for libraries that + we actually need. + The shipped getopt is abolished until someone wants it enough to figure + out a convenient way of having it used from multiple directories. + * clients/disorder.c: Abolish --length option. It was always in the + wrong place anyway and the client/server build split makes it even + siller as well as inconvenient. + + new files: + clients/.arch-ids/=id clients/Makefile.am + + removed files: + DESIGN2 + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am + clients/disorder.c configure.ac prepare server/Makefile.am + + renamed files: + progs/.arch-ids/=id + ==> server/.arch-ids/=id + progs/authorize.c + ==> clients/authorize.c + progs/authorize.h + ==> clients/authorize.h + progs/disorder.c + ==> clients/disorder.c + progs/test-eclient.c + ==> clients/test-eclient.c + + new directories: + clients clients/.arch-ids server/.arch-ids + + removed directories: + progs/.arch-ids + + renamed directories: + progs + ==> server + + +2006-04-01 14:46:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-250 + + Summary: + Minor fixes. + Revision: + disorder--mainline--0.1--patch-250 + + * gdisorder/gdisorder.c: Refetch server state once every 10m. + * gdisorder/gtkchoose.c: Make background white. Probably the wrong + answer - we really want to make it do whatever the standard tree view + widget does. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/gtkchoose.c + + +2006-04-01 14:27:41 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-249 + + Summary: + Show currently playing track + Revision: + disorder--mainline--0.1--patch-249 + + * lib/eclient.c: disorder_eclient_playing() reports currently playing + track. + * gdisorder/gtkqueue.c: Bung currently playing track at top of queue. It + could really do with being a separate color or something. + * gdisorder/gdisorder.c: Call playing_update() when the currently playing + track might have changed. + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/gdisorder.c + gdisorder/gdisorder.h gdisorder/gtkqueue.c lib/eclient.c + lib/eclient.h + + +2006-04-01 12:57:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-248 + + Summary: + Pick tracks in gdisorder. + Revision: + disorder--mainline--0.1--patch-248 + + * gdisorder/gtkchoose.c: Choose tracks from a tree structure. + + * gdisorder/gdisorder.c: choose_widget() moved to new gtkchoose.c. + * gdisorder/gtkqueue.c: Use scroll_widget(). + * gdisorder/gtkclient.c: Split out popup_protocol_error(). + * gdisorder/gtkmisc.c: Split out scroll_widget(). + + * lib/eclient.c: Support null callbacks. + + new files: + gdisorder/gtkchoose.c gdisorder/gtkmisc.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h + gdisorder/gtkclient.c gdisorder/gtkqueue.c lib/eclient.c + lib/eclient.h + + +2006-03-30 22:29:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-247 + + Summary: + Starting blocks for a GTK+ client + Revision: + disorder--mainline--0.1--patch-247 + + Currently the client can display the queue and recently played list and + keep up to date with them, and not much else. Hence it's noinst_ for + now. + + It's in a separate directory so that we can easily turn it on or off from + the configure with --with arguments. At some point the server and its + helpers will have to be moved to a new directory for the same reason so + you can conveniently do a client-only build. + + * lib/cache.c: Generic cache. + * lib/asprintf.c: New byte_xvasprintf(). + * lib/authhash.c: const-correct. + * lib/client.c: Split with_sockaddr() out to client-common.c + * lib/hash.c: New hash_foreach() (used by cache expiry) + * lib/queue.h: New selected field in queue entries for use by client. + Not in marshalled form! + * lib/queue.c: queue_unmarshall_vec() for parsing pre-split queue + descriptions. + * lib/client-common.c: Common code for the two C client implementations. + * lib/eclient.c: New asynchronous C client. + + * gdisorder/gdisorder.c: Main program for GTK+ client. Not finished! + * gdisorder/gtkqueue.c: Queue management in GTK+. + * gdisorder/gtkclient.c: Wrap an eclient up so it can be used in a GTK+ + program. + * gdisorder/gdisorder.h: Header file for GTK+ client. + + * progs/test-eclient.c: Test rig for eclient.c + + * doc/disorder_protocol.5.in: XX4 description was missing from last + commit. + + * configure.ac: Find GTK+/Glib includes; we need to fix them up to use + -isystem as they can provoke warnings. + + new files: + gdisorder/.arch-ids/=id gdisorder/Makefile.am + gdisorder/gdisorder.c gdisorder/gdisorder.h + gdisorder/gtkclient.c gdisorder/gtkqueue.c lib/cache.c + lib/cache.h lib/client-common.c lib/client-common.h + lib/eclient.c lib/eclient.h progs/test-eclient.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am configure.ac + doc/disorder_protocol.5.in lib/Makefile.am lib/asprintf.c + lib/authhash.c lib/authhash.h lib/client.c lib/hash.c + lib/hash.h lib/printf.h lib/queue.c lib/queue.h + progs/Makefile.am {arch}/=tagging-method + + new directories: + gdisorder gdisorder/.arch-ids + + +2006-03-30 21:50:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-246 + + Summary: + Log changes + Revision: + disorder--mainline--0.1--patch-246 + + Required for work that isn't checked in yet. + + * progs/server.c: Log returns 254 to indicate an indefinite body. + * doc/disorder_protocol.5.in: Document xx4 responses and log namespace + change. + * lib/queue.c: Change log tags so that they are valid C identifiers. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + lib/queue.c progs/server.c + + +2006-03-26 23:28:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-245 + + Summary: + Tidier debug output. + Revision: + disorder--mainline--0.1--patch-245 + + * lib/log.c: Strip ../ from filenames in debug output. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c + + +2006-03-26 19:36:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-244 + + Summary: + Quoting bug and response code sanity + Revision: + disorder--mainline--0.1--patch-244 + + * lib/split.c: Quote strings containing newlines properly. + * progs/server.c: Coherent response code policy. + * doc/disorder_protocol.5.in: Document response code policy and also the + authentication protocol. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + lib/split.c progs/server.c + + +2006-03-25 18:28:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-243 + + Summary: + Release 1.5.1 + Revision: + disorder--mainline--0.1--patch-243 + + * configure.ac: Release 1.5.1 + * CHANGES: Bring up to date + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog + + +2006-03-22 20:38:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-242 + + Summary: + Buglet in choose page + Revision: + disorder--mainline--0.1--patch-242 + + * templates/choosealpha.html: correct '*' link. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choosealpha.html + + +2006-03-20 22:43:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-241 + + Summary: + Documentation setting + Revision: + disorder--mainline--0.1--patch-241 + + * doc/disorder_config.5.in: Trivial formatting fix. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2006-03-20 22:41:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-240 + + Summary: + Release 1.5 + Revision: + disorder--mainline--0.1--patch-240 + + * lib/configuration.c: Compatibility alias 'nice' for 'nice_rescan' to + keep old config files working (it was in the example even if it wasn't + documented). + * examples/config.sample.in: Remove 'nice' from sample config. + * configure.ac: Release 1.5 + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog debian/disorder.config + examples/config.sample.in lib/configuration.c + + +2006-03-20 22:30:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-239 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-239 + + * templates/help.html: Eliminate 'just'. + * doc/checklist.txt: More checklist items. + * templates/about.html: Copyright dates + * scripts/dist: Copyright dates + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/checklist.txt + scripts/dist templates/about.html templates/help.html + + +2006-03-20 21:55:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-238 + + Summary: + Bug blame + Revision: + disorder--mainline--0.1--patch-238 + + * BUGS: Point a finger of blame at Libtool. + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 + + +2006-03-19 20:04:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-237 + + Summary: + Attribute scratches properly. + Revision: + disorder--mainline--0.1--patch-237 + + * progs/play.c: Attribute scratches to the user who requested the scratch. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2006-03-19 20:03:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-236 + + Summary: + Administrivia + Revision: + disorder--mainline--0.1--patch-236 + + * progs/rescan.c: Copyright date. + * progs/play.c: Copyright date. + * README: Copyright date. + * scripts/dist: cd into =build if it exists. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README progs/play.c + progs/rescan.c scripts/dist + + +2006-03-19 00:19:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-235 + + Summary: + New speaker_nice option. + Revision: + disorder--mainline--0.1--patch-235 + + * lib/configuration.c: New speaker_nice option. + * progs/disorderd.c: Start speaker process as root. + * progs/speaker.c: Set nice value, ignore SIGPIPE and become mortal. + * progs/trackdb.c: Reset subprocess priority to zero. (Ineffectual if + the main server is already at positive niceness.) + * lib/user.c: Moved to lib since shared between several programs. + + * doc/disorder_config.5.in: Document speaker_nice. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/Makefile.am lib/configuration.c + lib/configuration.h progs/Makefile.am progs/disorderd.c + progs/speaker.c progs/trackdb.c templates/credits.html + + renamed files: + progs/user.c + ==> lib/user.c + progs/user.h + ==> lib/user.h + + +2006-03-18 23:52:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-234 + + Summary: + Move deployed filtering of track names closer to the right place + Revision: + disorder--mainline--0.1--patch-234 + + * progs/dcgi.c: Use the server's regexp filtering rather than getting all + the files and throwing away the ones that do not match. + + * progs/disorder.c: Correct handling of variable-argument commands + Make regexp support available to files/allfiles/dirs. + + * progs/trackdb.c: Correct handling of pcre_exec() return value, so we + don't report no match if there are subpatterns. We still log + unexpected errors. + + * templates/choosealpha.html: With server-based regexp filtering, we need + slightly more subtle regexps to keep the initial 'the' out. Perhaps + better still (for choosealpha) would be some kind of server-held cache, + but we'll see what performance is like in reality. + + * doc/disorder.1.in: Document regexp support in the command line client. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_config.5.in progs/dcgi.c progs/disorder.c + progs/trackdb.c templates/choosealpha.html + + +2006-03-18 21:00:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-233 + + Summary: + First round of process priority changes. + Revision: + disorder--mainline--0.1--patch-233 + + * progs/disorderd.c: Apply nice_server config setting at startup. + * progs/rescan.c: nice_rescan applies to whole rescan process, not just + to collection scanner subprocesses. + * lib/configuration.c: 'nice' renamed to nice_rescan. Added + nice_server. + * doc/disorder_config.5.in: Document nice_rescan and nice_server. (The + former was never documented in its old guise as 'nice'.) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/disorderd.c progs/rescan.c + + +2006-03-18 20:43:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-232 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-232 + + * CHANGES: missing CHANGES note for the last commit. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2006-03-18 20:42:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-231 + + Summary: + Don't crash when the user tries to pause a non-pausible track. + Revision: + disorder--mainline--0.1--patch-231 + + * progs/play.c: Correct test for pause-capable players, which contained + an embarassing parenthesization error. Because of this mistake, if you + tried to pause a track using a non-pause-capable player, the server + would immediately crash. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2006-03-18 20:36:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-230 + + Summary: + Fix a disorder-speaker crash + Revision: + disorder--mainline--0.1--patch-230 + + * progs/speaker.c: If the speaker process detects underrun then it should + not later treat written_bytes as a byte count! This could lead to the + speaker process crashing. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/speaker.c + scripts/inst + + +2006-02-19 11:39:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-229 + + Summary: + Missing build dependencies + Revision: + disorder--mainline--0.1--patch-229 + + * debian/control (Build-Depends): Was missing libao-dev. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/control + + +2005-11-15 19:28:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-228 + + Summary: + Report track played so far properly. + Revision: + disorder--mainline--0.1--patch-228 + + * progs/speaker.c: Record (and therefore report) amount of a track played + so far, which got lost in the ALSA transition. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/checklist.txt + progs/speaker.c + + renamed files: + doc/ui-checklist.txt + ==> doc/checklist.txt + + +2005-11-15 19:14:45 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-227 + + Summary: + Clean up logging FD leaks. + Revision: + disorder--mainline--0.1--patch-227 + + * lib/logfd.c: Don't leak reader end of log pipe when it's finished. + * progs/play.c: Don't leak writer end of log pipe if fork fails. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/logfd.c progs/play.c + + +2005-11-05 15:38:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-226 + + Summary: + Release 1.4 + Revision: + disorder--mainline--0.1--patch-226 + + * BUGS: Document past problems with VIA OSS driver. + * configure.ac: Change version number. + * debian/changelog: New version number. + + modified files: + BUGS CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog + + +2005-11-05 15:01:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-225 + + Summary: + Update copyright dates and testing + Revision: + disorder--mainline--0.1--patch-225 + + * doc/ui-checklist.txt: More checks. + * scripts/check: Ignore certain files. + + new files: + scripts/copyright.exceptions + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/ui-checklist.txt + lib/syscalls.c lib/syscalls.h plugins/Makefile.am + plugins/exec.c plugins/shell.c scripts/check + + +2005-11-05 14:36:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-224 + + Summary: + New stopwords + Revision: + disorder--mainline--0.1--patch-224 + + * examples/config.sample.in: Add 'for' and 'is' to stopwords. + * debian/disorder.config: Add 'for' and 'is' to stopwords. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/disorder.config + examples/config.sample.in + + +2005-11-05 14:34:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-223 + + Summary: + Documentation cleanups. + Revision: + disorder--mainline--0.1--patch-223 + + * templates/help.html: Missing words. + * doc/disorder-dump.8.in: Typo. + * doc/disorder.3: Catch up with reality. + * doc/disorder_protocol.5.in: Document 'pause' and 'resume'. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder-dump.8.in + doc/disorder.3 doc/disorder_protocol.5.in doc/ui-checklist.txt + templates/help.html + + +2005-11-05 14:19:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-222 + + Summary: + Make @nfiles@ consistent. + Revision: + disorder--mainline--0.1--patch-222 + + * progs/dcgi.c: In @nfiles@, if cgi arg files is not set then default to + 1 (to be consistent with everywhere else). + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-11-05 14:15:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-221 + + Summary: + No Darwin support for now. + Revision: + disorder--mainline--0.1--patch-221 + + Remove references to Darwin, since ALSA dependency breaks it. You could + still build without support for raw players, but it won't work "out of + the box". + + removed files: + README.darwin + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 Makefile.am + + +2005-11-04 12:49:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-220 + + Summary: + disorder-speaker now uses ALSA. + Revision: + disorder--mainline--0.1--patch-220 + + This seems to work better than the various libao attempts. We still use + a libao plugin to capture decoded audio from ogg123 etc. + + * progs/speaker.c: Use ALSA directly rather than libao. + * lib/configuration.c: Configurable ALSA device. + * lib/log.c: Timestamp debug messages. + * configure.ac: Chheck for ALSA library. + * doc/disorder_config.5.in: Document 'device' option. + * README: Mention ALSA dependency. + * debian/control: Depend on alsa development library package. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README configure.ac + debian/changelog debian/control doc/disorder_config.5.in + lib/configuration.c lib/configuration.h lib/log.c + progs/Makefile.am progs/speaker.c + + +2005-11-02 17:58:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-219 + + Summary: + Log speaker stderr. + Revision: + disorder--mainline--0.1--patch-219 + + * progs/play.c: log speaker stderr + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/play.c + + +2005-11-01 15:54:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-218 + + Summary: + Typo fixes + Revision: + disorder--mainline--0.1--patch-218 + + * doc/disorder.1.in: Typo fixes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + + +2005-10-23 13:59:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-217 + + Summary: + Fix process-management problems. + Revision: + disorder--mainline--0.1--patch-217 + + * lib/hash.c: Grow array correctly so we don't lose PIDs. + * lib/queue.h: Abolish per-track signal. Too much hassle for not enough + gain. + * progs/play.c: Abolish per-track signal. Log failures to find PIDs. + * progs/speaker.c: Log too-short grace periods. + + * doc/disorder_config.5.in: Document the above. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw + debian/changelog doc/disorder_config.5.in lib/hash.c + lib/queue.c lib/queue.h progs/play.c progs/speaker.c + + +2005-10-22 16:22:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-216 + + Summary: + Various shutdown fixes + Revision: + disorder--mainline--0.1--patch-216 + + * progs/play.c: Terminate all players on shutdown, not just the one for + the currently playing track. + * lib/configuration.c: Default signal to forcibly terminate players is + now SIGKILL. + * examples/config.sample.in: Don't use --signal=SIGKILL any more. + * debian/disorder.config: Don't use --signal=SIGKILL any more. + * doc/disorder_config.5.in: Document change to SIGKILL. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.raw + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in lib/configuration.c progs/play.c + + +2005-10-22 16:03:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-215 + + Summary: + Stop web UI refreshing like mad + Revision: + disorder--mainline--0.1--patch-215 + + * progs/dcgi.c: Don't clamp refresh to gap if next track in queue is a + random track and random play is not enabled. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/dcgi.c + + +2005-10-16 20:16:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-214 + + Summary: + Don't forget PIDs across reconfiguration. + Revision: + disorder--mainline--0.1--patch-214 + + This change moves PIDs out of queue entries and instead uses a hash table + to map IDs to PIDs. The reason is that on reconfigure the queue is saved + and reloaded, losing the PID along the way. + + * lib/hash.c: Simple hash table. + * lib/queue.h: Don't keep PID in queue data structure. + * progs/play.c: Move PID records to a hash table. + + new files: + lib/hash.c lib/hash.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/queue.c lib/queue.h progs/play.c + + +2005-10-16 18:12:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-213 + + Summary: + Clean up rescan abort. + Revision: + disorder--mainline--0.1--patch-213 + + * progs/trackdb.c: reap_rescan() no longer trashes PID of new rescanner. + trackdb_scan() is now interruptible. Propagate debug status to + subprocesses. + * progs/deadlock.c: --no-debug option. + * progs/rescan.c: Cleaner abort. --no-debug option. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/deadlock.c + progs/rescan.c progs/trackdb-int.h progs/trackdb.c + + +2005-10-16 17:38:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-212 + + Summary: + Don't forget lookahead on reconfigure. + Revision: + disorder--mainline--0.1--patch-212 + + * progs/play.c: Commit queue after adding a random track. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/play.c + + +2005-10-16 17:33:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-211 + + Summary: + More on disorder-rescan interruption. + Revision: + disorder--mainline--0.1--patch-211 + + * progs/rescan.c: SA_RESTART for SIGINT/SIGTERM. + * CHANGES: Mention the reconfigure hang bug as fixed. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/rescan.c + + +2005-10-16 17:24:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-210 + + Summary: + Propagate reconfigure requests to the speaker process. + Revision: + disorder--mainline--0.1--patch-210 + + This is more work than it sounds because previously the reconfigure code + would leak memory in the absence of a garbage collector. It still will + on error, but this should be rather rare - if the server fails to parse + the configuration it doesn't tell the speaker to reload it. + + Along the way fixed a logging bug and a rescan bug than could hang the + server in a db close operation. + + * progs/state.c: Transmit reconfiguration requests to the speaker process. + * progs/play.c: Transmit reconfiguration requests to the speaker process. + * progs/speaker.c: Log configuration reloads. + * lib/configuration.c: Free old configs when new ones are installed. + This implies taking a bit more care over when we strdup. + Also put in VALUE/ADDRESS macros for less typo-prone access to + configuration struct members. + * lib/mem.c: xfree() calls free/GC_free as appropriate. + * lib/charset.c: Use realloc rather than malloc to avoid leaking memory + in non-GC case + * progs/rescan.c: Shut down cleanly on SIGTERM/SIGINT to keep db happy. + Also use stderr rather than stdout to determine whether to log to + syslog or stderr. + * progs/trackdb.c: Only redirect subprogram output if we're not on a + terminal. Also stomp db_deadlock_pid on a just-in-case basis. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/configuration.c lib/configuration.h lib/mem.c lib/mem.h + progs/play.c progs/play.h progs/rescan.c progs/speaker.c + progs/state.c progs/trackdb.c + + +2005-10-16 15:44:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-209 + + Summary: + More careful checking for track termination in speaker. + Revision: + disorder--mainline--0.1--patch-209 + + * progs/speaker.c: Cope with player being interrupted part way through a + frame. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + progs/speaker.c + + +2005-10-16 13:53:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-208 + + Summary: + Workarounds for various external bugs. + Revision: + disorder--mainline--0.1--patch-208 + + * README.raw: Document ogg123 braindamage. + * examples/config.sample.in: --signal=SIGKILL for ogg123. + * debian/disorder.config: --signal=SIGKILL for ogg123. + * driver/disorder.c: Call exit instead of _exit, to allow for player + cleanup. + * lib/event.c: Work around ptrace braindamage. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.raw + debian/changelog debian/disorder.config driver/disorder.c + examples/config.sample.in lib/event.c + + +2005-10-15 19:20:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-207 + + Summary: + Fix crash in speaker process. + Revision: + disorder--mainline--0.1--patch-207 + + * progs/speaker.c: Check the number of bits per sample in the incoming + data, not the currently playing track. This could not only cause the + answer to be potentially wrong, but would crash if there was no playing + track. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/speaker.c + + +2005-10-15 15:59:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-206 + + Summary: + Catch up. + Revision: + disorder--mainline--0.1--patch-206 + + * CHANGES: Mention multi-track editing. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-15 15:44:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-205 + + Summary: + Edit prefs for a whole directory at once. + Revision: + disorder--mainline--0.1--patch-205 + + * progs/dcgi.c: Support editing prefs for multiple track simultaneously. + * templates/choose.html: Link to whole-directory prefs edit. + * templates/prefs.html: Support editing prefs for multiple track + simultaneously. We lose the raw prefs interface for the time being; it + remains available via the command line however. + * templates/recent.html: New prefs args. + * templates/options.labels: New labels for whole-directory prefs-editing. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/choose.html templates/options.labels + templates/prefs.html templates/recent.html + + +2005-10-15 13:07:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-204 + + Summary: + Random track lookahead. + Revision: + disorder--mainline--0.1--patch-204 + + * lib/queue.c: Support adding queue entries just before the final random + track. Cope with reading back in randomly pick tracks from a saved + queue (they have no submitter which was previously not allowed). + * progs/dcgi.c: @who@ now just reports an empty string if there was no + submitter rather than  . Also correct the order of the table of + expansions. + * progs/play.c: add_random_track() adds a random track if random play is + enabled and there is no such track in the queue. play() now uses that + instead of doing it itself, and doesn't play random tracks found in the + queue if random play is disabled. It also adds a new random track just + after playing the old one. + If necessary add a random track when play/random play are enabled. + * progs/server.c: Add a new random track if the old one is removed. Also + when random track is moved to a non-final position in the queue then we + make it into a normal track and add a new random track. + * templates/playing.html: Indicate the random track in the queue and if + playing, using new labels. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/queue.c + lib/queue.h progs/dcgi.c progs/play.c progs/play.h + progs/server.c templates/options.labels templates/playing.html + + +2005-10-15 12:28:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-203 + + Summary: + Cope with randomly chosen tracks in the queue. + Revision: + disorder--mainline--0.1--patch-203 + + At the moment nothing adds such tracks to the queue, so this is just + laying foundations. + + * lib/queue.h: New 'random' track state indicates that the track was a + randomly chosen track in the queue rather than explicitly requested. + * progs/dcgi.c: @state@ expansion provides access to track state field in + queue. Process random tracks just as unplayed ones. + * progs/play.c: Handle 'random' state just like 'unplayed'. + * doc/disorder_config.5.in: Document @state@ expansion and track states. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + lib/queue.c lib/queue.h progs/dcgi.c progs/play.c + + +2005-10-15 11:58:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-202 + + Summary: + Complete label docs. + Revision: + disorder--mainline--0.1--patch-202 + + * templates/options.labels: Document links.css, missed in previous pass. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + + +2005-10-15 11:56:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-201 + + Summary: + More change detail. + Revision: + disorder--mainline--0.1--patch-201 + + * CHANGES: More detail. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-15 11:39:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-200 + + Summary: + Move documentation of labels to options.labels. + Revision: + disorder--mainline--0.1--patch-200 + + * templates/options.labels: Document labels. + * doc/disorder_config.5.in: No longer document labels here. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in templates/options.labels + + +2005-10-15 11:16:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-199 + + Summary: + New buttons to move to head/tail of queue. + Revision: + disorder--mainline--0.1--patch-199 + + * images/Makefile.am: Install new images. + * templates/playing.html: New buttons to move to head/tail. + * templates/options.labels: Labels to configure new buttons. + * debian/options.debian: Keep up to date. + * templates/help.html: Document new queue management. + * doc/disorder_config.5.in: Document new labels (at least as well as the + old ones; I am starting to think that the options.* files would be a + better place for this stuff.) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + debian/options.debian doc/disorder_config.5.in + images/Makefile.am templates/help.html + templates/options.labels templates/playing.html + + +2005-10-15 10:55:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-198 + + Summary: + new images + Revision: + disorder--mainline--0.1--patch-198 + + New {no,}{upup,downdown}.png images for sending a track right to the end + of the queue. + + new files: + images/.arch-ids/downdown.png.id + images/.arch-ids/nodowndown.png.id + images/.arch-ids/noupup.png.id images/.arch-ids/upup.png.id + images/downdown.png images/nodowndown.png images/noupup.png + images/upup.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-10-14 18:33:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-197 + + Summary: + more debian catchup + Revision: + disorder--mainline--0.1--patch-197 + + * debian/options.debian: bring images up to date + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/options.debian + + +2005-10-14 18:14:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-196 + + Summary: + debian config catchup + Revision: + disorder--mainline--0.1--patch-196 + + * debian/disorder.config: no gap between tracks + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/disorder.config + + +2005-10-09 13:31:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-195 + + Summary: + Documentation. + Revision: + disorder--mainline--0.1--patch-195 + + * templates/help.html: Bring help up to date with UI changes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/help.html + + +2005-10-09 13:24:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-194 + + Summary: + Release notes. + Revision: + disorder--mainline--0.1--patch-194 + + * CHANGES: More coherent change description. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-09 13:07:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-193 + + Summary: + More release notes. + Revision: + disorder--mainline--0.1--patch-193 + + * README.raw: mpg321 is less buggy than ogg123. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw + + +2005-10-09 13:05:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-192 + + Summary: + Fix plugin interface. + Revision: + disorder--mainline--0.1--patch-192 + + * lib/disorder.h: Missing 'extern'. + * lib/speaker.c: Build fixes (for Darwin). + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/speaker.c + + +2005-10-09 12:59:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-191 + + Summary: + Pre-decode raw tracks; better support for buggy players; tidy-ups. + Revision: + disorder--mainline--0.1--patch-191 + + * progs/play.c: prepare() and abandon() allow tracks to be prepared for + play (and abandoned when they are removed from the queue) before they + are actually to be played. + * progs/server.c: Call prepare() and abandon() when appropriate. + * driver/disorder.c: New 'fragile' option to work around players that + ignore write errors. + + * doc/disorder_config.5.in: Document libao driver properly. + * README.raw: Bring notes on raw players up to date. + + * examples/config.sample.in: Use fragile driver option for ogg213. + * debian/disorder.config: Use disorder libao driver. + * debian/control: Depend on mpg321 directly as we address its command + line syntax now. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.raw debian/control + debian/disorder.config doc/disorder_config.5.in + driver/disorder.c examples/config.sample.in progs/play.c + progs/play.h progs/server.c + + +2005-10-09 10:52:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-190 + + Summary: + cleanfiles + Revision: + disorder--mainline--0.1--patch-190 + + * progs/Makefile.am: clean up temprary files + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + + +2005-10-09 10:41:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-189 + + Summary: + miscellaneous command-line fixups + Revision: + disorder--mainline--0.1--patch-189 + + * progs/Makefile.am: check that command completions are up to date + * scripts/completion.bash: bring command completions up to date + * progs/disorder.c: fix help for scratch-id + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + progs/disorder.c scripts/completion.bash + + +2005-10-08 17:00:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-188 + + Summary: + more web pause + Revision: + disorder--mainline--0.1--patch-188 + + Sort out pausing labels and use thereof. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + templates/playing.html + + +2005-10-08 16:55:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-187 + + Summary: + pausing from the web + Revision: + disorder--mainline--0.1--patch-187 + + * progs/dcgi.c: new @paused@ expansion and pause/resume actions. + * templates/playing.html: pausing. State buttons now have a uniform + button and a tick/cross by them to indicate state. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/disorder.css templates/options.labels + templates/playing.html + + +2005-10-08 16:32:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-186 + + Summary: + Images again + Revision: + disorder--mainline--0.1--patch-186 + + More coherent image naming. + + removed files: + images/.arch-ids/cross.png.id images/cross.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/Makefile.am + templates/options.labels templates/playing.html + + renamed files: + images/.arch-ids/noscratch.png.id + ==> images/.arch-ids/nocross.png.id + images/.arch-ids/scratch.png.id + ==> images/.arch-ids/cross.png.id + images/noscratch.png + ==> images/nocross.png + images/scratch.png + ==> images/cross.png + + +2005-10-08 16:29:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-185 + + Summary: + new images + Revision: + disorder--mainline--0.1--patch-185 + + Tick and cross images. + + new files: + images/.arch-ids/cross.png.id images/.arch-ids/tick.png.id + images/cross.png images/tick.png + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/Makefile.am + + +2005-10-08 15:08:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-184 + + Summary: + release notes + Revision: + disorder--mainline--0.1--patch-184 + + * CHANGES: mention pausing + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-10-08 15:05:57 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-183 + + Summary: + more pausing work + Revision: + disorder--mainline--0.1--patch-183 + + * lib/plugin.c: fix a rather disastrous typo + * lib/queue.c: new 'paused' state for a track. queue_fix_sofar() broken + out as a separate function. + * progs/dcgi.c: cope with paused tracks + * progs/server.c: call queue_fix_sofar() before sending currently playing + track or calculating expected start times. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c lib/queue.c + lib/queue.h progs/dcgi.c progs/disorder.c progs/play.c + progs/server.c + + +2005-10-08 14:38:22 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-182 + + Summary: + support pausers that don't know how much played + Revision: + disorder--mainline--0.1--patch-182 + + * lib/queue.c: support a played-so-far return of -1, as described in + disorder.h + * doc/disorder.3: document played-so-far of -1. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 lib/queue.c + + +2005-10-08 14:26:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-181 + + Summary: + typo + Revision: + disorder--mainline--0.1--patch-181 + + * progs/disorder.c: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-10-08 14:24:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-180 + + Summary: + more docs + Revision: + disorder--mainline--0.1--patch-180 + + * doc/disorder.3: note that prefork/pause functions mustn't block. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + + +2005-10-08 14:15:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-179 + + Summary: + more pause/resume work + Revision: + disorder--mainline--0.1--patch-179 + + * lib/play.c: Pausing is now a capabality (of standalone players) not + a new player type. New notify calls for pausing/resuming. + * python/disorder.py.in: Python bindings for pause/resume. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/plugin.c lib/plugin.h plugins/notify.c + progs/play.c python/disorder.py.in + + +2005-10-08 14:05:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-178 + + Summary: + docs + minor fixes + Revision: + disorder--mainline--0.1--patch-178 + + * doc/disorder.3: document updated player plugin interface + * doc/disorder.1.in: document pause/resume commands + * lib/disorder.h: correct sifnature of disorder_play_resume + * progs/disorder.c: correct order of commands + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder.3 lib/disorder.h progs/disorder.c + + +2005-10-08 13:49:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-177 + + Summary: + typo fix + Revision: + disorder--mainline--0.1--patch-177 + + * progs/disorder.c: typo + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-10-08 13:48:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-176 + + Summary: + implement pausing support + Revision: + disorder--mainline--0.1--patch-176 + + _PLAYER_PAUSES is not really tested since I don't have any playes that + support that protocol. Also the web interface does not know anything + about this yet, and it is not documented. + + * progs/play.c: Implement pausing. + + * lib/queue.c: Pause/resume tracking for _PLAYER_PAUSES players. + * lib/client.c: C pause/resume bindings. + * lib/plugin.c: Pause/resume stubs. + * progs/disorder.c: Pause/resume commands. + * progs/server.c: Pause/resume commands. Automatically resumes under + conditions. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/client.c lib/client.h + lib/plugin.c lib/plugin.h lib/queue.c lib/queue.h + progs/disorder.c progs/play.c progs/play.h progs/server.c + + +2005-10-05 21:53:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-175 + + Summary: + documentation catchup + Revision: + disorder--mainline--0.1--patch-175 + + First pass at documenting the latest batch of changes. Also updated the + sample config file to use raw players for OGG and MP3, and to remove the + inter-track gap. + + new files: + README.raw + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + README.upgrades doc/disorder_config.5.in + examples/config.sample.in + + +2005-10-05 20:58:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-174 + + Summary: + implement and use the speaker process. + Revision: + disorder--mainline--0.1--patch-174 + + No documentation yet! + + * driver/disorder.c: Look at ${DISORDER_RAW_FD} for default output file + descriptor. + * lib/plugin.c: New play_get_type(). Tidy up. + * lib/queue.h: Correct order of enum constants. + New queue entry fields: + * type - type word from plugin + * pid - process ID of decoder/player + * sofar - how many seconds played so far + * signal - signal to kill decoder/player + * lib/queue.c: fake up sofar field where it's not filled in by the + speaker process + * lib/syscalls.c: we never use chdir, so remove it. + * plugins/execraw.c: exec plugin for decoders, uses modified exec.c + * progs/deadlock.c: use stderr not stdout to determine whether we're a + daemon + * progs/disorder.c: report sofar + * progs/disorderd.c: start speaker process + * progs/play.c: use the speaker process for _RAW players. + + send the speaker an SM_PLAY if a _RAW player + + support --wait-for-device and --signal player options + + act on incoming messages from the speaker + + split player-finished logic into process-finished and generic halves + + still scratch even if subprocess terminated + + disconnect from speaker when quitting + * lib/signame.c: split out of configuration.c, needed by --signal player + option + * lib/speaker.c: speaker/server communication support + * progs/speaker.c: the speaker process itself. + + new files: + lib/signame.c lib/signame.h lib/speaker.c lib/speaker.h + plugins/execraw.c progs/speaker.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 driver/disorder.c + lib/Makefile.am lib/configuration.c lib/configuration.h + lib/disorder.h lib/plugin.c lib/plugin.h lib/queue.c + lib/queue.h lib/syscalls.c lib/syscalls.h plugins/Makefile.am + plugins/exec.c plugins/shell.c progs/Makefile.am + progs/deadlock.c progs/disorder.c progs/disorderd.c + progs/play.c progs/play.h progs/state.c + + +2005-10-01 17:31:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-173 + + Summary: + Prefork/cleanup support + Revision: + disorder--mainline--0.1--patch-173 + + * lib/plugin.c: simplify plugin opening interface. The main reason for + the plugin struct is to keep track of plugin names and to avoid repeat + opening, but it also allows the hiding of the dlopen() interface. + Also add support for the prefork/cleanup interface. + + * lib/queue.h: track plugin and its data for each track. Currently only + for the playing track but perhaps also for pre-decoded tracks in the + future. + + * progs/play.c: Support the prefork/cleanup player plugin interface. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c lib/plugin.h + lib/queue.c lib/queue.h progs/play.c + + +2005-10-01 16:35:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-172 + + Summary: + new plugin interface + Revision: + disorder--mainline--0.1--patch-172 + + New plugin interface declarations. No implementation behind them yet + however. + + * lib/disorder.h: new player plugin declarations + * plugins/shell.c: new plugin interface + * plugins/exec.c: new plugin interface + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/disorder.h + plugins/exec.c plugins/shell.c + + +2005-10-01 16:16:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-171 + + Summary: + libao driver + Revision: + disorder--mainline--0.1--patch-171 + + * driver/disorder.c: libao driver that outputs in raw format with a + descriptive header. + * README: note dependency on libao + + new files: + driver/.arch-ids/=id driver/Makefile.am driver/disorder.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am README + README.darwin configure.ac + + new directories: + driver driver/.arch-ids + + +2005-09-27 18:30:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-170 + + Summary: + random play enabled on first startup + Revision: + disorder--mainline--0.1--patch-170 + + If random play is enabled before we've got any tracks then + trackdb_random() tried to get record 1 of 0, and fails, and terminates + the process. Fixed. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/trackdb.c + + +2005-08-07 11:11:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-169 + + Summary: + build against fink libraries without extra configure args + Revision: + disorder--mainline--0.1--patch-169 + + * configure.ac: if fink is installed, add some CPPFLAGS/LDFLAGS based on + its path. + * README.darwin: no longer need to specify paths to fink libraries. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.darwin configure.ac + + +2005-07-04 21:55:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-168 + + Summary: + more build-time testing + Revision: + disorder--mainline--0.1--patch-168 + + * lib/Makefile.am: don't automatically run checks + * progs/Makefile.am: check that --help options work + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/Makefile.am scripts/dist scripts/inst + + +2005-07-04 21:40:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-167 + + Summary: + don't crash in --help-commands + Revision: + disorder--mainline--0.1--patch-167 + + * progs/disorder.c: don't crash in --help-commands + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/disorder.c + + +2005-06-18 17:31:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-166 + + Summary: + correct version numbers + Revision: + disorder--mainline--0.1--patch-166 + + * README.upgrades: correct version numbers + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + + +2005-06-17 17:24:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-165 + + Summary: + post 1.3 + Revision: + disorder--mainline--0.1--patch-165 + + * CHANGES: remove entries up to 1.0. 200 lines is plenty of history in a + file people are expected to actually read. + * configure.ac: version changed to 1.3+dev + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + + +2005-06-16 20:42:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-164 + + Summary: + release 1.2 + Revision: + disorder--mainline--0.1--patch-164 + + * progs/server.c: save queue after move operation + * configure.ac: release 1.3 + * debian/changelog: release 1.3 + * CHANGES: release 1.3 + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog progs/server.c + + +2005-06-16 20:31:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-163 + + Summary: + missing documentation + Revision: + disorder--mainline--0.1--patch-163 + + * doc/disorder_config.5.in: document 'alias' directive + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-06-16 20:16:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-162 + + Summary: + distribution administrivia + Revision: + disorder--mainline--0.1--patch-162 + + * debian/rules.m4: install all READMEs + * progs/Makefile.am: remember trackdb-int.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/changelog + debian/rules.m4 progs/Makefile.am + + +2005-06-08 09:48:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-161 + + Summary: + quieten tree-lint + Revision: + disorder--mainline--0.1--patch-161 + + * {arch}/=tagging-method: stop tree-lint from complaining about emacs + droppings + + modified files: + ChangeLog.d/disorder--mainline--0.1 {arch}/=tagging-method + + +2005-06-01 22:45:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-160 + + Summary: + darwin port update + Revision: + disorder--mainline--0.1--patch-160 + + Build on Mac OS X again. + + * progs/trackdb.c: missing header + * progs/trackdb-int.h: correct types + * README.darwin: note requirement for db4.3 + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.darwin + progs/trackdb-int.h progs/trackdb.c + + +2005-05-31 22:13:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-159 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-159 + + * CHANGES: mention missing files included for 1.2.1. The fclose checked + in 1.2.1 isn't present any more anyway. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-05-31 19:55:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-158 + + Summary: + web ui reports errors more gracefully + Revision: + disorder--mainline--0.1--patch-158 + + * progs/cgimain.c: report errors by calling disorder_cgi_error() + * progs/dcgi.c: disorder_cgi_error expands the 'error' template with the + 'error' label set to an error indicator string. + * progs/cgi.c: cgi_set_option allows web options to be set other than + through the config files + + * templates/error.html: new template for error pages + * templates/options.labels: default error strings + + new files: + templates/error.html + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/cgi.c progs/cgi.h + progs/cgimain.c progs/dcgi.c progs/dcgi.h + templates/Makefile.am templates/options.labels + + +2005-05-31 19:26:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-157 + + Summary: + management screen fixes + Revision: + disorder--mainline--0.1--patch-157 + + * templates/playing.html: put a <form> around volume input boxes, so the + client knows where to send the change. + * templates/disorder.css: management stuff is all displayed inline + * doc/ui-checklist.txt: new checklist for manual UI testing + + new files: + doc/ui-checklist.txt + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/disorder.css + templates/playing.html + + +2005-05-31 18:23:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-156 + + Summary: + minor build fixes + Revision: + disorder--mainline--0.1--patch-156 + + * progs/dcgi.c: quieten compiler + * lib/Makefile.am: delete the right file + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/dcgi.c + + +2005-05-30 11:02:45 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-155 + + Summary: + color enable/disable buttons to reflect current state + Revision: + disorder--mainline--0.1--patch-155 + + * templates/disorder.css: enable/disable button colors + * templates/playing.html: enable/disable buttons get red/green + backgrounds + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/disorder.css templates/options.labels + templates/playing.html + + +2005-05-30 10:27:41 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-154 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-154 + + Administrivia. + + modified files: + ChangeLog.d/disorder--mainline--0.1 acinclude.m4 + examples/disorder.init.in progs/api-server.c + + +2005-05-29 23:17:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-153 + + Summary: + update CHANGES + Revision: + disorder--mainline--0.1--patch-153 + + * CHANGES: note that subprocess output is logged + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-05-29 22:51:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-152 + + Summary: + log output of subprocesses + Revision: + disorder--mainline--0.1--patch-152 + + * lib/logfd.c: logfd() returns an FD which (via the event loop) ends up + in log output. Useful to pick up stdout/stderr of subprocesses. + * progs/trackdb.c: log output of deadlock and rescan subprocesses + * progs/play.c: log output of player subprocesses + + * progs/rescan.c: check for newline in raw path, not track + * progs/dcgi.c: quieten compiler + * progs/dump.c: quieten compiler + + new files: + lib/logfd.c lib/logfd.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/dcgi.c progs/dump.c progs/play.c progs/rescan.c + progs/trackdb.c + + +2005-05-29 18:30:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-151 + + Summary: + unweird search output + Revision: + disorder--mainline--0.1--patch-151 + + Searching was producing weird results: the artist 'Various' was being + displayed as the first aliased artist (but sorted, correctly, as + 'Various'). This change fixes this, though artists are still sorted by + their display name (so the 'The ...' bug remains in this context). + + * progs/dcgi.c: @search@ takes an additional CONTEXT argument, and marks + the last element in a group correctly. + * doc/disorder_config.5.in: document changes to @search@ + * templates/search.html: @search@ context used to avoid weirdness. + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c templates/search.html + + +2005-05-29 18:01:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-150 + + Summary: + rewritten track database code + Revision: + disorder--mainline--0.1--patch-150 + + * lib/disorder.h: _count and _getn are gone, replaced by _random + * plugins/pick.c: use _random instead of _count and _getn + + * lib/client.c: rescan no longer takes an arg; dump is gone; new resolve + command. + * lib/filepart.c: strip_extension() and extension() + * lib/trackname.c: implement 'ext' and 'path' parts here + * progs/api-server.c: adapt to new trackdb_ interface + * progs/dcgi.c: resolve paths when figuring out track status; implement + @resolve@ + * progs/disorder.c: new resolve, scratch-id; modified rescan + * progs/disorderd.c: adapt to new trackdb_ code; open database after + taking lockfile, not before; move user change code to user.c + * progs/play.c: adapt to new trackdb_ interface + * progs/server.c: adapt to new trackdb_ interface. resolve aliases when + playing. dump is gone. new resolve command. + * progs/state.c: dapt to new trackdb_ interface + * progs/trackdb.c: new trackdb. Largely rewritten though a little old + tracks.c material remains. + + * progs/dump.c: rewritten for new trackdb_ code. Dump and undump both + access the database directly and work while the server is running + (although particularly undumping while it is running is likely to be + painful). --recompute-aliases and --remove-pathless can be used to + tidy up broken databases, mostly useful when developing. --recover and + --recover-fatal provide access to libdb facilities (though these are + also available through db_recover). + * progs/deadlock.c: disorder-deadlock implementation. Just runs the + deadlock detector once a second. + * progs/rescan.c: disorder-rescan implementation. + * progs/user.c: user-switching code taken from disorderd.c + + * templates/choose.html: resolve track when linking to prefs + + * scripts/completion.bash: disorder-dump support + * examples/disorder.init.in: put sbindir on the path + + * debian/control: need libdb4.3-dev + + * doc/disorder-dump.8.in: document rewritten dump program + * doc/disorder.1.in: document resolve, scratch-id and modified rescan + * doc/disorder.3: _count and _getn are gone, replaced by _random; recheck + happens in a subprocess. + * doc/disorder_config.5.in: document @resolve@ + * doc/disorder_protocol.5.in: document resolve and changed rescan; dump + is gone. + * doc/disorderd.8.in: helper programs must be on the path; discuss + backups using disorder-dump. Mention DB_CONFIG. + + new files: + doc/disorder-deadlock.8.in doc/disorder-rescan.8.in + progs/deadlock.c progs/rescan.c progs/trackdb-int.h + progs/trackdb.c progs/trackdb.h progs/user.c progs/user.h + + removed files: + progs/rescan.c progs/tracks.c progs/tracks.h + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + README.upgrades debian/control doc/Makefile.am + doc/disorder-dump.8.in doc/disorder.1.in doc/disorder.3 + doc/disorder_config.5.in doc/disorder_protocol.5.in + doc/disorderd.8.in examples/disorder.init.in lib/client.c + lib/client.h lib/disorder.h lib/event.c lib/filepart.c + lib/filepart.h lib/log-impl.h lib/trackname.c plugins/pick.c + progs/Makefile.am progs/api-server.c progs/dcgi.c + progs/disorder.c progs/disorderd.c progs/dump.c progs/play.c + progs/server.c progs/state.c scripts/completion.bash + scripts/inst templates/choose.html + + +2005-05-27 14:24:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-149 + + Summary: + new 'lock' directive + Revision: + disorder--mainline--0.1--patch-149 + + * progs/disorderd.c: take lock here, under control of 'lock' directive + * progs/tracks.c: don't lock here any more + * lib/configuration.c: 'lock' directive + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/disorderd.c progs/tracks.c + + +2005-05-27 14:19:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-148 + + Summary: + wrap up program name setting + Revision: + disorder--mainline--0.1--patch-148 + + * lib/log.c: set_progname() sets progname from argv[0] + * progs/disorderd.c: use set_progname() + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c lib/log.h + progs/disorderd.c + + +2005-05-27 14:17:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-147 + + Summary: + optionally abort on fatal error + Revision: + disorder--mainline--0.1--patch-147 + + * lib/log-impl.h: DISORDER_FATAL_ABORT=yes forces fatal() to abort, for + debugging purposes. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log-impl.h + + +2005-05-27 14:04:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-146 + + Summary: + more liberal url-encoding + Revision: + disorder--mainline--0.1--patch-146 + + * lib/kvp.c: allow RFC2396 unreserved characters plus '/' to pass + URL-encoding unchanged. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/kvp.c + + +2005-05-27 13:55:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-145 + + Summary: + more conservative signal handling + Revision: + disorder--mainline--0.1--patch-145 + + * lib/event.c: if we fail to write to the signal pipe then abort. This + saves worry about write() modifiying errno. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/event.c + + +2005-05-27 13:50:27 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-144 + + Summary: + libtool 1.4 no good + Revision: + disorder--mainline--0.1--patch-144 + + * README: libtool note + + modified files: + ChangeLog.d/disorder--mainline--0.1 README + + +2005-05-26 12:02:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-143 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-143 + + Update copyright dates. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/regsub.c lib/regsub.h + lib/vector.h + + +2005-05-26 11:54:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-142 + + Summary: + detect missing symbols in plugins early + Revision: + disorder--mainline--0.1--patch-142 + + * lib/plugin.c: use RTLD_NOW instead of RTLD_LAZY. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/plugin.c + + +2005-05-22 13:15:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-141 + + Summary: + minor doc fixes + Revision: + disorder--mainline--0.1--patch-141 + + * debian/README.Debian: leftover names converted to /etc/disorder. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/README.Debian + + +2005-05-21 17:16:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-140 + + Summary: + check for UTF-8 support in pcre. + Revision: + disorder--mainline--0.1--patch-140 + + * acinclude.m4: new RJK_REQUIRE_PCRE_UTF8 macro checks that PCRE was + built with UTF-8 support. + + modified files: + ChangeLog.d/disorder--mainline--0.1 acinclude.m4 configure.ac + + +2005-05-21 12:17:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-139 + + Summary: + config overview + Revision: + disorder--mainline--0.1--patch-139 + + * doc/disorder_config.5.in: add an overview section + + + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-05-15 20:02:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-138 + + Summary: + unchecked fclose + Revision: + disorder--mainline--0.1--patch-138 + + * progs/tracks.c: unchecked fclose() + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-05-15 19:59:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-137 + + Summary: + correct for multiple =build directories + Revision: + disorder--mainline--0.1--patch-137 + + * {arch}/=tagging-method: correct =build* exclusion + + modified files: + ChangeLog.d/disorder--mainline--0.1 {arch}/=tagging-method + + +2005-05-15 19:57:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-136 + + Summary: + typos + Revision: + disorder--mainline--0.1--patch-136 + + * doc/disorder_config.5.in: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-05-11 18:56:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-135 + + Summary: + add missing files + Revision: + disorder--mainline--0.1--patch-135 + + * lib/filepart.c, lib/filepart.c: missing files from previous commit. + Thought that wasn't supposed to be possible... + + new files: + lib/filepart.c lib/filepart.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-05-08 19:05:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-134 + + Summary: + results of trackname_ prefs appear in virtual filesystem + Revision: + disorder--mainline--0.1--patch-134 + + * lib/configuration.c: 'alias' directive; syntax-check collection root + * lib/regsub.c: REGSUB_REPLACE flag to just expand the substitution + string rather than substituting it into the subject string + * lib/trackname.c: compute track name parts from track name without + collection root. + * progs/rescan.c: recompute aliases as part of rescan. + * progs/tracks.c: support aliases, i.e. names that appear in the virtual + filesystem corresponding to the results of trackname_display_ prefs. + When they are in the same directory as their real file, only the alias + is visible. + + * README.upgrades: note 'ext' namepart and namepart changes + * debian/disorder.config: add 'ext' directive and update namepart + directives for new semantics + * doc/disorder_config.5.in: note that collection root is removed. + * examples/config.sample.in: add 'ext' directive and update namepart + directives for new semantics + + modified files: + ChangeLog.d/disorder--mainline--0.1 README.upgrades + debian/disorder.config doc/disorder_config.5.in + examples/config.sample.in lib/Makefile.am lib/configuration.c + lib/configuration.h lib/regsub.c lib/regsub.h lib/trackname.c + lib/trackname.h lib/vector.h progs/play.c progs/rescan.c + progs/tracks.c progs/tracks.h + + +2005-05-07 16:08:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-133 + + Summary: + 'disorder authorize' command to simplify adding users + Revision: + disorder--mainline--0.1--patch-133 + + * lib/configuration.c: export paths to subsiduary configuration files as + required. + * progs/disorder.c: 'authorize' command + * progs/authorize.c: implementation of 'authorize' command + * scripts/completion.bash: add 'authorize' command to list + * README: mention disorder authorize; alternative advice about + config.USER ownership. + * doc/disorder.1.in: document 'authorize' command + * doc/disorder_config.5.in: config.private is root:jukebox, not jukebox:* + according to README and debian/postinst; make disorder_config(5) + consistent. + + new files: + progs/authorize.c progs/authorize.h + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + doc/disorder.1.in doc/disorder_config.5.in lib/configuration.c + lib/configuration.h progs/Makefile.am progs/disorder.c + scripts/completion.bash + + +2005-05-07 13:50:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-132 + + Summary: + more encoding safeguard text + Revision: + disorder--mainline--0.1--patch-132 + + * debian/templates (disorder/encoding): note in template text that you + can't guess the encoding. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/templates + + +2005-05-07 13:30:54 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-131 + + Summary: + notes about wrong fs encoding troubles + Revision: + disorder--mainline--0.1--patch-131 + + * README: emphasize need to get filesystem encoding right + * BUGS: note about difficulty of recovering from wrong filesystem + encoding. + + modified files: + BUGS ChangeLog.d/disorder--mainline--0.1 README + + +2005-03-12 17:53:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-130 + + Summary: + missing file + Revision: + disorder--mainline--0.1--patch-130 + + * progs/Makefile.am: remember to distribute getopt.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + + +2005-03-12 17:51:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-129 + + Summary: + post 1.2 + Revision: + disorder--mainline--0.1--patch-129 + + * configure.ac: 1.2+dev + + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + + +2005-03-11 20:07:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-128 + + Summary: + release 1.2 + Revision: + disorder--mainline--0.1--patch-128 + + * configure.ac, debian/changelog, CHANGES: version number + * scripts/inst: run ldconfig + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog scripts/inst + + +2005-03-05 20:57:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-127 + + Summary: + build fix + Revision: + disorder--mainline--0.1--patch-127 + + * plugins/mad.c: quieten gcc 4 prerelease + + modified files: + ChangeLog.d/disorder--mainline--0.1 plugins/mad.c + + +2005-03-04 23:31:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-126 + + Summary: + fix broken @navigate@ + Revision: + disorder--mainline--0.1--patch-126 + + * progs/dcgi.c: fix path name parsing in @navigate@. Was always broken + but never showed up until memory allocation changes in patch-125. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-03-04 19:41:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-125 + + Summary: + disable gc for some utilities + Revision: + disorder--mainline--0.1--patch-125 + + * lib/mem.c: callers can disable gc too + * progs/cgimain.c, progs/trackname.c: no gc for web interface or + trackname utility. These are very short running programs. + * progs/disorderd.c: enable gc. Long running program. + * progs/disorder.c, progs/dump.c: enable gc. disorder(1) might run a + long time if log is used; dump.c allocates memory for every track which + soon goes unreachable. If there are very many tracks then not freeing + this memory along the way might consume excessive amounts. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mem.c lib/mem.h + progs/cgimain.c progs/disorder.c progs/disorderd.c + progs/dump.c progs/trackname.c + + +2005-03-04 19:22:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-124 + + Summary: + optional disabling of garbage collection for debug purposes + Revision: + disorder--mainline--0.1--patch-124 + + * lib/mem.c: allow garbage collection to be turned off, e.g. for use with + memory allocation checkers that don't play nicely with libgc. To turn + garbage collection off set the environment variable DISORDER_GC=no. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mem.c + + +2005-03-01 00:08:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-123 + + Summary: + tidying up + Revision: + disorder--mainline--0.1--patch-123 + + * lib/log.h: move log_output structure to log.c, since it is not needed + by callers any more. + * templates/disorder.css, templates/volume.html, + templates/stylesheet.html, scripts/htmlman: copyright date + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/log.c lib/log.h + progs/dcgi.c progs/dcgi.h scripts/htmlman + templates/choosealpha.html templates/disorder.css + templates/sidebar.html templates/stylesheet.html + templates/volume.html + + +2005-02-27 19:43:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-122 + + Summary: + more spelling + Revision: + disorder--mainline--0.1--patch-122 + + * python/disorder.py.in: typo fixes + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + + +2005-02-27 19:41:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-121 + + Summary: + better python docs + Revision: + disorder--mainline--0.1--patch-121 + + * python/disorder.py.in: more verbose docs + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + templates/help.html + + +2005-02-27 19:21:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-120 + + Summary: + more 'tooltips' + Revision: + disorder--mainline--0.1--patch-120 + + * templates/playing.html: TITLE attributes for links and buttons + * templates/options.labels: values for the above attributes + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/options.labels + templates/playing.html + + +2005-02-27 19:05:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-119 + + Summary: + typo fixes + Revision: + disorder--mainline--0.1--patch-119 + + * README.upgrades: typo fixes + * CHANGES: typo fixes + * doc/disorder_config.5.in: typo fixes + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README.upgrades + doc/disorder_config.5.in + + +2005-02-27 18:24:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-118 + + Summary: + remove annoying hang in tkdisorder + Revision: + disorder--mainline--0.1--patch-118 + + * python/tkdisorder: perform initial fill of recent window (or whatever) + in a background thread, to avoid blocking the rest of the UI. We set + the cursor to 'watch' for the duration. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-02-26 20:36:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-117 + + Summary: + debian fixes for new web arrangements + Revision: + disorder--mainline--0.1--patch-117 + + * debian/options.debian: this becomes a debian-specific version of + /etc/disorder/options, with alternative URLs for static content. It's + marked as a conffile but the user would be better off putting their + changes into options.user. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/conffiles + debian/options.debian debian/rules.m4 + + +2005-02-26 18:46:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-116 + + Summary: + simpler static content configuration + Revision: + disorder--mainline--0.1--patch-116 + + * templates/options.labels: images.* and links.* are the full URL + (possibly relative to the disorder cgi base URL). + * templates/playing.html, templates/volume.html, templates/recent.html, + templates/help.html, templates/choose.html: fix image links + * templates/stylesheet.html: fix stylesheet URL + * doc/disorder_config.5.in: document URL changes + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + templates/choose.html templates/help.html + templates/options.labels templates/playing.html + templates/recent.html templates/stylesheet.html + templates/volume.html + + +2005-02-24 22:58:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-115 + + Summary: + static (non-embedded) stylesheet + Revision: + disorder--mainline--0.1--patch-115 + + * templates/disorder.css: copied from contents of stylesheet.html + * templates/stylesheet.html: just link to disorder.css + * templates/options.labels: links.css label identifies stylesheet + + new files: + templates/disorder.css + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/Makefile.am templates/options.labels + templates/stylesheet.html + + +2005-02-20 19:29:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-114 + + Summary: + link/image title attributes + Revision: + disorder--mainline--0.1--patch-114 + + * templates/options.labels: new *.*verbose labels used for informative + TITLE attributes on various links and images + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choose.html + templates/credits.html templates/options.labels + templates/playing.html templates/recent.html + templates/topbar.html templates/volume.html + + +2005-02-20 18:34:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-113 + + Summary: + more flexible choice of graphics + Revision: + disorder--mainline--0.1--patch-113 + + * templates/options.labels: individual labels for each graphic. + Currently everything has to be relative to url.static which might not + be ideal. + * templates/playing.html, templates/choose.html, templates/help.html, + templates/recent.html, templates/volume.html: use new labels for + graphics. We discard the width and height attributes which is a shame + but most of the time browsers will have a copy already so it shouldn't + be too problematic. + * doc/disorder_config.5.in: document new labels + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + templates/choose.html templates/help.html + templates/options.labels templates/playing.html + templates/recent.html templates/volume.html + + +2005-02-20 12:39:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-112 + + Summary: + missing bit of patch-107 + Revision: + disorder--mainline--0.1--patch-112 + + * templates/help.html: forgot to includ ethe menu end file in patch-107 + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/help.html + + +2005-02-20 12:35:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-111 + + Summary: + definitions moved from macros to library + Revision: + disorder--mainline--0.1--patch-111 + + * lib/defs.c: make macros defined by configure (VERSION, PKGCONFDIR, etc) + available as library symbols, rather than repeating them everywhere + they are use. + + new files: + lib/defs.c lib/defs.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/configuration.c lib/configuration.h lib/plugin.c + progs/cgi.c progs/dcgi.c progs/disorder.c progs/disorderd.c + progs/dump.c progs/server.c progs/trackname.c + + +2005-02-19 23:48:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-110 + + Summary: + fix search facility + Revision: + disorder--mainline--0.1--patch-110 + + * progs/dcgi.c: unbreak the search facility, which didn't return all the + results for an artist (or album) which contains more than one hit. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + + +2005-02-19 23:27:28 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-109 + + Summary: + missing bits from previous changes + Revision: + disorder--mainline--0.1--patch-109 + + * scripts/htmlman: make generated web pages use the new menu + infrastructure. + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/htmlman + + +2005-02-19 23:18:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-108 + + Summary: + revert clumsiness + Revision: + disorder--mainline--0.1--patch-108 + + * templates/choose.html: accidentally reverted class change in patch-107. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/choose.html + + +2005-02-19 23:10:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-107 + + Summary: + flexible menu infrastructure, new default menu layout + Revision: + disorder--mainline--0.1--patch-107 + + * progs/dcgi.c: @action@ expansion to make determining the current action + easier. + + * templates/options.labels: default 'menu' label to 'topbar' + + * doc/disorder_config.5.in: document 'menu' label and @action@ expansion + * templates/help.html: updated for the above changes + + * templates/topbar.html: new default menu template + * templates/topbarend.html: menu tempates now have a closing half too, + where the credits are output from. + * templates/stylesheet.html: formatting rules for topbar; make title + bigger to stand out against (rather big and friendly) menu items. + + * templates/sidebar.html: open the content div from the sidebar template + * templates/sidebarend.html: final half of sidebar template closes the + content div as well as outputting the credits. + + new files: + templates/sidebarend.html templates/topbar.html + templates/topbarend.html + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/Makefile.am + templates/about.html templates/choose.html + templates/choosealpha.html templates/help.html + templates/options.labels templates/playing.html + templates/prefs.html templates/recent.html + templates/search.html templates/sidebar.html + templates/stylesheet.html templates/volume.html + + +2005-02-19 22:02:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-106 + + Summary: + more powerful management page + Revision: + disorder--mainline--0.1--patch-106 + + * templates/playing.html: put volume control into management page + * templates/options.labels: new playing.volume label; + playing.{random,playing} labels now contain the colon formerly in the + template (for greater flexibility). + * doc/disorder_config.5.in: document playing.volume (and playing.playing + which was formerly missing) + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in templates/options.labels + templates/playing.html + + +2005-02-19 21:28:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-105 + + Summary: + volume control bug fixes + visual improvements + Revision: + disorder--mainline--0.1--patch-105 + + * progs/dcgi.c: make act_volume() redirect back to itself when a change + occurs, so that it's safe to reload volume pages + * templates/volume.html: use new graphics for up/down + * templates/stylesheet.html: center volume control + * templates/options.labels: volume.{left,right} set to null on the + assumption that user can deduce that the left and rights input boxes + control the left and right speakers respectively + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/dcgi.c + templates/options.labels templates/stylesheet.html + templates/volume.html + + +2005-02-19 21:03:03 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-104 + + Summary: + more graphical improvements + Revision: + disorder--mainline--0.1--patch-104 + + * images/scratch.png, images/noscratch.png: cleaner scratch button + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/noscratch.png + images/scratch.png + + +2005-02-19 20:37:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-103 + + Summary: + tidy up graphics a little + Revision: + disorder--mainline--0.1--patch-103 + + * images/edit.png: truncate the pencil perpendicular to its long axis, + rather than according to the corner of the bounding square, to + eliminate the visually confusing corner. + + modified files: + ChangeLog.d/disorder--mainline--0.1 images/edit.png + + +2005-02-19 20:32:56 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-102 + + Summary: + grey out inactive buttons + Revision: + disorder--mainline--0.1--patch-102 + + * progs/dcgi.c: @isfirst@ and @islast@ expansions report whether this is + the first or last (or other) iteration of a recursive template + expansion. + * templates/playing.html: 'grey out' up/down buttons for first/last track + in queue (since they don't do anything useful). + * doc/disorder_config.5.in: document @isfirst@/@islast@ + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + progs/dcgi.c progs/dcgi.h templates/playing.html + + +2005-02-19 20:17:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-101 + + Summary: + ignore whitespace between expansion arguments + Revision: + disorder--mainline--0.1--patch-101 + + * progs/cgi.c: ignore whitespace after '}' and at the start and end of + unquoted template expansion args. + * doc/disorder_config.5.in: document new whitespace rules + * templates/playing.html: missing class attribute on an img + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/cgi.c templates/playing.html + + +2005-02-19 19:05:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-100 + + Summary: + graphical buttons in web interface + Revision: + disorder--mainline--0.1--patch-100 + + * README, README.upgrades: mention appearance of /static + * doc/disorder_config.5.in: document url.static + + * templates/choose.html: graphical button for edit + * templates/help.html: catch up with graphical buttons; tidy up a bit. + * templates/playing.html: graphic buttons for scratch/remove/up/down + * templates/recent.html: graphical button for edit + * templates/stylesheet.html: img.button class for new graphical buttons + + * images/*.png: new graphics + + * debian/options.debian: debian-specific location for static content + * debian/conffiles: options.debian is a conffile + * debian/rules.m4: install static content under /var/www + install options.debian + + * progs/play.h: update copyright date + * examples/Makefile.am: update copyright date + * progs/daemonize.c: update copyright date + + new files: + debian/options.debian images/.arch-ids/=id + images/.arch-ids/down.png.id images/.arch-ids/edit.png.id + images/.arch-ids/nodown.png.id + images/.arch-ids/noscratch.png.id images/.arch-ids/noup.png.id + images/.arch-ids/scratch.png.id images/.arch-ids/up.png.id + images/Makefile.am images/down.png images/edit.png + images/nodown.png images/noscratch.png images/noup.png + images/scratch.png images/up.png + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 Makefile.am README + README.upgrades configure.ac debian/Makefile.am + debian/autorules.m4 debian/conffiles debian/rules.m4 + doc/disorder_config.5.in examples/Makefile.am + progs/daemonize.c progs/play.h templates/choose.html + templates/help.html templates/playing.html + templates/recent.html templates/stylesheet.html + + new directories: + images images/.arch-ids + + +2005-02-19 14:16:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-99 + + Summary: + restore debian/changelog to proper name + Revision: + disorder--mainline--0.1--patch-99 + + * debian/changelog: rename back to the standard name. I can't quite see + from the change history why it was changelog.Debian in the first place. + Hopefuly nothing will break. + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/Makefile.am + debian/autorules.m4 + + renamed files: + debian/changelog.Debian + ==> debian/changelog + + +2005-02-18 20:49:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-98 + + Summary: + docs catch up with changes below + Revision: + disorder--mainline--0.1--patch-98 + + * doc/disorder_protocol.5.in: document new 'quitting' state + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_protocol.5.in + + +2005-02-18 20:27:06 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-97 + + Summary: + mention previous bug fix + Revision: + disorder--mainline--0.1--patch-97 + + * CHANGES: mention previous bug fix + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + + +2005-02-18 20:15:05 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-96 + + Summary: + add current track to recently played list on server shutdown + Revision: + disorder--mainline--0.1--patch-96 + + When the recently played list was not preserved across restarts this was + unnecessary. It was left out when it started being preserved. + + * progs/play.c: when quitting, explicitly call player_finished() to get + the track added to the recently played list (and do notifications etc) + * progs/state.c: tell playing module we're quitting rather than just + disabling playing and expecting it to guess + * lib/queue.h: new track state for a track terminated by server shutdown + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/queue.c lib/queue.h + progs/dcgi.c progs/play.c progs/play.h progs/state.c + + +2005-02-18 16:51:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-95 + + Summary: + disorder.monitor python class + Revision: + disorder--mainline--0.1--patch-95 + + * python/disorder.py.in: disorder.monitor class provides cooked access to + event log + * examples/disorder-log: sketch example of using disorder.monitor + + * CHANGES: mention event log changes + + new files: + examples/disorder-log + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + examples/Makefile.am python/disorder.py.in + + +2005-02-18 15:56:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-94 + + Summary: + event log interface + Revision: + disorder--mainline--0.1--patch-94 + + * lib/eventlog.c: new event log interface for sending messages to clients + that issue the 'log' command + * lib/queue.c: event log queue, moved, removed, recent-* messages + * progs/play.c: event log completed, scratched, failed, playing messages + * progs/disorder.c: mention event log in command help + * progs/server.c: use event log interface for 'log' command + + * lib/log.c: don't send log messages to clients; logging interface is + simplified slightly. + * progs/daemonize.c: following simplified logging interface + + * doc/disorder_protocol.5.in: document event log + + * lib/configuration.c: remove left-over debugging printf, oops! + * progs/server.c: set SO_REUSEADDR so we don't have to cool off a bit + before binding to a TCP socket again. + + new files: + lib/eventlog.c lib/eventlog.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + doc/disorder_protocol.5.in lib/Makefile.am lib/configuration.c + lib/log.c lib/log.h lib/queue.c progs/daemonize.c + progs/disorder.c progs/play.c progs/server.c + python/disorder.py.in + + +2005-02-17 00:02:42 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-93 + + Summary: + put README on the web + Revision: + disorder--mainline--0.1--patch-93 + + * scripts/dist: put README on the web + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/dist + + +2005-02-16 23:49:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-92 + + Summary: + update copyright dates for files changed in 2005 + Revision: + disorder--mainline--0.1--patch-92 + + * scripts/check: case independent check + + modified files: + ChangeLog.d/disorder--mainline--0.1 debian/copyright + lib/client.c lib/client.h lib/kvp.c lib/kvp.h lib/log.c + lib/log.h lib/table.h plugins/tracklength.c progs/Makefile.am + progs/cgi.h progs/cgimain.c progs/tracks.h scripts/check + templates/Makefile.am templates/choose.html + templates/playing.html templates/recent.html + templates/search.html + + +2005-02-16 23:40:00 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-91 + + Summary: + unicode search strings + Revision: + disorder--mainline--0.1--patch-91 + + * templates/search.html: use POST with multipart/form-data and an + explicit charset for the search form, rather than GET, so that we can + portably have non-ASCII characters in search terms. + back= URLs still use GET, but we can interpret query strings that + encode non-ASCII characters however we choose, and we choose UTF-8. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + templates/search.html + + +2005-02-16 23:13:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-90 + + Summary: + configurable scratch signal + Revision: + disorder--mainline--0.1--patch-90 + + * lib/configuration.c: new 'signal' configuration item controls the + signal that will be used to interrupt players. Default SIGINT. + * progs/play.c: interrupt tracks using configured signal + + * lib/table.h: document TABLE_FIND() macro + + * doc/disorder_config.5.in: document 'signal' + * doc/disorder.3: document scratching interface. + + I've only listed signals from glibc's <bits/signal.h>; more could easily + be added. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + doc/disorder_config.5.in lib/configuration.c + lib/configuration.h lib/table.h progs/play.c + + +2005-02-16 22:17:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-89 + + Summary: + make artist/album in playing/recent be links + Revision: + disorder--mainline--0.1--patch-89 + + * progs/dcgi.c: generalize @dirname@ and @basename@ + * templates/playing.html: make artist/album be links + * templates/recent.html: make artist/album be links + * doc/disorder_config.5.in: document extended @dirname@/@basename@ + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/playing.html + templates/recent.html + + +2005-02-16 21:32:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-88 + + Summary: + improved track selection extended to searching, debugged + Revision: + disorder--mainline--0.1--patch-88 + + The previous approach, of recording the directory to go back to, didn't + work for searching and didn't work properly where a regexp argument + reduced the set of tracks listed. The new approach is to specify the + exact URL to return to. + + * progs/dcgi.c: new 'back' CGI argument provides URL to redirect back to + after any operation except 'playing'. We use this to simply act_play(). + New @thisurl@ expansion gives the current page's URL, with a freshened + nonce. + + * templates/choose.html: use new 'back' behaviour; canonicalize name of + nonce arguments. + * templates/help.html: canonicalize name of nonce arguments. + * templates/search.html: use new 'back' behaviour as per choose.html + + * doc/disorder_config.5.in: document the above + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/choose.html + templates/help.html templates/search.html + + +2005-02-06 23:45:49 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-87 + + Summary: + typo + Revision: + disorder--mainline--0.1--patch-87 + + * doc/disorder_config.5.in: typo fix + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder_config.5.in + + +2005-02-06 23:44:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-86 + + Summary: + improved track picking interface + Revision: + disorder--mainline--0.1--patch-86 + + This change follows watching a user pick several tracks fromg a single + album without the benefit of an 'open in new tab' client command (the + client they were using had the feature but they didn't know to use it). + + The new behaviour is that when you pick a track to be played you stay on + the same screen, with an indicator appearing beside the track to show + that it is on the queue. + + The old behaviour is still available, by editing templates/choose.html. + + * lib/kvp.c: urlencodestring() provides a more convenient interface for + getting url-encoded strings + * progs/dcgi.c: action=play now takes a back=directory argument to + redirect back into action=choose with a chosen directory. Used to stay + on the same page when choosing multiple tracks. + New @trackstate@ action reports current track state as 'playing', + 'queued' or '' if neither. + * templates/choose.html: use the above to stay on the same page when + choosing tracks while also giving visual feedback + * doc/disorder_config.5.in: document the above changes + * CHANGES: mention recent changes + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in lib/kvp.c lib/kvp.h progs/dcgi.c + templates/choose.html + + +2005-02-06 20:39:47 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-85 + + Summary: + faster searching + Revision: + disorder--mainline--0.1--patch-85 + + * progs/tracks.c: track_search() retrieves word list for each track from + isearch database, which is rather fewer queries than reconstructing the + word list on the fly. + isearch_words() factors out common code for getting word lists from + isearch.db. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-02-06 18:37:32 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-84 + + Summary: + inverse search index + Revision: + disorder--mainline--0.1--patch-84 + + * progs/tracks.c: isearch.db is 'inverse' of search db, so we can keep + search db properly up to date. + DBCALL writes to debug output. + get/put write to debug output, report db name + opendb() centralizes database opening + track_getpart() wraps trackname_part(), doing database lookup too + * progs/server.c: use track_getpart() instead of manually checking + database and calling trackname_part(). + * lib/log.c: report file/line in debug output + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/configuration.c + lib/configuration.h lib/log.c lib/log.h progs/cgi.h + progs/server.c progs/tracks.c progs/tracks.h + + +2005-02-06 16:00:55 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-83 + + Summary: + internal documentation + Revision: + disorder--mainline--0.1--patch-83 + + * progs/tracks.c: describe current database tables + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-02-05 19:50:44 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-82 + + Summary: + tkdisorder improvements + Revision: + disorder--mainline--0.1--patch-82 + + * python/tkdisorder: use a canvas to display queue/recent listings, + showing artist, album and title. A bit slow. Could use timings, + submitters, etc too. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-02-05 18:18:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-81 + + Summary: + avoid accumulating overlarge recently played list + Revision: + disorder--mainline--0.1--patch-81 + + * lib/queue.c: recompute size of 'recent' list on loading it. Previously + it was not recounted but started again from 0 even if it wasn't empty; + so restarting the server effectively bumped the history variable + upwards by whatever the current size of the list was. + * {arch}/=tagging-method: ignore =obj directory + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/queue.c + {arch}/=tagging-method + + +2005-02-05 17:20:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-80 + + Summary: + make tkdisorder use 'part' command + Revision: + disorder--mainline--0.1--patch-80 + + * python/tkdisorder: replace local track parsing with calls to + disorder.client.part() + * python/disorder.py.in: DISORDER_PYTHON_DEBUG can be used to turn on + disorder.py debugging + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + python/tkdisorder + + +2005-02-05 15:33:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-79 + + Summary: + build fixes + Revision: + disorder--mainline--0.1--patch-79 + + * lib/types.h: work around broken apple header file by undefining PRI?MAX + values that GNU C rejects in pedantic mode. + * plugins/tracklength.c: cast strncmp() args to reliably match declaration + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/types.h + plugins/tracklength.c + + +2005-02-05 15:12:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-78 + + Summary: + fix trackname parsing for scratches + Revision: + disorder--mainline--0.1--patch-78 + + * progs/server.c: don't insist that the track exists, as this can break + scratches which aren't required to 'exist' (in this sense) + * progs/trackname.c: new internal program exposes trackname_part() for + testing purposes. + + new files: + progs/trackname.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/Makefile.am + progs/server.c + + +2005-02-05 11:54:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-77 + + Summary: + move trackname parsing into server + Revision: + disorder--mainline--0.1--patch-77 + + Moving the trackname parsing into the server allows every client to get + the same logic without having to locally reimplement it. + + * debian/disorder.config: add namepart directives + * examples/config.sample.in: add namepart directives + * doc/disorder_config.5.in: document namepart directive; remove + trackname-part references. + + * lib/trackname.c: trackname_part() function does regexp substitution + based on namepart directives. + * lib/configuration.c: namepart directive + * lib/regsub.c: moved to lib + + * progs/server.c: implement 'part' command + * lib/client.c: add disorder_part() function; report transmitted commands + in debug output. + * doc/disorder_protocol.5.in: document 'part' command + + * progs/disorder.c: expose 'part' command to command line + * doc/disorder.1.in: document 'part' command + + * python/disorder.py.in: expose 'part' command to python + + * progs/cgi.c: abolish trackname-part + * progs/cgimain.c: environment variable DISORDER_DEBUG enables debugging. + * progs/dcgi.c: use disorder_part() to get track name parts + * templates/options: options.trackname is gone + * templates/options.trackname: gone + + * README.upgrades: new upgrading instructions mention this change. + * README: mention README.upgrades + + new files: + README.upgrades lib/trackname.c lib/trackname.h + + removed files: + templates/options.trackname + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 Makefile.am README + debian/disorder.config doc/disorder.1.in + doc/disorder_config.5.in doc/disorder_protocol.5.in + examples/config.sample.in lib/Makefile.am lib/client.c + lib/client.h lib/configuration.c lib/configuration.h + progs/Makefile.am progs/cgi.c progs/cgimain.c progs/dcgi.c + progs/disorder.c progs/server.c python/disorder.py.in + templates/Makefile.am templates/options + + renamed files: + progs/regsub.c + ==> lib/regsub.c + progs/regsub.h + ==> lib/regsub.h + + +2005-02-04 13:29:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-76 + + Summary: + preparations for the future + Revision: + disorder--mainline--0.1--patch-76 + + Prepare for move of DisOrder website to www.greenend.org/rjk/disorder. + Version number to 1.1+dev. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/copyright scripts/check scripts/dist + templates/about.html templates/credits.html + + +2005-02-03 22:26:13 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-75 + + Summary: + better ChangeLog.d handling + Revision: + disorder--mainline--0.1--patch-75 + + Distribute ChangeLog.d/* by mentioning it in EXTRA_DIST rather than + giving it its own Makefile. + + removed files: + ChangeLog.d/Makefile.am + + modified files: + ChangeLog.d/disorder--mainline--0.1 Makefile.am configure.ac + + +2005-02-03 17:07:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-74 + + Summary: + automake bodge + Revision: + disorder--mainline--0.1--patch-74 + + * prepare: make sure Automake adds INSTALL, but doesn't fall over because + of missing ChangeLog. + + modified files: + ChangeLog.d/disorder--mainline--0.1 prepare + + +2005-02-03 00:02:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-73 + + Summary: + fix up dist script + Revision: + disorder--mainline--0.1--patch-73 + + * scripts/dist: install change history files + + + + modified files: + ChangeLog.d/disorder--mainline--0.1 scripts/dist + + +2005-02-02 23:50:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-72 + + Summary: + release 1.1 + Revision: + disorder--mainline--0.1--patch-72 + + * configure.ac, CHANGES, debian/changelog.Debian: version number to 1.1 + * debian/control: Build-Depends on libdb4.3 then libdb4.2 + * scripts/makedeb: bzip2 + * scripts/check: script to scan for missing copyright dates + * lib/Makefile.am: tie libdisorder shared library version to disorder + version to avoid appearing to promise anything about ABI stability. + + Everything else: missing copyright dates. + + new files: + scripts/check + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 configure.ac + debian/changelog.Debian debian/config debian/control + doc/Makefile.am doc/disorder.3 doc/disorderd.8.in + lib/Makefile.am lib/charset.c lib/charset.h + lib/configuration.c lib/configuration.h lib/disorder.h + lib/hex.c lib/hex.h lib/plugin.c lib/plugin.h lib/queue.h + lib/types.h lib/utf8.h plugins/notify.c plugins/pick.c + progs/cgi.c progs/dcgi.c progs/play.c progs/server.c + progs/state.c progs/tracks.c python/Makefile.am + scripts/Makefile.am scripts/inst scripts/makedeb + scripts/sedfiles.make templates/credits.html + templates/help.html templates/prefs.html + + +2005-02-01 20:09:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-71 + + Summary: + more portability fixes + Revision: + disorder--mainline--0.1--patch-71 + + The aim is to build with CC=gcc -std=c89 -pedantic, though sadly it's not + practical to add -Werror in there as well. + + * configure.ac: turn on -Werror (if that's being used) when checking for + long long, since that's how it'll be used later on. In fact this only + rejects long long for compilers that warn but accept it, but it's + useful to be able to build without long long being available. + However, we disable -Werror if we can't convert function pointers to + object pointers without warning: DisOrder without dlsym() is + essentially hopeless. + * lib/basen.c: rename div to divide (libc conflict) + * lib/configuration.c: variable initializers are not allowed in C89 + * lib/types.h: include <stdlib.h> before redefing atoll() etc + Define PRIxMAX + * progs/server.c: use uintmax_t rather than explicit unsigned long long + to report times in client log messages. + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac lib/basen.c + lib/configuration.c lib/types.h progs/server.c + + +2005-01-30 13:08:35 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-70 + + Summary: + cope with broken darwin headers + Revision: + disorder--mainline--0.1--patch-70 + + 'gcc-3.3 -std=c99' on Darwin turns declaration of strtoll and atoll off, + even though they are in the C99 library clause. + + * plugins/pick.c: cope with undeclared atoll + * lib/types.h: cope with undeclared strtoll/atoll + * configure.ac: check whether strtoll/atoll are declared; include + <sys/types.h> for ssize_t + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac lib/types.h + plugins/pick.c + + +2005-01-29 19:18:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-69 + + Summary: + libdb version checking makes more sense + Revision: + disorder--mainline--0.1--patch-69 + + * progs/tracks.c: reverse sense of libdb 4.2/4.3 tests; now DB42 means + just libdb 4.2, rather than DB43 meaning anything from 4.3 onwards. + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 23:49:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-68 + + Summary: + fix botched libdb upgrade + Revision: + disorder--mainline--0.1--patch-68 + + * progs/tracks.c: 4.2 code was bust, restore + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 23:47:29 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-67 + + Summary: + improved distribution script + Revision: + disorder--mainline--0.1--patch-67 + + * scripts/webman: dead + * scripts/dist: new distribution script installs tarball and web man + pages in right place + + new files: + scripts/dist + + removed files: + scripts/webman + + modified files: + ChangeLog.d/disorder--mainline--0.1 + + +2005-01-27 23:37:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-66 + + Summary: + abolish backward compatibility code + Revision: + disorder--mainline--0.1--patch-66 + + * progs/tracks.c: remove upgrade code for ancient database location and + content + * README: db 4.3 and gcc 3.4 work + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + progs/tracks.c + + +2005-01-27 23:20:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-65 + + Summary: + db version 4.3 support + Revision: + disorder--mainline--0.1--patch-65 + + We support 4.2 and 4.3 for now. 4.2 support can be expected to go away + at some point. + + * progs/tracks.c: db4.3 support + + modified files: + ChangeLog.d/disorder--mainline--0.1 progs/tracks.c + + +2005-01-27 22:55:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-64 + + Summary: + spelling corrections + Revision: + disorder--mainline--0.1--patch-64 + + * python/disorder.py.in: minor typo fixes + * doc/disorder_config.5.in: typo fix + * progs/disorder.c: typo fix + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/disorder.c + python/disorder.py.in + + +2005-01-25 23:52:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-63 + + Summary: + C dialect strictness + Revision: + disorder--mainline--0.1--patch-63 + + * configure.ac: move _GNU_SOURCE define above library checks, since + <db.h> gets on badly with -std=c89 otherwise. + * lib/configuration.h: 'restrict' is a C99 keyword, don't use it as a + structure member name! + * lib/configuration.c: 'restrict' option field is now 'restrictions' + * progs/server.c: 'restrict' option field is now 'restrictions' + * progs/dcgi.c: 'restrict' option field is now 'restrictions' + * progs/cgi.c: remove extraneous semicolon + * lib/plugin.c: cast to keep gcc --std=c99 -pedantic happy + + modified files: + ChangeLog.d/disorder--mainline--0.1 configure.ac + lib/configuration.c lib/configuration.h lib/plugin.c + progs/cgi.c progs/dcgi.c progs/server.c + + +2005-01-25 18:35:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-62 + + Summary: + completion.bash + Revision: + disorder--mainline--0.1--patch-62 + + * scripts/completion.bash: renamed from disorder-bash-completion. + 'disorder' will already be in the path since it is installed to + pkgdatadir. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 README + scripts/Makefile.am + + renamed files: + scripts/disorder-bash-completion + ==> scripts/completion.bash + + +2005-01-24 19:49:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-61 + + Summary: + bash completion + Revision: + disorder--mainline--0.1--patch-61 + + * scripts/disorder-bash-completion: bash completion for disorder and + disorderd + + new files: + scripts/disorder-bash-completion + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + scripts/Makefile.am + + +2005-01-24 01:47:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-60 + + Summary: + more UTF-8 testing stuff + Revision: + disorder--mainline--0.1--patch-60 + + * lib/charset.c: ucs4cmp, used in testing + * lib/test.c: fix broken UTF-8 testing + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/charset.h lib/test.c + + +2005-01-24 01:42:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-59 + + Summary: + more tests + Revision: + disorder--mainline--0.1--patch-59 + + * lib/charset.c: new ucs42utf8 function, currently only used by tests + * lib/test.c: test hex codec; ucs42utf8; casefold; check the basic + character set looks like ASCII. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/charset.c + lib/charset.h lib/test.c + + +2005-01-23 19:17:20 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-58 + + Summary: + eliminate bogus diagnostics + Revision: + disorder--mainline--0.1--patch-58 + + * lib/hex.c: prevoius change to unhexdigit() produced an error on every + call. Oops. + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/hex.c + + +2005-01-23 19:08:58 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-57 + + Summary: + base64 corrections + Revision: + disorder--mainline--0.1--patch-57 + + * lib/test.c: MIME base64 tests + * lib/mime.c: corrected base64 parsing + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mime.c lib/test.c + + +2005-01-23 18:18:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-56 + + Summary: + quoted-printable tests and fixes + Revision: + disorder--mainline--0.1--patch-56 + + * lib/test.c: a few MIME decoding tests + * lib/hex.c: quiet hex digit conversion + * lib/mime.c: corrected decoding of quoted-printable; + eliminate bogus diagnostics + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/hex.c + lib/hex.h lib/mime.c lib/test.c + + +2005-01-23 17:49:14 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-55 + + Summary: + tests, further UTF-8 improvements + Revision: + disorder--mainline--0.1--patch-55 + + * lib/test.c: tests for UTF-8 decoding and validation + * lib/utf8.h: corrected again, sigh + * lib/Makefile.am: run library tests at build time (not that there are + many of them) + + new files: + lib/test.c + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am lib/utf8.h + + +2005-01-19 23:09:55 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-54 + + Summary: + finally correct UTF-8 checking + Revision: + disorder--mainline--0.1--patch-54 + + * lib/utf8.h: check for invalid UTF-8 sequences more strictly + parse [U+10000,U+10FFFF] correctly(!) + * lib/utf8.c: validutf8() function checks a string + * progs/cgi.c: use validutf8() instead of wrongly(!) checking manually. + + new files: + lib/utf8.c + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/utf8.h progs/cgi.c + + +2005-01-18 20:49:23 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-53 + + Summary: + stricter CGI arg checking + Revision: + disorder--mainline--0.1--patch-53 + + * progs/cgi.c: UTF-8 checking of CGI arguments + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 progs/cgi.c + + +2005-01-17 23:39:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-52 + + Summary: + make prefs.html self-consistent + Revision: + disorder--mainline--0.1--patch-52 + + * templates/prefs.html: even/odd styles count from 0, not 1 l-) + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/prefs.html + + +2005-01-17 23:26:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-51 + + Summary: + base64/qp support in MIME parsing + Revision: + disorder--mainline--0.1--patch-51 + + * lib/mime.c: base64 and qp support for MIME body parts. mime_header() + becomes mime_parse() and promises do deal with + content-transfer-encoding for you. + * progs/cgi.c: track mime.c changes + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/mime.c lib/mime.h + progs/cgi.c + + +2005-01-17 22:31:40 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-50 + + Summary: + use multipart/form-data for preference submissions + Revision: + disorder--mainline--0.1--patch-50 + + * progs/cgi.c: accept multipart/form-data POST data. Note that the + character set is assumed to be UTF-8. + * lib/mime.c,: enough MIME support for multipart/form-data support. We + don't do QP or BASE64 yet(!) + * templates/prefs.html: use multi-part/form-data submission for + preference updates. + + new files: + lib/mime.c lib/mime.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + progs/cgi.c templates/prefs.html + + +2005-01-16 19:57:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-49 + + Summary: + python administrivia + Revision: + disorder--mainline--0.1--patch-49 + + * doc/Makefile.am: ship tkdisorder.1 + * doc/tkdisorder.1: refer to 'pydoc disorder' + * doc/disorder.1.in: refer to 'pydoc disorder' + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + doc/disorder.1.in doc/tkdisorder.1 + + +2005-01-16 16:25:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-48 + + Summary: + tkdisorder administrivia + Revision: + disorder--mainline--0.1--patch-48 + + * python/disorder.py.in: copyright message, version number + * python/tkdisorder: copyright message, display version + * scripts/sedfiles.make: version number substitution + * doc/tkdisorder.1: document tkdisorder + + new files: + doc/tkdisorder.1 + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/Makefile.am + python/Makefile.am python/disorder.py.in python/tkdisorder + scripts/sedfiles.make + + +2005-01-16 15:20:43 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-47 + + Summary: + more user-friendly "cooked" preferences interface + Revision: + disorder--mainline--0.1--patch-47 + + * templates/prefs.html: cooked preferences; wider input boxes + * templates/options.labels: new labels for cooked prefs + * templates/help.html: describe cooked preferences interface + * progs/dcgi.c: prefs is now a proper action, @prefs@ just iterates over + the set preferences. @pref@ added. @part@ can now take a track name. + * doc/disorder_config.5.in: document new/modified expansions and action + * CHANGES: mention cooked prefs + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in progs/dcgi.c templates/help.html + templates/options.labels templates/prefs.html + + +2005-01-12 23:24:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-46 + + Summary: + preserve recently played list across server restarts + Revision: + disorder--mainline--0.1--patch-46 + + * lib/queue.c: save/restore recently-played list + * progs/state.c: restore recently-played list at startup + * progs/play.c: save recently-played list after modifying it + * doc/disorderd.8.in: mention new file + * doc/disorder_config.5.in: recent list no longer nuked at startup + * CHANGES: mention the change + + A better approach might be for the mutating queue_*() functions to set a + flag, which is then queried each time round the event loop to determine + when a save is required. + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 + doc/disorder_config.5.in doc/disorderd.8.in lib/queue.c + lib/queue.h progs/disorder.c progs/play.c progs/state.c + + +2005-01-04 19:56:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-45 + + Summary: + further tkdisorder improvements + Revision: + disorder--mainline--0.1--patch-45 + + * python/tkdisorder: break up QueueWidget into TrackListWidget and + {Queue,Recent}Widget, replacing callback with subclassing. + Add title to main window. + Be more careful about using clients from the right thread. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 16:30:02 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-44 + + Summary: + 'recent' button + Revision: + disorder--mainline--0.1--patch-44 + + * python/tkdisorder: 'recent' button pops up a window with the last N + tracks. Generalized QueueWidget a bit. MonitorStateThread can have + widgets added to and removed from its notification list. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 14:39:10 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-43 + + Summary: + scratch button for tkdisorder + Revision: + disorder--mainline--0.1--patch-43 + + * python/tkdisorder: Quit and Scratch buttons + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/tkdisorder + + +2005-01-03 14:21:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-42 + + Summary: + more efficient tkdisorder + Revision: + disorder--mainline--0.1--patch-42 + + We could make it more efficient yet by ignoring irrelevant log messages. + + * python/disorder.py.in: warning about disorder.log() return value + * python/tkdisorder: Use disorder.log() to watch for server state changes + rather than plling. Also separate clients for separate threads rather + than locking a single client. + + modified files: + ChangeLog.d/disorder--mainline--0.1 python/disorder.py.in + python/tkdisorder + + +2005-01-03 13:51:01 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-41 + + Summary: + notifications and logs for queue and playing state changes + Revision: + disorder--mainline--0.1--patch-41 + + * lib/disorder.h: new notify plugin calls report when a track is moved or + removed in the queue + * doc/disorder.3: document new notify calls + * lib/plugin.c: stubs for new notify calls + * lib/plugin.c: stubs for new notify calls + * plugins/notify.c: empty implementations of new calls + + * lib/queue.c: call new notify plugin calls + log queue changes + * progs/play.c: log playing, scratchin and completion of tracks + unconditionally log playing status + + modified files: + ChangeLog.d/disorder--mainline--0.1 doc/disorder.3 + lib/disorder.h lib/plugin.c lib/plugin.h lib/queue.c + lib/queue.h plugins/notify.c progs/play.c progs/server.c + + +2005-01-03 00:26:18 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-40 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-40 + + More copyright dates. + + modified files: + ChangeLog.d/disorder--mainline--0.1 templates/about.html + + +2005-01-03 00:25:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-39 + + Summary: + update copyright dates + Revision: + disorder--mainline--0.1--patch-39 + + It's 2005 now. + + modified files: + ChangeLog.d/Makefile.am ChangeLog.d/disorder--mainline--0.1 + Makefile.am README configure.ac debian/rules.m4 + doc/disorder.1.in doc/disorder_config.5.in + doc/disorder_protocol.5.in lib/basen.c lib/basen.h lib/queue.c + prepare progs/disorder.c + + +2005-01-03 00:21:16 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-38 + + Summary: + Typo fixes + Revision: + disorder--mainline--0.1--patch-38 + + Trivial corrections to various bits of documentation. + + modified files: + ChangeLog.d/disorder--mainline--0.1 README doc/disorder.1.in + doc/disorder_config.5.in doc/disorder_protocol.5.in + + +2005-01-02 23:56:24 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-37 + + Summary: + automake --foreign + Revision: + disorder--mainline--0.1--patch-37 + + Reduce Automake strictness and delete quietening files. + + removed files: + .arch-ids/ChangeLog.id AUTHORS ChangeLog NEWS + + modified files: + ChangeLog.d/disorder--mainline--0.1 prepare + + +2005-01-02 21:31:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-36 + + Summary: + documentation trivia + Revision: + disorder--mainline--0.1--patch-36 + + * CHANGES: describe recent changes + * doc/disorder.1.in: unify remove/move language and caveat + + modified files: + CHANGES ChangeLog.d/disorder--mainline--0.1 doc/disorder.1.in + + +2005-01-02 21:16:36 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-35 + + Summary: + track ID improvements + Revision: + disorder--mainline--0.1--patch-35 + + * progs/disorder.c: report track id in output + * lib/basen.c: arbitrary base printer + * lib/queue.c: queue IDs are now base-62 not hex, making them much + shorter. The serial number is made the most significant word to + increase diversity of adjacently generated IDs. + + new files: + ChangeLog lib/basen.c lib/basen.h + + modified files: + ChangeLog.d/disorder--mainline--0.1 lib/Makefile.am + lib/queue.c progs/disorder.c + + +2005-01-02 20:42:37 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-34 + + Summary: + ChangeLog.d directory + Revision: + disorder--mainline--0.1--patch-34 + + Move changelogs into ChangeLog.d directory. + + * CHANGES: note movement of ChangeLogs + + new files: + .arch-ids/ChangeLog.id ChangeLog ChangeLog.d/.arch-ids/=id + ChangeLog.d/Makefile.am + + modified files: + CHANGES ChangeLog Makefile.am configure.ac debian/rules.m4 + + renamed files: + ChangeLog + ==> ChangeLog.d/disorder--mainline--0.1 + ChangeLog.cvs + ==> ChangeLog.d/cvs--ChangeLog + + new directories: + ChangeLog.d ChangeLog.d/.arch-ids + + +2005-01-02 20:27:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-33 + + Summary: + expose track movement to the command line interface + Revision: + disorder--mainline--0.1--patch-33 + + * progs/disorder.c: implement 'move' command + * doc/disorder.1.in: document 'move' command + + modified files: + ChangeLog doc/disorder.1.in progs/disorder.c + + +2004-12-30 16:21:07 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-32 + + Summary: + sort 'The Beatles' under B not T + Revision: + disorder--mainline--0.1--patch-32 + + * progs/dcgi.c: filter according to regexp in the web interface rather + than in the server. This is slower but can actually be made correct. + This might be a problem for sites with very large numbers of (in + particular) top-level directories. + part() is renamed tracknamepart() as a no-longer-required build fix, + but it's also a bit clearer what it does, so I left it in. + + * BUGS: remove fixed bugs. + + modified files: + BUGS ChangeLog progs/dcgi.c + + +2004-12-30 14:42:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-31 + + Summary: + strip out libdb backwards compatibility cruft + Revision: + disorder--mainline--0.1--patch-31 + + Anything before libdb 4.2 is now regarded obsolete, and thus the grotty + configure and preprocessor logic to support older versions is removed. + + * progs/dbfixup.h: removed + * progs/tracks.c: strip out db version fixup hackery. Now we only + support libdb 4.2 (and possibly later). + * README: note that libdb 4.1 and earlier won't work + * progs/Makefile.am: dbfixup.h is gone + * configure.ac: remove obsolete libdb feature tests + + + removed files: + progs/dbfixup.h + + modified files: + ChangeLog README configure.ac progs/Makefile.am progs/tracks.c + + +2004-12-30 14:18:19 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-30 + + Summary: + documentation fixups + Revision: + disorder--mainline--0.1--patch-30 + + * scripts/webman: html man pages are in doc directory now + * scripts/htmlman: fix title of unmunged man pages + * configure.ac: version number to 1.0+dev + + + modified files: + ChangeLog configure.ac scripts/htmlman scripts/webman + + +2004-12-30 12:55:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-29 + + Summary: + release 1.0 + Revision: + disorder--mainline--0.1--patch-29 + + * README: list development-time dependencies + * scripts/Makefile.am: ship sedfiles.make + * debian/rules.m4: disorder.init and disorder.cgi moved + * debian/autorules.m4: debian.changelog moved + + + modified files: + CHANGES ChangeLog README configure.ac debian/autorules.m4 + debian/changelog.Debian debian/rules.m4 scripts/Makefile.am + scripts/makedeb + + +2004-12-05 11:21:09 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-28 + + Summary: + more post-split tidy-up + Revision: + disorder--mainline--0.1--patch-28 + + * scripts/inst: disorder.cgi is in progs/ now + + modified files: + ChangeLog scripts/inst + + +2004-12-04 17:36:25 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-27 + + Summary: + more most-split tidy-ups + Revision: + disorder--mainline--0.1--patch-27 + + * debian/changelog.Debian: a saner name + + modified files: + ChangeLog debian/Makefile.am debian/autorules.m4 + + renamed files: + debian/ChangeLog + ==> debian/changelog.Debian + + +2004-12-04 16:18:26 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-26 + + Summary: + make python optional + Revision: + disorder--mainline--0.1--patch-26 + + Python support should be optional. Implement this by suppressing + recursion into python/ if no Python interpreter is found. Not tested on + a system without Python, please report success/failure in such cases. + + modified files: + ChangeLog Makefile.am README configure.ac + + +2004-12-04 16:06:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-25 + + Summary: + fix up build instructions + Revision: + disorder--mainline--0.1--patch-25 + + * BUGS: note that darwin can't do volume control + * Makefile.am: ship README.darwin + * README: link to mailing lists + catch up with directory split + * README.darwin: caveats + + modified files: + BUGS CHANGES ChangeLog Makefile.am README README.darwin + + renamed files: + inst + ==> scripts/inst + webman + ==> scripts/webman + + +2004-12-04 15:26:48 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-24 + + Summary: + post-directory-split tidy-ups + Revision: + disorder--mainline--0.1--patch-24 + + * scripts/sedfiles.make: de-dupe seddery into a single file + * debian/autorules.m4: catch up with filename case change + (workaround for automake wackiness) + + new files: + scripts/sedfiles.make + + modified files: + ChangeLog debian/autorules.m4 doc/Makefile.am + examples/Makefile.am python/Makefile.am + + +2004-12-04 13:50:38 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-23 + + Summary: + split into subdirectories + Revision: + disorder--mainline--0.1--patch-23 + + Almost everything moves out of the root. The new directory structure is: + templates/ web interface template files + scripts/ scripts used in the build process + lib/ libdisorder + progs/ the server and the front-end programs + doc/ man pages + plugins/ the standard plugins + debian/ debian build files + sounds/ standard scratch sounds + python/ python support code + examples/ config/init examples, etc + config.aux/ autotools auxilary files + + new files: + config.aux/.arch-ids/=id doc/.arch-ids/=id doc/Makefile.am + examples/.arch-ids/=id examples/Makefile.am lib/.arch-ids/=id + lib/Makefile.am progs/.arch-ids/=id progs/Makefile.am + python/.arch-ids/=id python/Makefile.am scripts/.arch-ids/=id + scripts/Makefile.am + + modified files: + ChangeLog Makefile.am configure.ac debian/Makefile.am + plugins/Makefile.am prepare + + renamed files: + addr.c + ==> lib/addr.c + addr.h + ==> lib/addr.h + api-client.c + ==> progs/api-client.c + api-client.h + ==> progs/api-client.h + api-server.c + ==> progs/api-server.c + api.c + ==> progs/api.c + asprintf.c + ==> lib/asprintf.c + authhash.c + ==> lib/authhash.c + authhash.h + ==> lib/authhash.h + casefold.h + ==> lib/casefold.h + cgi.c + ==> progs/cgi.c + cgi.h + ==> progs/cgi.h + cgimain.c + ==> progs/cgimain.c + charset.c + ==> lib/charset.c + charset.h + ==> lib/charset.h + client.c + ==> lib/client.c + client.h + ==> lib/client.h + config.sample.in + ==> examples/config.sample.in + configuration.c + ==> lib/configuration.c + configuration.h + ==> lib/configuration.h + daemonize.c + ==> progs/daemonize.c + daemonize.h + ==> progs/daemonize.h + dbfixup.h + ==> progs/dbfixup.h + dcgi.c + ==> progs/dcgi.c + dcgi.h + ==> progs/dcgi.h + debian/changelog + ==> debian/ChangeLog + disorder-dump.8.in + ==> doc/disorder-dump.8.in + disorder.1.in + ==> doc/disorder.1.in + disorder.3 + ==> doc/disorder.3 + disorder.c + ==> progs/disorder.c + disorder.h + ==> lib/disorder.h + disorder.init.in + ==> examples/disorder.init.in + disorder.py.in + ==> python/disorder.py.in + disorder_config.5.in + ==> doc/disorder_config.5.in + disorder_protocol.5.in + ==> doc/disorder_protocol.5.in + disorderd.8.in + ==> doc/disorderd.8.in + disorderd.c + ==> progs/disorderd.c + dump.c + ==> progs/dump.c + event.c + ==> lib/event.c + event.h + ==> lib/event.h + fprintf.c + ==> lib/fprintf.c + hex.c + ==> lib/hex.c + hex.h + ==> lib/hex.h + htmlman + ==> scripts/htmlman + inputline.c + ==> lib/inputline.c + inputline.h + ==> lib/inputline.h + kvp.c + ==> lib/kvp.c + kvp.h + ==> lib/kvp.h + log-impl.h + ==> lib/log-impl.h + log.c + ==> lib/log.c + log.h + ==> lib/log.h + makedeb + ==> scripts/makedeb + mem-impl.h + ==> lib/mem-impl.h + mem.c + ==> lib/mem.c + mem.h + ==> lib/mem.h + mixer.c + ==> lib/mixer.c + mixer.h + ==> lib/mixer.h + play.c + ==> progs/play.c + play.h + ==> progs/play.h + plugin.c + ==> lib/plugin.c + plugin.h + ==> lib/plugin.h + printf.c + ==> lib/printf.c + printf.h + ==> lib/printf.h + queue.c + ==> lib/queue.c + queue.h + ==> lib/queue.h + regsub.c + ==> progs/regsub.c + regsub.h + ==> progs/regsub.h + rescan.c + ==> progs/rescan.c + server.c + ==> progs/server.c + server.h + ==> progs/server.h + sink.c + ==> lib/sink.c + sink.h + ==> lib/sink.h + snprintf.c + ==> lib/snprintf.c + split.c + ==> lib/split.c + split.h + ==> lib/split.h + state.c + ==> progs/state.c + state.h + ==> progs/state.h + syscalls.c + ==> lib/syscalls.c + syscalls.h + ==> lib/syscalls.h + table.c + ==> lib/table.c + table.h + ==> lib/table.h + tkdisorder + ==> python/tkdisorder + tracks.c + ==> progs/tracks.c + tracks.h + ==> progs/tracks.h + types.h + ==> lib/types.h + unicodegc.h + ==> lib/unicodegc.h + utf8.h + ==> lib/utf8.h + vacopy.h + ==> lib/vacopy.h + vector.c + ==> lib/vector.c + vector.h + ==> lib/vector.h + words.c + ==> lib/words.c + words.h + ==> lib/words.h + wstat.c + ==> lib/wstat.c + wstat.h + ==> lib/wstat.h + + new directories: + config.aux config.aux/.arch-ids doc doc/.arch-ids examples + examples/.arch-ids lib lib/.arch-ids progs progs/.arch-ids + python python/.arch-ids scripts scripts/.arch-ids + + +2004-12-03 16:18:53 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-22 + + Summary: + missing from previous change + Revision: + disorder--mainline--0.1--patch-22 + + * dump.c: missing includes + * disorder.c: missing includes + + modified files: + ChangeLog disorder.c dump.c + + +2004-12-03 16:09:46 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-21 + + Summary: + fix garbage collection problems under Darwin + Revision: + disorder--mainline--0.1--patch-21 + + Call GC_init() at the start of each program. This is necesary on Darwin and + leaving it out led to crashes. + + * play.c: more idiomatic xmalloc + * README.darwin: where to get ogg123/mpg321. + + modified files: + ChangeLog README.darwin cgimain.c disorder.c disorderd.c + dump.c mem.c mem.h play.c + + +2004-12-03 13:15:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-20 + + Summary: + first pass at Darwin (Mac OS X) port + Revision: + disorder--mainline--0.1--patch-20 + + The code now builds, runs, and plays music (albeit rather tinnily on my + laptop's speakers), but has a tendency to crash e.g. if you scratch three + or four tracks in a row. + + Code changes: + + * asprintf.c: error-checking wrapper for byte_asprintf + * event.c: EPROTO might not exist + * mixer.c: don't know how to set the volume if we don't have + <sys/soundcard.h> + * printf.c: support 'q' length modifier as a synonym for 'll' + * server.c: Darwin's getpeername() doesn't reliably fill in sa_family, + so remember the protocol family ourselves. + * tracks.c: DBT.size is not size_t + pass length back from track_states() correctly + + Build system changes: + + * prepare: look in /sw for the benefit of finkified Darwin. + * Makefile.am: Abandoned the attempt to restrict exported symbols; it is + impractical to do portably. + * configure.ac: Use RJK_CHECK_LIB for iconv as AC_CHECK_LIB is not up to + the job. + * confgure.ac: If fdatasync() is not available use fsync() instead. + * Makefile.am: Only use getopt* sources if necessary, and disable -Werror + in that case (since the code doesn't compile cleanly + enough) + + new files: + README.darwin + + removed files: + disorder.vs + + modified files: + ChangeLog Makefile.am README addr.c asprintf.c cgi.c cgimain.c + client.c configuration.c configure.ac dcgi.c event.c mixer.c + plugin.c prepare printf.c printf.h queue.c server.c + sounds/slap.ogg state.c tracks.c wstat.c + + +2004-12-01 23:51:21 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-19 + + Summary: + DisOrder 0.13 + Revision: + disorder--mainline--0.1--patch-19 + + New release. + + modified files: + CHANGES ChangeLog configure.ac debian/changelog + + +2004-12-01 23:41:59 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-18 + + Summary: + fix crash on queue_find() of non-existent tracks + Revision: + disorder--mainline--0.1--patch-18 + + * queue.c: queue_find() was returning &qhead on error, which seriously + confuses its callers; for instance it could lead to the 'fake' + root node in the queue being removed, with hilarious + consequences when the queue was written back out. + Changed queue_find() to correctly return 0 on error. + + modified files: + ChangeLog queue.c + + +2004-10-31 13:56:12 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-17 + + Summary: + change version to 0.12+dev + Revision: + disorder--mainline--0.1--patch-17 + + 0.12 branch is disorder--release-0-12--0.1. + + modified files: + ChangeLog configure.ac + + +2004-10-31 13:50:51 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-16 + + Summary: + delete non-working configs/ directory + Revision: + disorder--mainline--0.1--patch-16 + + + removed files: + configs/.arch-ids/=id configs/disorder-0.12.arch + configs/disorder.arch + + modified files: + ChangeLog + + removed directories: + configs configs/.arch-ids + + +2004-10-31 13:34:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-15 + + Summary: + config for release 0.12 + Revision: + disorder--mainline--0.1--patch-15 + + Add configs for + disorder main development line + disorder-0.12 release 0.12 + + new files: + configs/.arch-ids/=id configs/disorder-0.12.arch + configs/disorder.arch + + modified files: + ChangeLog + + new directories: + configs configs/.arch-ids + + +2004-10-30 18:52:50 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-14 + + Summary: + release 0.12 + Revision: + disorder--mainline--0.1--patch-14 + + ChangeLog is now an arch changelog; old changes can be found in + ChangeLog.cvs. + + Release 0.12. + + new files: + ChangeLog + + modified files: + CHANGES Makefile.am configure.ac debian/changelog + debian/rules.m4 + + renamed files: + ChangeLog + ==> ChangeLog.cvs + + +2004-10-30 18:37:34 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-13 + + Summary: + tidy up disorder.py + Revision: + disorder--mainline--0.1--patch-13 + + Remove a bogus debugging print + + Add an example to the disorder.log() documentation + + modified files: + disorder.py.in + + +2004-10-30 18:33:08 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-12 + + Summary: + log command could crash server + Revision: + disorder--mainline--0.1--patch-12 + + Log output to a conn was never stopped even when the client no longer + wanted it. So not only would bogus log messages be sent if the conn was + kept open, once it was closed there would be a crash. + + Fixed by storing the log output in the conn and closing it at the right + point. + + modified files: + CHANGES server.c + + +2004-10-30 18:26:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-11 + + Summary: + support 'log' in python client + Revision: + disorder--mainline--0.1--patch-11 + + New disorder.log method supports log command. + + modified files: + disorder.py.in + + +2004-10-29 19:50:39 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-10 + + Summary: + more detailed dependencies table + Revision: + disorder--mainline--0.1--patch-10 + + List version numbers for build dependencies. + + modified files: + README + + +2004-10-29 19:39:33 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-9 + + Summary: + deprecate libdb before 4.2 + Revision: + disorder--mainline--0.1--patch-9 + + No code changes, but no promises regarding older libdb versions. + + modified files: + README + + +2004-10-29 19:38:04 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-8 + + Summary: + fix debian build rules + Revision: + disorder--mainline--0.1--patch-8 + + Cope with debian/rules being made outside ${srcdir}. + + modified files: + debian/Makefile.am debian/autorules.m4 + + +2004-10-29 15:15:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-7 + + Summary: + enable/disable from web templates + Revision: + disorder--mainline--0.1--patch-7 + + Add enable, disable, disable-now actions and @enabled@ expansion, and + documented them. The playing.html has an example usage in a comment, not + enabled because we find {enable,disable}-random much more useful in + practice. + + modified files: + dcgi.c disorder_config.5.in templates/options.labels + templates/playing.html + + +2004-10-28 23:05:52 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-6 + + Summary: + typo fix in error message + Revision: + disorder--mainline--0.1--patch-6 + + + modified files: + dcgi.c + + +2004-10-28 23:04:31 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-5 + + Summary: + add missing documentation + Revision: + disorder--mainline--0.1--patch-5 + + Describe various missing labels and the action= values. + + modified files: + disorder_config.5.in + + +2004-10-28 22:52:15 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-4 + + Summary: + add random play enable/disable buttons + Revision: + disorder--mainline--0.1--patch-4 + + @random-enabled@ expansion reports current state as a boolean + + random-enable and random-disable actions do the obvious thing + + New labels provide the text. playing.html template includes the buttons + in management mode. + + + modified files: + CHANGES dcgi.c disorder_config.5.in templates/help.html + templates/options.labels templates/playing.html + + +2004-10-28 21:04:30 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-3 + + Summary: + Fix file permissions mangled in import + Revision: + disorder--mainline--0.1--patch-3 + + Added executable bit trampled by my add-arch-tag script. + + modified files: + debian/config debian/postinst debian/postrm debian/prerm + htmlman inst makedeb prepare tkdisorder webman + + +2004-10-28 20:45:11 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-2 + + Summary: + Further error corrections for import + Revision: + disorder--mainline--0.1--patch-2 + + Sort out confusion over aclocal.m4/acinclude.m4 + + + new files: + acinclude.m4 + + removed files: + aclocal.m4 + + modified files: + {arch}/=tagging-method + + +2004-10-28 20:02:17 GMT Richard Kettlewell <rjk@greenend.org.uk> patch-1 + + Summary: + Fix missing files in import + Revision: + disorder--mainline--0.1--patch-1 + + Fix =tagging-method to not make debian/*.m4 and configure.ac(!) precious + Add arch-tags to add new files. + + new files: + configure.ac debian/autorules.m4 debian/rules.m4 + + modified files: + {arch}/=tagging-method + + +2004-10-28 19:19:21 GMT Richard Kettlewell <rjk@greenend.org.uk> base-0 + + Summary: + import from cvs tag disorder--mainline--0_1--base-0 + Revision: + disorder--mainline--0.1--base-0 + + Add arch-tag lines to almost all files + 'arch add' binary files and files without known comment syntax: + sounds/scratch.ogg + sounds/slap.ogg + debian/conffiles + debian/templates + Omitted .cvsignore files. + + Make various generated files (whether made by automake, prepare, etc) + precious. + + new files: + AUTHORS BUGS CHANGES ChangeLog DESIGN2 Makefile.am NEWS README + README.streams TODO aclocal.m4 addr.c addr.h api-client.c + api-client.h api-server.c api.c asprintf.c authhash.c + authhash.h casefold.h cgi.c cgi.h cgimain.c charset.c + charset.h client.c client.h config.sample.in configuration.c + configuration.h daemonize.c daemonize.h dbfixup.h dcgi.c + dcgi.h debian/Makefile.am debian/README.Debian + debian/changelog debian/conffiles debian/config debian/control + debian/copyright debian/disorder.config debian/htaccess + debian/postinst debian/postrm debian/prerm debian/templates + disorder-dump.8.in disorder.1.in disorder.3 disorder.c + disorder.h disorder.init.in disorder.py.in disorder.vs + disorder_config.5.in disorder_protocol.5.in disorderd.8.in + disorderd.c dump.c event.c event.h fprintf.c hex.c hex.h + htmlman inputline.c inputline.h inst kvp.c kvp.h log-impl.h + log.c log.h makedeb mem-impl.h mem.c mem.h mixer.c mixer.h + play.c play.h plugin.c plugin.h plugins/Makefile.am + plugins/exec.c plugins/fs.c plugins/mad.c plugins/madshim.h + plugins/notify.c plugins/pick.c plugins/shell.c + plugins/tracklength.c prepare printf.c printf.h queue.c + queue.h regsub.c regsub.h rescan.c server.c server.h sink.c + sink.h snprintf.c sounds/Makefile.am sounds/scratch.ogg + sounds/slap.ogg split.c split.h state.c state.h syscalls.c + syscalls.h table.c table.h templates/Makefile.am + templates/about.html templates/choose.html + templates/choosealpha.html templates/credits.html + templates/help.html templates/options + templates/options.columns templates/options.labels + templates/options.trackname templates/options.transform + templates/playing.html templates/prefs.html + templates/recent.html templates/search.html + templates/sidebar.html templates/stdhead.html + templates/stylesheet.html templates/volume.html tkdisorder + tracks.c tracks.h types.h unicodegc.h utf8.h vacopy.h vector.c + vector.h webman words.c words.h wstat.c wstat.h + + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..401a14c --- /dev/null +++ b/Makefile.am @@ -0,0 +1,27 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +EXTRA_DIST=TODO CHANGES README.streams BUGS ChangeLog.d \ +README.upgrades README.client +SUBDIRS=@subdirs@ + +echo-distdir: + @echo $(distdir) +# arch-tag:7e0566bae866a64c2f998d4253581ce0 diff --git a/README b/README new file mode 100644 index 0000000..64d4940 --- /dev/null +++ b/README @@ -0,0 +1,334 @@ +DisOrder +======== + +This program is used to play random and chosen tracks from a collection of +digital audio files (for instance MP3 and OGG files). If you just set it going +it plays random tracks from your collection, but you can also ask for specific +tracks to be played, either via a command line program or a web interface, and +you can 'scratch' the current track. + +See CHANGES for details of recent changes to DisOrder. + +Currently it only runs on Linux. It could probably be ported to other UNIX +variants in some cases without too much effort. Things you will need: + +Build dependencies: + Name Tested Notes + libdb 4.3.21 4.2 and earlier won't work + libgc 6.3 + libvorbisfile 1.0.1 + libpcre 4.5 need UTF-8 support + libmad 0.15.1b + libgcrypt 1.2.0 + libao 0.8.6 + libasound 1.0.8 + Python 2.3 (optional) + GNU C 3.3, 3.4 + +"Tested" means I've built against that version; earlier or later versions will +often work too. + +Runtime dependencies: + * Players: + + ogg123 and mpg321 work for me, but you could potentially use others. + * Web server: + + Apache 1.3.x works for me, but anything that supports CGI and + authentication should be suitable. + +Development dependencies (only developers will need these): + Automake 1.9.4 AM_PATH_PYTHON not good enough in 1.7 + Autoconf 2.59 + Libtool 1.5.6 1.4 not good enough + Arch 1.3-1 + +Mailing lists: + http://www.chiark.greenend.org.uk/mailman/listinfo/sgo-software-discuss + - discussion of DisOrder (and other software), bug reports, etc + http://www.chiark.greenend.org.uk/mailman/listinfo/sgo-software-announce + - announcements of new versions of DisOrder + + +Installation +============ + + "This place'd be a paradise tomorrow, if every department had a supervisor + with a machine-gun" + +NOTE: If you are upgrading from an earlier version, see README.upgrades. + +1. Build the software. Do something like this: + + ./configure --sysconfdir=/etc --localstatedir=/var + make + + See INSTALL for more details about driving configure. The precise set of + options you pass to configure is up to you, if you like configuration being + in /usr/local/etc or wherever then that should work. + + If you only want to build a subset of DisOrder, specify one or more of the + following options: + --without-server Don't build server or web interface + --without-gtk Don't build GTK+ client (Disobedience) + --without-python Don't build Python support + + See README.client for setting up a standalone client. + +2. Install it. Most of the installation is done via the install target: + + make installdirs install + + The CGI interface has to be installed separately, and you must use Libtool + to install it. For instance: + + ./libtool --mode=install install -m 755 progs/disorder.cgi /usr/local/lib/cgi-bin/disorder + + Depending on how your system is configured you may need to link the disorder + libao driver into the right directory: + + ln -s /usr/local/lib/ao/plugins-2/libdisorder.so /usr/lib/ao/plugins-2/. + +3. Create a 'jukebox' user and group, with the jukebox group being the default + group of the jukebox user. The server will run as this user and group. + Check that this user can read your music files and write to the audio + device, e.g. by playing a track. The exact name doesn't matter, it could be + 'jukebox' or 'disorder' or 'fred' or whatever. + + Do not use a general-purpose user or group, you must create ones + specifically for DisOrder. + +4. Create /etc/disorder/config. Start from examples/config.sample and adapt it + to your own requirements. In particular, you should: + * edit the 'player' commands to reflect the software you have installed. + * edit the 'collection' command to identify the location(s) of your own + digital audio files. These commands also specify the encoding of + filenames, which you should be sure to get right as recovery from an + error here can be painful (see BUGS). + * edit the 'scratch' commands to supply scratch sounds (or delete them if + you don't want any). + * edit the 'trust' command to reflect the user the web interface will + eventually run as. + * edit the 'url' command to give the URL of the web interface. + * add or remove 'stopword' entries as necessary (these words won't take + part in track name searches from the web interface). + + See disorder_config(5) for more details. + +5. Create /etc/disorder/config.private. This should be readable only by the + jukebox group: + + touch /etc/disorder/config.private + chown root:jukebox /etc/disorder/config.private + chmod 640 /etc/disorder/config.private + + Set up a username and password for root, for example with line like this: + + allow root somepassword + + Use (for instance) pwgen(1) to create the password. DO NOT use your root + password - this is a password to give root access to the server, not to give + access to the root login. + + See disorderd(8) and disorder_config(5) for more details. + +6. Make sure the server is started at boot time. On many Linux systems, + examples/disorder.init should be more or less suitable; install it in + /etc/init.d, adapting it as necessary, and make appropriate links from + /etc/rc[0-6].d. If you have a BSD style init then you are on your own. + +7. Make sure the state directory (/var/disorder or /usr/local/var/disorder or + as determined by configure) exists and is writable by the jukebox user. + + mkdir -m 755 /var/disorder + chown disorder:root /var/disorder + +8. Start the server, for instance: + + /etc/init.d/disorder start + + By default disorderd logs to daemon.*; check your syslog.conf to see where + this ends up and look for log messages from disorderd there. If it didn't + start up correctly there should be an error message. Correct the problem + and try again. + +9. After a minute it should start to play something. Try scratching it, as any + of the users you set up in step 5: + + disorder scratch + + The track should stop playing, and (if you set any up) a scratch sound play. + +10. Add any other users you want to config.private. Each user's password + should be stored in a file in their home directory, ~/.disorder/passwd, + which should be readable only by them, and should take the form of a single + line: + + password MYPASSWORD + + (root doesn't need this as the client can read it out of config.private + when running as root.) + + Note that the server must be reloaded (e.g. by 'disorder reconfigure') + when new users are added. + + Alternatively the administrator can create /etc/disorder/config.USERNAME + containing the same thing as above. It can either be owned by the user and + mode 400, or owned by root and the user's group (if you have per-user + groups) and mode 440. + + You can use 'disorder authorize' to automatically pick passwords and + create these files. + +11. Optionally source completion.bash from /etc/profile or similar, for + example: + + . /usr/local/share/disorder/completion.bash + + This provides completion over disorder command and option names. + + +Web Interface +============= + + "Thought I was a gonner baby, but I'm bullet proof" + +These instructions assumes you are using Apache 1.3.x. + +You need to configure a number of things to make this work: + +1. If you want to have a 'jukebox' virtual host, modify the DNS (or hosts file + if you are somehow reading this in the 1980s) accordingly and use a fragment + such as this one: + + <VirtualHost HOSTNAME> + DocumentRoot /home/jukebox/public_html + ServerName jukebox.DOMAIN + ServerAlias jukebox + ServerAdmin webmaster@DOMAIN + ErrorLog /var/log/apache/jukebox/error.log + TransferLog /var/log/apache/jukebox/access.log + Alias /static/ /usr/local/share/disorder/static/ + </VirtualHost> + + /static/ should point to the 'static' directory installed by DisOrder. If + you don't want to use the name 'static' then you can change the url.static + label in the web interface configuration to your preferred URL; see + disorder_config(5) for details. + + Don't forget to reload Apache after modifying its configuration. + + Separate logging is not required but I find it convenient. Up to you. + +2. disorder.cgi assumes it is subject to access control (and in particular uses + the username to report who did what). Here's how I configured Apache, given + the above VirtualHost settings: + + <Directory /home/jukebox> + Require valid-user + AuthType basic + AuthName jukebox + AuthUserFile /home/jukebox/http.users + </Directory> + + Adjust this according to wherever you're going to install disorder.cgi and + its expected URL. + + Don't forget to reload apache after modifying its configuration. If you got + it wrong, fix it and restart Apache. + +3. Create the password file configured above. Something like this: + + # htpasswd -b -c /home/jukebox/http.users myusername mypassword + Adding password for user myusername + # htpasswd -b /home/jukebox/http.users othername otherpass + Adding password for user othername + +4. The jukebox must be configured to trust the web user. I added the following + line to my /etc/disorder/config: + + trust www-data + + This might not be the same on your system! You have to specify the user + that the CGI script runs as, whatever that is. + +5. Install disorder.cgi in an appropriate location. Remember to make it + executable. With the above configuration I installed it as + ~jukebox/public_html/index.cgi. + +6. Give www-data (or whatever user it is) a password and edit + /etc/disorder/config.private accordingly. This file should be mode 640 and + owned by root:jukebox. The line should look something like this: + + allow www-data MYPASSWORD + + After editing the config file, you must make the daemon re-read it: + + disorder reconfigure + +7. Teach www-data its password, by putting it in /etc/disorder/config.www-data. + This file should be mode 640 and owned by root:www-data. + + password MYPASSWORD + + (You could also use ~www-data/.disorder/passwd for this but on some systems + the web server user's home directory is inside the document root, which + would have rather unfortunate consequences.) + +8. Try it out. You should be asked for a username and password that you + configured earlier, and be shown details of what is playing and what other + tracks have been configured for future play. + +9. Some features take time to start working, for instance those involving + reporting the length of tracks. This is because the server starts up as + quickly as possible even if the full track data has not yet been gathered; + the track data is then calculated in the background. + +10. If you run into problems, always look at the appropriate error log; the + message you see in your web browser will usually not be sufficient to + diagnose the problem all by itself. + +11. If you have a huge number of top level directories, then you might find + that the 'Choose' page is unreasonably large. If so add the following line + to /etc/disorder/options.user: + label sidebar.choosewhich choosealpha + + This will make 'Choose' be a link for each letter of the 26-letter Roman + alphabet; follow the link and you just get the directories which start with + that letter. The "*" link at the end gives you directories which don't + start with a letter. + + You can copy choosealpha.html to /etc/disorder and edit it to change the + set of initial choices to anything that can be expressed with regexps. The + regexps must be URL-encoded UTF-8 PCRE regexps. + + +Copyright +========= + + "Nothing but another drug, a licence that you buy and sell" + +DisOrder - select and play digital audio files +Copyright (C) 2003, 2004, 2005, 2006 Richard Kettlewell +Portions extracted from MPG321, http://mpg321.sourceforge.net/ + Copyright (C) 2001 Joe Drew + Copyright (C) 2000-2001 Robert Leslie +Binaries may derive extra copyright owners through linkage (binary distributors +are expected to do their own legwork) + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 59 Temple +Place, Suite 330, Boston, MA 02111-1307 USA + +Local Variables: +mode:text +fill-column:79 +End: +arch-tag:e7058f9442f954f3dd51523a1e805c32 diff --git a/README.client b/README.client new file mode 100644 index 0000000..9a2ff74 --- /dev/null +++ b/README.client @@ -0,0 +1,47 @@ +Setting up a standalone DisOrder client +======================================= + +Although DisOrder can still only play from a single computer, it is possible to +control it over the network if the server has a 'listen PORT' directive in its +configuration file. + +There is currently no standard DisOrder port number. + + +1. Configure the software with --without-server (and, optionally, +--without-python) and build and install it. Set up a stub config in +/etc/disorder/config (or /usr/local/etc/disorder/config if you didn't +set a nondefault --prefix) with the following contents: + + connect jukebox PORT + +where PORT is the same as 'listen PORT' on the server. + + +2. Copy the password file for each user to /etc/disorder/config.USER, +the contents being: + + password PASSWORD + +Alternatvely, each user can use ~/.disorder/passwd, with the same contents. If +the DisOrder username differs from the local username then use a 'username' +directive. + + +3. Test by issuing 'disorder playing'. + + +4. Run 'disobedience' for the GUI client. + + +The web interface could in principle be made to work on a separate +machine from the main server, though it is unlikely to be efficient +and at the moment it is built whenever the server is, so you will have +to unpick them a bit yourself if you wish to do this. + + +Local Variables: +mode:text +fill-column:79 +End: +# arch-tag:jEmzIKHdvK6GSjnax7Kp5Q diff --git a/README.raw b/README.raw new file mode 100644 index 0000000..cf2ca0c --- /dev/null +++ b/README.raw @@ -0,0 +1,55 @@ +* DisOrder Raw Format Players + +** Purpose + +The purpose of raw format players is: + + * Support pausing of playing tracks, with the audio device closed when not + in active use. + + * Eliminate the inter-track gap. + + * Perhaps in the future support network play. + +** Usage + +To use raw format, use the execraw module and make the command choose the +"disorder" libao driver. + +You should pass the "fragile" option to ogg123. This is because ogg123 ignores +write errors! + +mpg321 does not appear to have this bug. + +For _non_ raw players it is advisable to use the new --wait-for-device option. +This repeatedly tries to open the audio device before starting the player +proper. It times out after a couple of seconds. + +See disorder_config(5) and the example configuration file for further +information and examples. + +** Low-Level Details + +Raw format players are started slightly differently to normal ones. Before +they are executed a pipe is created and one end passed to a special speaker +process, which is spawned by the main server at startup. The file descriptor +of the player's end is identified by $DISORDER_RAW_FD. + +The expected data format is a ao_sample_format structure followed by the raw +sample data. However, this may be changed without notice in future versions of +DisOrder. If you need a stable interface here for some reason then get in +touch. + +Raw format players may be started before the track is to be played, and (if the +track is then removed from the queue before it reaches the head) terminated +before the track ever reaches a physical speaker. The point of this is to +allow audio data to be ready to play the moment the previous track end, without +having to wait for the player to start up. There is no way for a player to +tell that this is going on. + +Local Variables: +mode:outline +fill-column:79 +End: + +# arch-tag:FDgaJ8rznoa6TnmXxU1iPw diff --git a/README.streams b/README.streams new file mode 100644 index 0000000..4e6e425 --- /dev/null +++ b/README.streams @@ -0,0 +1,32 @@ +DisOrder and Internet Streams +============================= + +DisOrder doesn't have any built-in support for playing streams but you can make +it do so. I use the following in my configuration file: + + player /export/radio/*.oggradio shell 'xargs ogg123 -q < "$TRACK"' + collection fs iso-8859-1 /export/radio + +After setting this up you'll need to re-read the config file and provoke a +rescan: + + disorder reconfigure rescan /export/radio + +/export/radio contains a file for each stream, containing the URL to use: + + lyonesse$ cat /export/radio/CUR1350.oggradio + http://cur.chu.cam.ac.uk:8000/cur.ogg + +You'll probably want to prevent random play of streams: + + disorder set /export/radio/CUR1350.oggradio pick_at_random 0 + +You can then queue a stream like any other track. It won't automatically +interrupt the playing track, you have to scratch it manually. Go back to +normal play by scratching the stream. + +Local Variables: +mode:text +fill-column:79 +End: +arch-tag:ae95108d51c55288c4f6da4102343cd5 diff --git a/README.upgrades b/README.upgrades new file mode 100644 index 0000000..6e279e7 --- /dev/null +++ b/README.upgrades @@ -0,0 +1,112 @@ +* Upgrading DisOrder + +The general procedure is: + + * stop the old daemon, e.g. with + /etc/init.d/disorder stop + * build and install the new version as described in the README + * update the configuration files (see below) + * start the new daemon, e.g. with + /etc/init.d/disorder start + +The rest of this file describes things you must pay attention to when +upgrading between particular versions. Minor versions are not +explicitly mentioned; a version number like 1.1 implicitly includes +all 1.1.x versions. + +* 1.5 -> 1.6 + +** 'transform' and 'namepart' directives + +'transform' has moved from the web options to the main configuration file, so +that they can be used by other interfaces. The syntax and semantics are +unchanged. + +More importantly however both 'transform' and 'namepart' are now optional, with +sensible defaults being built in. So if you were already using the default +values you can just delete all instances of both. + +** enabled' and 'random_enabled' directives + +These have been removed. Instead the state persists from one run of the server +to the next. + +* 1.3 -> 1.4 + +** Raw Format Decoders + +You will probably want reconfigure your install to use the new facilities +(although the old way works fine). See the example configuration file and +README.raw for more details. + +Depending on how your system is configured you may need to link the disorder +libao driver into the right directory: + + ln -s /usr/local/lib/ao/plugins-2/libdisorder.so /usr/lib/ao/plugins-2/. + +* 1.2 -> 1.3 + +** Server Environment + +It is important that $sbindir is on the server's path. The example init script +guarantees this. You may need to modify the installed one. You will get +"deadlock manager unexpectedly terminated" if you get this wrong. + +** namepart directives + +These have changed in three ways. + +Firstly they have changed to substitute in a more convenient way. Instead of +matches for the regexp being substituted back into the original track name, the +replacement string now completely replaces it. Given the usual uses of +namepart, this is much more convenient. If you've stuck with the defaults no +changes should be needed for this. + +Secondly they are matched against the track name with the collection root +stripped off. + +Finally you will need to add an extra line to your config file as follows for +the new track aliasing mechanisms to work properly: + +namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +* 1.1 -> 1.2 + +** Web Interface Changes + +The web interface now includes static content as well as templates. +The static content must be given a name visible to HTTP clients which +maps to its location in the real filesystem. + +The README suggests using a rule in httpd.conf to make /static in the +HTTP namespace point to /usr/local/share/disorder/static, which is +where DisOrder installs its static content (by default). +Alternatively you can set the url.static label to the base URL of the +static content. + +** Configuration File Changes + +The trackname-part web interface directive has now gone, and the +options.trackname file with it. + +It is replaced by a new namepart directive in the main configuration +file. This has exactly the same syntax as trackname-part, only the +name and location have changed. + +The reason for the change is to allow track name parsing to be +centrally configured, rather than every interface to DisOrder having +to implement it locally. + +If you do not install new namepart directives into the main +configuration file then track titles will show up blank. + +If you do not remove the trackname-part directives from the web +interface configuration then you will get error messages in the web +server's error log. + +Local Variables: +mode:outline +fill-column:79 +End: + +# arch-tag:j+OBlcYYyUdGBVbVXVgXew diff --git a/TODO b/TODO new file mode 100644 index 0000000..704fa3e --- /dev/null +++ b/TODO @@ -0,0 +1,26 @@ +-*-outline-*- + +* plugins + +** configuration + +Allow plugins to be configured via the main config file somehow. + +* web interface + +** language choice + +Parse HTTP_ACCEPT_LANGUAGE and use it to choose template subdirectory. +I might leave this until I hear that someone actually wants a +multilingual jukebox. + +** rearrange queue + +Needs thought on how to design the interface. + +** improve volume control + +** templates + +Build defaults into program to save file IO. +arch-tag:d8e9783460cc93d13e90d8b8e3f1d481 diff --git a/acinclude.m4 b/acinclude.m4 new file mode 100644 index 0000000..03fd6ca --- /dev/null +++ b/acinclude.m4 @@ -0,0 +1,105 @@ +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +AC_DEFUN([RJK_FIND_GC_H],[ + AC_CACHE_CHECK([looking for <gc.h>],[rjk_cv_gc_h],[ + AC_PREPROC_IFELSE([ + #include <gc.h> + ], + [rjk_cv_gc_h="on default include path"],[ + oldCPPFLAGS="${CPPFLAGS}" + for dir in /usr/include/gc /usr/local/include/gc; do + if test "x$GCC" = xyes; then + CPPFLAGS="${oldCPPFLAGS} -isystem $dir" + else + CPPFLAGS="${oldCPPFLAGS} -I$dir" + fi + AC_PREPROC_IFELSE([ + #include <gc.h> + ], + [rjk_cv_gc_h=$dir;break],[rjk_cv_gc_h="not found"]) + done + CPPFLAGS="${oldCPPFLAGS}" + ]) + ]) + case "$rjk_cv_gc_h" in + "not found" ) + missing_headers="$missing_headers gc.h" + ;; + /* ) + if test "x$GCC" = xyes; then + CPPFLAGS="${CPPFLAGS} -isystem $rjk_cv_gc_h" + else + CPPFLAGS="${CPPFLAGS} -I$rjk_cv_gc_h" + fi + ;; + esac +]) + +AC_DEFUN([RJK_CHECK_LIB],[ + AC_CACHE_CHECK([for $2 in -l$1],[rjk_cv_lib_$1_$2],[ + save_LIBS="$LIBS" + LIBS="${LIBS} -l$1" + AC_LINK_IFELSE([AC_LANG_PROGRAM([$3],[$2;])], + [rjk_cv_lib_$1_$2=yes], + [rjk_cv_lib_$1_$2=no]) + LIBS="$save_LIBS" + ]) + if test $rjk_cv_lib_$1_$2 = yes; then + $4 + else + $5 + fi +]) + +AC_DEFUN([RJK_REQUIRE_PCRE_UTF8],[ + AC_CACHE_CHECK([whether libpcre was built with UTF-8 support], + [rjk_cv_pcre_utf8],[ + save_LIBS="$LIBS" + LIBS="$LIBS $1" + AC_RUN_IFELSE([AC_LANG_PROGRAM([ + #include <pcre.h> + #include <stdio.h> + ], + [ + pcre *r; + const char *errptr; + int erroffset; + + r = pcre_compile("\x80\x80", PCRE_UTF8, + &errptr, &erroffset, 0); + if(!r) { + fprintf(stderr, "pcre_compile: %s at %d", + errptr, erroffset); + exit(0); + } else { + fprintf(stderr, "accepted bogus UTF-8 string\n"); + exit(1); + } + ])], + [rjk_cv_pcre_utf8=yes], + [rjk_cv_pcre_utf8=no], + [AC_MSG_ERROR([cross-compiling, cannot check libpcre behaviour])]) + LIBS="$save_LIBS" + ]) + if test $rjk_cv_pcre_utf8 = no; then + AC_MSG_ERROR([please rebuild your pcre library with --enable-utf8]) + fi +]) +# arch-tag:d09b2112a218009313949a279401a5b4 diff --git a/clients/Makefile.am b/clients/Makefile.am new file mode 100644 index 0000000..5cdb0cb --- /dev/null +++ b/clients/Makefile.am @@ -0,0 +1,70 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_PROGRAMS=disorder disorderfm +noinst_PROGRAMS=test-eclient filename-bytes + +AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib + +disorder_SOURCES=disorder.c authorize.c authorize.h +disorder_LDADD=$(LIBOBJS) ../lib/libdisorder.la +disorder_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la + +disorderfm_SOURCES=disorderfm.c +disorderfm_LDADD=$(LIBOBJS) ../lib/libdisorder.la +disorderfm_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la + +filename_bytes_SOURCES=filename-bytes.c + +test_eclient_SOURCES=test-eclient.c +test_eclient_LDADD=../lib/libdisorder.la +test_eclient_DEPENDENCIES=../lib/libdisorder.la + +install-exec-hook: + $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir) + +check: check-help check-completions + +# check everything has working --help +check-help: all + ./disorder --version > /dev/null + ./disorder --help > /dev/null + ./disorder --help-commands > /dev/null + +# check that the command completions are up to date +check-completions: + ./disorder --help-commands \ + | awk '/^ [a-z]/ { print $$1 }' \ + | sort > ,commands + ( set -e;completions() { \ + for x; do \ + case $$x in\ + quack ) ;;\ + [a-z]* ) echo $$x; ;;\ + esac;\ + done;\ + }; \ + complete() { if [ "$$7" = disorder ]; then completions $$6; fi }; \ + . ${top_srcdir}/scripts/completion.bash )\ + | sort > ,completions + diff -u ,commands ,completions + +CLEANFILES=,commands ,completions +# arch-tag:3wdM7iS0B+n8makCG+YcAg diff --git a/clients/authorize.c b/clients/authorize.c new file mode 100644 index 0000000..e38aed4 --- /dev/null +++ b/clients/authorize.c @@ -0,0 +1,104 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <pwd.h> +#include <gcrypt.h> +#include <errno.h> +#include <unistd.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdio.h> + +#include "authorize.h" +#include "log.h" +#include "configuration.h" +#include "printf.h" +#include "hex.h" + +int authorize(const char *user) { + uint8_t pwbin[10]; + const struct passwd *pw, *jbpw; + gid_t jbgid; + char *c, *t, *pwhex; + int fd; + FILE *fp; + + if(!(jbpw = getpwnam(config->user))) + fatal(0, "cannot find user %s", config->user); + jbgid = jbpw->pw_gid; + if(!(pw = getpwnam(user))) + fatal(0, "no such user as %s", user); + if((c = config_userconf(0, pw)) && access(c, F_OK) == 0) { + error(0, "%s already exists", c); + return -1; + } + if((c = config_usersysconf(pw)) && access(c, F_OK) == 0) { + error(0, "%s already exists", c); + return -1; + } + byte_xasprintf(&t, "%s.new", c); + gcry_randomize(pwbin, sizeof pwbin, GCRY_STRONG_RANDOM); + pwhex = hex(pwbin, sizeof pwbin); + + /* create config.USER, to end up with mode 440 user:jukebox */ + if((fd = open(t, O_WRONLY|O_CREAT|O_EXCL, 0600)) < 0) + fatal(errno, "error creating %s", t); + if(fchown(fd, pw->pw_uid, -1) < 0) + fatal(errno, "error chowning %s", t); + if(fchmod(fd, 0400) < 0) + fatal(errno, "error chmoding %s", t); + if(!(fp = fdopen(fd, "w"))) + fatal(errno, "error calling fdopen"); + if(fprintf(fp, "password %s\n", pwhex) < 0 + || fclose(fp) < 0) + fatal(errno, "error writing to %s", t); + if(rename(t, c) < 0) + fatal(errno, "error renaming %s to %s", t, c); + + /* append to config.private. We might create it along the way (though this + * is unlikely) in which case it had better be 640 root:jukebox */ + if(!(c = config_private())) + fatal(0, "cannot determine private config file"); + if((fd = open(c, O_WRONLY|O_APPEND|O_CREAT, 0600)) < 0) + fatal(errno, "error opening %s", c); + if(fchown(fd, 0, jbgid) < 0) + fatal(errno, "error chowning %s", c); + if(fchmod(fd, 0640) < 0) + fatal(errno, "error chmoding %s", t); + if(!(fp = fdopen(fd, "a"))) + fatal(errno, "error calling fdopen"); + if(fprintf(fp, "allow %s %s\n", user, pwhex) < 0 + || fclose(fp) < 0) + fatal(errno, "error appending to %s", c); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:BHATzWNN/1ccK/g2pbA63Q */ diff --git a/clients/authorize.h b/clients/authorize.h new file mode 100644 index 0000000..0098b09 --- /dev/null +++ b/clients/authorize.h @@ -0,0 +1,35 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef AUTHORIZE_H +#define AUTHORIZE_H + +int authorize(const char *user); + +#endif /* AUTHORIZE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:2mW0Quc+PMX1C3BWywYFsQ */ diff --git a/clients/disorder.c b/clients/disorder.c new file mode 100644 index 0000000..46e2c2d --- /dev/null +++ b/clients/disorder.c @@ -0,0 +1,554 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <getopt.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <locale.h> +#include <time.h> +#include <stddef.h> +#include <unistd.h> +#include <assert.h> + +#include "configuration.h" +#include "syscalls.h" +#include "log.h" +#include "queue.h" +#include "client.h" +#include "wstat.h" +#include "table.h" +#include "charset.h" +#include "kvp.h" +#include "split.h" +#include "sink.h" +#include "plugin.h" +#include "mem.h" +#include "defs.h" +#include "authorize.h" +#include "vector.h" + +static int auto_reconfigure; + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "config", required_argument, 0, 'c' }, + { "debug", no_argument, 0, 'd' }, + { "help-commands", no_argument, 0, 'H' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Usage:\n" + " disorder [OPTIONS] COMMAND ...\n" + "Options:\n" + " --help, -h Display usage message\n" + " --help-commands, -H List commands\n" + " --version, -V Display version number\n" + " --config PATH, -c PATH Set configuration file\n" + " --debug, -d Turn on debugging\n"); + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorder version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +static void cf_version(disorder_client *c, + char attribute((unused)) **argv) { + char *v; + + if(disorder_version(c, &v)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(v))); +} + +static void print_queue_entry(const struct queue_entry *q) { + if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track))); + if(q->id) xprintf(" id %s\n", nullcheck(utf82mb(q->id))); + if(q->submitter) xprintf(" submitted by %s at %s", + nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + if(q->played) xprintf(" played at %s", ctime(&q->played)); + if(q->state == playing_started + || q->state == playing_paused) xprintf(" %lds so far", q->sofar); + else if(q->expected) xprintf(" might start at %s", ctime(&q->expected)); + if(q->scratched) xprintf(" scratched by %s\n", + nullcheck(utf82mb(q->scratched))); + else xprintf(" %s\n", playing_states[q->state]); + if(q->wstat) xprintf(" %s\n", wstat(q->wstat)); +} + +static void cf_playing(disorder_client *c, + char attribute((unused)) **argv) { + struct queue_entry *q; + + if(disorder_playing(c, &q)) exit(EXIT_FAILURE); + if(q) + print_queue_entry(q); + else + xprintf("nothing\n"); +} + +static void cf_play(disorder_client *c, char **argv) { + while(*argv) + if(disorder_play(c, *argv++)) exit(EXIT_FAILURE); +} + +static void cf_remove(disorder_client *c, char **argv) { + if(disorder_remove(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_disable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_disable(c)) exit(EXIT_FAILURE); +} + +static void cf_enable(disorder_client *c, char attribute((unused)) **argv) { + if(disorder_enable(c)) exit(EXIT_FAILURE); +} + +static void cf_scratch(disorder_client *c, + char **argv) { + if(disorder_scratch(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_shutdown(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_shutdown(c)) exit(EXIT_FAILURE); +} + +static void cf_reconfigure(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_reconfigure(c)) exit(EXIT_FAILURE); +} + +static void cf_rescan(disorder_client *c, char attribute((unused)) **argv) { + if(disorder_rescan(c)) exit(EXIT_FAILURE); +} + +static void cf_somequeue(disorder_client *c, + int (*fn)(disorder_client *c, + struct queue_entry **qp)) { + struct queue_entry *q; + + if(fn(c, &q)) exit(EXIT_FAILURE); + while(q) { + print_queue_entry(q); + q = q->next; + } +} + +static void cf_recent(disorder_client *c, char attribute((unused)) **argv) { + cf_somequeue(c, disorder_recent); +} + +static void cf_queue(disorder_client *c, char attribute((unused)) **argv) { + cf_somequeue(c, disorder_queue); +} + +static void cf_quack(disorder_client attribute((unused)) *c, + char attribute((unused)) **argv) { + xprintf("\n" + " .------------------.\n" + " | Naath is a babe! |\n" + " `---------+--------'\n" + " \\\n" + " >0\n" + " (<)'\n" + "~~~~~~~~~~~~~~~~~~~~~~\n" + "\n"); +} + +static void cf_somelist(disorder_client *c, char **argv, + int (*fn)(disorder_client *c, + const char *arg, const char *re, + char ***vecp, int *nvecp)) { + char **vec; + const char *re; + + if(argv[1]) + re = xstrdup(argv[1] + 1); + else + re = 0; + if(fn(c, argv[0], re, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static int isarg_regexp(const char *s) { + return s[0] == '~'; +} + +static void cf_dirs(disorder_client *c, + char **argv) { + cf_somelist(c, argv, disorder_directories); +} + +static void cf_files(disorder_client *c, char **argv) { + cf_somelist(c, argv, disorder_files); +} + +static void cf_allfiles(disorder_client *c, char **argv) { + cf_somelist(c, argv, disorder_allfiles); +} + +static void cf_get(disorder_client *c, char **argv) { + char *value; + + if(disorder_get(c, argv[0], argv[1], &value)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(value))); +} + +static void cf_length(disorder_client *c, char **argv) { + long length; + + if(disorder_length(c, argv[0], &length)) exit(EXIT_FAILURE); + xprintf("%ld\n", length); +} + +static void cf_set(disorder_client *c, char **argv) { + if(disorder_set(c, argv[0], argv[1], argv[2])) exit(EXIT_FAILURE); +} + +static void cf_unset(disorder_client *c, char **argv) { + if(disorder_unset(c, argv[0], argv[1])) exit(EXIT_FAILURE); +} + +static void cf_prefs(disorder_client *c, char **argv) { + struct kvp *k; + + if(disorder_prefs(c, argv[0], &k)) exit(EXIT_FAILURE); + for(; k; k = k->next) + xprintf("%s = %s\n", + nullcheck(utf82mb(k->name)), nullcheck(utf82mb(k->value))); +} + +static void cf_search(disorder_client *c, char **argv) { + char **results; + int nresults, n; + + if(disorder_search(c, *argv, &results, &nresults)) exit(EXIT_FAILURE); + for(n = 0; n < nresults; ++n) + xprintf("%s\n", nullcheck(utf82mb(results[n]))); +} + +static void cf_random_disable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_random_disable(c)) exit(EXIT_FAILURE); +} + +static void cf_random_enable(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_random_enable(c)) exit(EXIT_FAILURE); +} + +static void cf_stats(disorder_client *c, + char attribute((unused)) **argv) { + char **vec; + + if(disorder_stats(c, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static void cf_get_volume(disorder_client *c, + char attribute((unused)) **argv) { + int l, r; + + if(disorder_get_volume(c, &l, &r)) exit(EXIT_FAILURE); + xprintf("%d %d\n", l, r); +} + +static void cf_set_volume(disorder_client *c, + char **argv) { + if(disorder_set_volume(c, atoi(argv[0]), atoi(argv[1]))) exit(EXIT_FAILURE); +} + +static void cf_become(disorder_client *c, + char **argv) { + if(disorder_become(c, argv[0])) exit(EXIT_FAILURE); +} + +static void cf_log(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_log(c, sink_stdio("stdout", stdout))) exit(EXIT_FAILURE); +} + +static void cf_move(disorder_client *c, + char **argv) { + long n; + int e; + + if((e = xstrtol(&n, argv[1], 0, 10))) + fatal(e, "cannot convert '%s'", argv[1]); + if(n > INT_MAX || n < INT_MIN) + fatal(e, "%ld out of range", n); + if(disorder_move(c, argv[0], (int)n)) exit(EXIT_FAILURE); +} + +static void cf_part(disorder_client *c, + char **argv) { + char *s; + + if(disorder_part(c, &s, argv[0], argv[1], argv[2])) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(s))); +} + +static int isarg_filename(const char *s) { + return s[0] == '/'; +} + +static void cf_authorize(disorder_client attribute((unused)) *c, + char **argv) { + if(!authorize(argv[0])) + auto_reconfigure = 1; +} + +static void cf_resolve(disorder_client *c, + char **argv) { + char *track; + + if(disorder_resolve(c, &track, argv[0])) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(track))); +} + +static void cf_pause(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_pause(c)) exit(EXIT_FAILURE); +} + +static void cf_resume(disorder_client *c, + char attribute((unused)) **argv) { + if(disorder_resume(c)) exit(EXIT_FAILURE); +} + +static void cf_tags(disorder_client *c, + char attribute((unused)) **argv) { + char **vec; + + if(disorder_tags(c, &vec, 0)) exit(EXIT_FAILURE); + while(*vec) + xprintf("%s\n", nullcheck(utf82mb(*vec++))); +} + +static void cf_get_global(disorder_client *c, char **argv) { + char *value; + + if(disorder_get_global(c, argv[0], &value)) exit(EXIT_FAILURE); + xprintf("%s\n", nullcheck(utf82mb(value))); +} + +static void cf_set_global(disorder_client *c, char **argv) { + if(disorder_set_global(c, argv[0], argv[1])) exit(EXIT_FAILURE); +} + +static void cf_unset_global(disorder_client *c, char **argv) { + if(disorder_unset_global(c, argv[0])) exit(EXIT_FAILURE); +} + +static const struct command { + const char *name; + int min, max; + void (*fn)(disorder_client *c, char **); + int (*isarg)(const char *); + const char *argstr, *desc; +} commands[] = { + { "allfiles", 1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]", + "List all files and directories in DIR" }, + { "authorize", 1, 1, cf_authorize, 0, "USER", + "Authorize USER to connect to the server" }, + { "become", 1, 1, cf_become, 0, "USER", + "Become user USER" }, + { "dirs", 1, 2, cf_dirs, isarg_regexp, "DIR [~REGEXP]", + "List directories in DIR" }, + { "disable", 0, 0, cf_disable, 0, "", + "Disable play" }, + { "disable-random", 0, 0, cf_random_disable, 0, "", + "Disable random play" }, + { "enable", 0, 0, cf_enable, 0, "", + "Enable play" }, + { "enable-random", 0, 0, cf_random_enable, 0, "", + "Enable random play" }, + { "files", 1, 2, cf_files, isarg_regexp, "DIR [~REGEXP]", + "List files in DIR" }, + { "get", 2, 2, cf_get, 0, "TRACK NAME", + "Get a preference value" }, + { "get-global", 1, 1, cf_get_global, 0, "NAME", + "Get a global preference value" }, + { "get-volume", 0, 0, cf_get_volume, 0, "", + "Get the current volume" }, + { "length", 1, 1, cf_length, 0, "TRACK", + "Get the length of TRACK in seconds" }, + { "log", 0, 0, cf_log, 0, "", + "Copy event log to stdout" }, + { "move", 2, 2, cf_move, 0, "TRACK DELTA", + "Move a track in the queue" }, + { "part", 3, 3, cf_part, 0, "TRACK CONTEXT PART", + "Find a track name part" }, + { "pause", 0, 0, cf_pause, 0, "", + "Pause the currently playing track" }, + { "play", 1, INT_MAX, cf_play, isarg_filename, "TRACKS...", + "Add TRACKS to the end of the queue" }, + { "playing", 0, 0, cf_playing, 0, "", + "Report the playing track" }, + { "prefs", 1, 1, cf_prefs, 0, "TRACK", + "Display all the preferences for TRACK" }, + { "quack", 0, 0, cf_quack, 0, 0, 0 }, + { "queue", 0, 0, cf_queue, 0, "", + "Display the current queue" }, + { "random-disable", 0, 0, cf_random_disable, 0, "", + "Disable random play" }, + { "random-enable", 0, 0, cf_random_enable, 0, "", + "Enable random play" }, + { "recent", 0, 0, cf_recent, 0, "", + "Display recently played track" }, + { "reconfigure", 0, 0, cf_reconfigure, 0, "", + "Reconfigure the daemon" }, + { "remove", 1, 1, cf_remove, 0, "TRACK", + "Remove a track from the queue" }, + { "rescan", 0, 0, cf_rescan, 0, "", + "Rescan for new tracks" }, + { "resolve", 1, 1, cf_resolve, 0, "TRACK", + "Resolve alias for TRACK" }, + { "resume", 0, 0, cf_resume, 0, "", + "Resume after a pause" }, + { "scratch", 0, 0, cf_scratch, 0, "", + "Scratch the currently playing track" }, + { "scratch-id", 1, 1, cf_scratch, 0, "ID", + "Scratch the currently playing track" }, + { "search", 1, 1, cf_search, 0, "WORDS", + "Display tracks matching all the words" }, + { "set", 3, 3, cf_set, 0, "TRACK NAME VALUE", + "Set a preference value" }, + { "set-global", 2, 2, cf_set_global, 0, "NAME VALUE", + "Set a global preference value" }, + { "set-volume", 2, 2, cf_set_volume, 0, "LEFT RIGHT", + "Set the volume" }, + { "shutdown", 0, 0, cf_shutdown, 0, "", + "Shut down the daemon" }, + { "stats", 0, 0, cf_stats, 0, "", + "Display server statistics" }, + { "tags", 0, 0, cf_tags, 0, "", + "List known tags" }, + { "unset", 2, 2, cf_unset, 0, "TRACK NAME", + "Unset a preference" }, + { "unset-global", 1, 1, cf_unset_global, 0, "NAME", + "Unset a global preference" }, + { "version", 0, 0, cf_version, 0, "", + "Display the server version" }, +}; + +static void help_commands(void) { + unsigned n, max = 0, l; + + xprintf("Command summary:\n"); + for(n = 0; n < sizeof commands / sizeof *commands; ++n) { + if(!commands[n].desc) continue; + l = strlen(commands[n].name); + if(*commands[n].argstr) + l += strlen(commands[n].argstr) + 1; + if(l > max) + max = l; + } + for(n = 0; n < sizeof commands / sizeof *commands; ++n) { + if(!commands[n].desc) continue; + l = strlen(commands[n].name); + if(*commands[n].argstr) + l += strlen(commands[n].argstr) + 1; + xprintf(" %s%s%s%*s %s\n", commands[n].name, + *commands[n].argstr ? " " : "", + commands[n].argstr, + max - l, "", + commands[n].desc); + } + xfclose(stdout); + exit(0); +} + +int main(int argc, char **argv) { + int n, i, j; + disorder_client *c = 0; + const char *s; + int status = 0; + struct vector args; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + while((n = getopt_long(argc, argv, "hVc:dHL", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'H': help_commands(); + case 'V': version(); + case 'c': configfile = optarg; break; + case 'd': debugging = 1; break; + default: fatal(0, "invalid option"); + } + } + if(config_read()) fatal(0, "cannot read configuration"); + if(!(c = disorder_new(1))) exit(EXIT_FAILURE); + s = config_get_file("socket"); + if(disorder_connect(c)) exit(EXIT_FAILURE); + n = optind; + /* accumulate command args */ + while(n < argc) { + if((i = TABLE_FIND(commands, struct command, name, argv[n])) < 0) + fatal(0, "unknown command '%s'", argv[n]); + if(n + commands[i].min >= argc) + fatal(0, "missing arguments to '%s'", argv[n]); + n++; + vector_init(&args); + for(j = 0; j < commands[i].min; ++j) + vector_append(&args, nullcheck(mb2utf8(argv[n + j]))); + for(; j < commands[i].max + && n + j < argc + && commands[i].isarg(argv[n + j]); ++j) + vector_append(&args, nullcheck(mb2utf8(argv[n + j]))); + vector_terminate(&args); + commands[i].fn(c, args.vec); + n += j; + } + if(auto_reconfigure) { + assert(c != 0); + if(disorder_reconfigure(c)) exit(EXIT_FAILURE); + } + if(c && disorder_close(c)) exit(EXIT_FAILURE); + if(fclose(stdout) < 0) fatal(errno, "error closing stdout"); + return status; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0ff200f4c42e9b04dd781fa89c6129f6 */ diff --git a/clients/disorderfm.c b/clients/disorderfm.c new file mode 100644 index 0000000..0fd4fa0 --- /dev/null +++ b/clients/disorderfm.c @@ -0,0 +1,414 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <getopt.h> +#include <unistd.h> +#include <locale.h> +#include <fcntl.h> +#include <errno.h> +#include <dirent.h> +#include <sys/stat.h> +#include <langinfo.h> +#include <string.h> +#include <fnmatch.h> + +#include "syscalls.h" +#include "log.h" +#include "printf.h" +#include "charset.h" +#include "defs.h" +#include "mem.h" + +/* Arguments etc ----------------------------------------------------------- */ + +typedef int copyfn(const char *from, const char *to); +typedef int mkdirfn(const char *dir, mode_t mode); + +/* Input and output directories */ +static const char *source, *destination; + +/* Function used to copy or link a file */ +static copyfn *copier = link; + +/* Function used to make a directory */ +static mkdirfn *dirmaker = mkdir; + +/* Various encodings */ +static const char *fromencoding, *toencoding, *tagencoding; + +/* Directory for untagged files */ +static const char *untagged; + +/* Extract tag information? */ +static int extracttags; + +/* Windows-friendly filenames? */ +static int windowsfriendly; + +/* Native character encoding (i.e. from LC_CTYPE) */ +static const char *nativeencoding; + +/* Count of errors */ +static long errors; + +/* Included/excluded filename patterns */ +static struct pattern { + struct pattern *next; + const char *pattern; + int type; +} *patterns, **patterns_end = &patterns; + +static int default_inclusion = 1; + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "debug", no_argument, 0, 'd' }, + { "from", required_argument, 0, 'f' }, + { "to", required_argument, 0, 't' }, + { "include", required_argument, 0, 'i' }, + { "exclude", required_argument, 0, 'e' }, + { "extract-tags", no_argument, 0, 'E' }, + { "tag-encoding", required_argument, 0, 'T' }, + { "untagged", required_argument, 0, 'u' }, + { "windows-friendly", no_argument, 0, 'w' }, + { "link", no_argument, 0, 'l' }, + { "symlink", no_argument, 0, 's' }, + { "copy", no_argument, 0, 'c' }, + { "no-action", no_argument, 0, 'n' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Usage:\n" +" disorderfm [OPTIONS] SOURCE DESTINATION\n" +"Options:\n" +" --from, -f ENCODING Source encoding\n" +" --to, -t ENCODING Destination encoding\n" +"If neither --from nor --to are specified then no encoding translation is\n" +"performed. If only one is specified then the other defaults to the current\n" +"locale's encoding.\n" +" --windows-friendly, -w Replace illegal characters with '_'\n" +" --include, -i PATTERN Include files matching a glob pattern\n" +" --exclude, -e PATTERN Include files matching a glob pattern\n" +"--include and --exclude may be used multiple times. They are checked in\n" +"order and the first match wins. If --include is ever used then nonmatching\n" +"files are excluded, otherwise they are included.\n" +" --link, -l Link files from source to destination (default)\n" +" --symlink, -s Symlink files from source to destination\n" +" --copy, -c Copy files from source to destination\n" +" --no-action, -n Just report what would be done\n" +" --debug, -d Debug mode\n" +" --help, -h Display usage message\n" +" --version, -V Display version number\n"); + /* TODO: tag extraction stuff when implemented */ + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorderfm version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +/* Utilities --------------------------------------------------------------- */ + +/* Copy FROM to TO. Has the same signature as link/symlink. */ +static int copy(const char *from, const char *to) { + int fdin, fdout; + char buffer[4096]; + int n; + + if((fdin = open(from, O_RDONLY)) < 0) + fatal(errno, "error opening %s", from); + if((fdout = open(to, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0) + fatal(errno, "error opening %s", to); + while((n = read(fdin, buffer, sizeof buffer)) > 0) { + if(write(fdout, buffer, n) < 0) + fatal(errno, "error writing to %s", to); + } + if(n < 0) fatal(errno, "error reading %s", from); + if(close(fdout) < 0) fatal(errno, "error closing %s", to); + xclose(fdin); + return 0; +} + +static int nocopy(const char *from, const char *to) { + xprintf("%s -> %s\n", + any2mb(fromencoding, from), + any2mb(toencoding, to)); + return 0; +} + +static int nomkdir(const char *dir, mode_t attribute((unused)) mode) { + xprintf("mkdir %s\n", any2mb(toencoding, dir)); + return 0; +} + +/* Name translation -------------------------------------------------------- */ + +static int bad_windows_char(int c) { + switch(c) { + default: + return 0; + /* Documented as bad by MS */ + case '<': + case '>': + case ':': + case '"': + case '\\': + case '|': + /* Not documented as bad by MS but Samba mangles anyway? */ + case '*': + return 1; + } +} + +/* Return the translated form of PATH */ +static char *nametrans(const char *path) { + char *t = any2any(fromencoding, toencoding, path); + + if(windowsfriendly) { + /* See: + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp?frame=true&hidetoc=true */ + /* List of forbidden names */ + static const char *const devicenames[] = { + "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", + "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "CLOCK$" + }; +#define NDEVICENAMES (sizeof devicenames / sizeof *devicenames) + char *s; + size_t n, l; + + /* Certain characters are just not allowed. We replace them with + * underscores. */ + for(s = t; *s; ++s) + if(bad_windows_char((unsigned char)*s)) + *s = '_'; + /* Trailing spaces and dots are not allowed. We just strip them. */ + while(s > t && (s[-1] == ' ' || s[-1] == '.')) + --s; + *s = 0; + /* Reject device names */ + if((s = strchr(t, '.'))) l = s - t; + else l = 0; + for(n = 0; n < NDEVICENAMES; ++n) + if(l == strlen(devicenames[n]) && !strncasecmp(devicenames[n], t, l)) + break; + if(n < NDEVICENAMES) + byte_xasprintf(&t, "_%s", t); + } + return t; +} + +/* The file walker --------------------------------------------------------- */ + +/* Visit file or directory PATH relative to SOURCE. SOURCE is a null pointer + * at the top level. + * + * PATH is something we extracted from the filesystem so by assumption is in + * the FROM encoding, which might _not_ be the same as the current locale's + * encoding. + * + * For most errors we carry on as best we can. + */ +static void visit(const char *path, const char *destpath) { + const struct pattern *p; + struct stat sb; + /* fullsourcepath is the full source pathname for PATH */ + char *fullsourcepath; + /* fulldestpath will be the full destination pathname */ + char *fulldestpath; + /* String to use in error messags. We convert to the current locale; this + * may be somewhat misleading but is necessary to avoid getting EILSEQ in + * error messages. */ + char *errsourcepath, *errdestpath; + + D(("visit %s", path ? path : "NULL")); + + /* Set up all the various path names */ + if(path) { + byte_xasprintf(&fullsourcepath, "%s/%s", + source, path); + byte_xasprintf(&fulldestpath, "%s/%s", + destination, destpath); + byte_xasprintf(&errsourcepath, "%s/%s", + source, any2mb(fromencoding, path)); + byte_xasprintf(&errdestpath, "%s/%s", + destination, any2mb(toencoding, destpath)); + for(p = patterns; p; p = p->next) + if(fnmatch(p->pattern, path, FNM_PATHNAME) == 0) + break; + if(p) { + /* We found a matching pattern */ + if(p->type == 'e') { + D(("%s matches %s therefore excluding", + path, p->pattern)); + return; + } + } else { + /* We did not find a matching pattern */ + if(!default_inclusion) { + D(("%s matches nothing and not including by default", path)); + return; + } + } + } else { + fullsourcepath = errsourcepath = (char *)source; + fulldestpath = errdestpath = (char *)destination; + } + + /* The destination directory might be a subdirectory of the source + * directory. In that case we'd better not descend into it when we encounter + * it in the source. */ + if(!strcmp(fullsourcepath, destination)) { + info("%s matches destination directory, not recursing", errsourcepath); + return; + } + + /* Find out what kind of file we're dealing with */ + if(stat(fullsourcepath, &sb) < 0) { + error(errno, "cannot stat %s", errsourcepath ); + ++errors; + return; + } + if(S_ISREG(sb.st_mode)) { + if(copier != nocopy) + if(unlink(fulldestpath) < 0 && errno != ENOENT) { + error(errno, "cannot remove %s", errdestpath); + ++errors; + return; + } + if(copier(fullsourcepath, fulldestpath) < 0) { + error(errno, "cannot link %s to %s", errsourcepath, errdestpath); + ++errors; + return; + } + } else if(S_ISDIR(sb.st_mode)) { + DIR *dp; + struct dirent *de; + char *childpath, *childdestpath; + + /* We create the directory on the destination side. If it already exists, + * that's fine. */ + if(dirmaker(fulldestpath, 0777) < 0 && errno != EEXIST) { + error(errno, "cannot mkdir %s", errdestpath); + ++errors; + return; + } + /* We read the directory and visit all the files in it in any old order. */ + if(!(dp = opendir(fullsourcepath))) { + error(errno, "cannot open directory %s", errsourcepath); + ++errors; + return; + } + while(((errno = 0), (de = readdir(dp)))) { + if(!strcmp(de->d_name, ".") + || !strcmp(de->d_name, "..")) continue; + if(path) { + byte_xasprintf(&childpath, "%s/%s", path, de->d_name); + byte_xasprintf(&childdestpath, "%s/%s", + destpath, nametrans(de->d_name)); + } else { + childpath = de->d_name; + childdestpath = nametrans(de->d_name); + } + visit(childpath, childdestpath); + } + if(errno) fatal(errno, "error reading directory %s", errsourcepath); + closedir(dp); + } else { + /* We don't handle special files, but we'd better warn the user. */ + info("ignoring %s", errsourcepath); + } +} + +int main(int argc, char **argv) { + int n; + struct pattern *p; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + while((n = getopt_long(argc, argv, "hVdf:t:i:e:ET:u:wlscn", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'V': version(); + case 'd': debugging = 1; break; + case 'f': fromencoding = optarg; break; + case 't': toencoding = optarg; break; + case 'i': + case 'e': + p = xmalloc(sizeof *p); + p->type = n; + p->pattern = optarg; + p->next = 0; + *patterns_end = p; + patterns_end = &p->next; + if(n == 'i') default_inclusion = 0; + break; + case 'E': extracttags = 1; break; + case 'T': tagencoding = optarg; break; + case 'u': untagged = optarg; break; + case 'w': windowsfriendly = 1; break; + case 'l': copier = link; break; + case 's': copier = symlink; break; + case 'c': copier = copy; break; + case 'n': copier = nocopy; dirmaker = nomkdir; break; + default: fatal(0, "invalid option"); + } + } + if(optind == argc) fatal(0, "missing SOURCE and DESTINATION arguments"); + else if(optind + 1 == argc) fatal(0, "missing DESTINATION argument"); + else if(optind + 2 != argc) fatal(0, "redundant extra arguments"); + if(extracttags) fatal(0, "--extract-tags is not implemented yet"); /* TODO */ + if(tagencoding && !extracttags) + fatal(0, "--tag-encoding without --extra-tags does not make sense"); + if(untagged && !extracttags) + fatal(0, "--untagged without --extra-tags does not make sense"); + source = argv[optind]; + destination = argv[optind + 1]; + nativeencoding = nl_langinfo(CODESET); + if(fromencoding || toencoding) { + if(!fromencoding) fromencoding = nativeencoding; + if(!toencoding) toencoding = nativeencoding; + } + if(!tagencoding) tagencoding = nativeencoding; + visit(0, 0); + xfclose(stdout); + if(errors) fprintf(stderr, "%ld errors\n", errors); + return !!errors; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:YWy+lwnCOS0d8Q5hjJ5gyQ */ diff --git a/clients/filename-bytes.c b/clients/filename-bytes.c new file mode 100644 index 0000000..67ce1fd --- /dev/null +++ b/clients/filename-bytes.c @@ -0,0 +1,42 @@ +/* Grotty program to print out the bytes making up filenames in some + * directory */ + +#include "config.h" + +#include <dirent.h> +#include <stdio.h> +#include <ctype.h> + +int main(int attribute((unused)) argc, char **argv) { + DIR *dp; + struct dirent *de; + int n; + + if(!(dp = opendir(argv[1]))) return -1; + while((de = readdir(dp))) { + for(n = 0; de->d_name[n]; ++n) { + printf("%02x", (unsigned char)de->d_name[n]); + if(n) putchar(' '); + } + putchar('\n'); + for(n = 0; de->d_name[n]; ++n) { + if(isprint((unsigned char)de->d_name[n])) + printf(" %c", (unsigned char)de->d_name[n]); + else + printf(" "); + if(n) putchar(' '); + } + putchar('\n'); + } + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:UBGSjLfIc10t/LG6EKQprw */ diff --git a/clients/test-eclient.c b/clients/test-eclient.c new file mode 100644 index 0000000..67900ad --- /dev/null +++ b/clients/test-eclient.c @@ -0,0 +1,189 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/select.h> +#include <stdio.h> +#include <assert.h> +#include <errno.h> +#include <stdlib.h> +#include <time.h> + +#include "queue.h" +#include "mem.h" +#include "log.h" +#include "eclient.h" +#include "configuration.h" +#include "syscalls.h" +#include "wstat.h" +#include "charset.h" + +/* TODO: a more comprehensive test */ + +static fd_set rfd, wfd; +static int maxfd; +static disorder_eclient *clients[1024]; +static char **tracks; +static disorder_eclient *c; +static char u_value; +static int quit; + +static const char *modes[] = { "none", "read", "write", "read write" }; + +static void cb_comms_error(void *u, const char *msg) { + assert(u == &u_value); + fprintf(stderr, "! comms error: %s\n", msg); +} + +static void cb_protocol_error(void *u, + void attribute((unused)) *v, + int attribute((unused)) code, + const char *msg) { + assert(u == &u_value); + fprintf(stderr, "! protocol error: %s\n", msg); +} + +static void cb_poll(void *u, disorder_eclient *c_, int fd, unsigned mode) { + assert(u == &u_value); + assert(fd >= 0); + assert(fd < 1024); /* bodge */ + fprintf(stderr, " poll callback %d %s\n", fd, modes[mode]); + if(mode & DISORDER_POLL_READ) + FD_SET(fd, &rfd); + else + FD_CLR(fd, &rfd); + if(mode & DISORDER_POLL_WRITE) + FD_SET(fd, &wfd); + else + FD_CLR(fd, &wfd); + clients[fd] = mode ? c_ : 0; + if(fd > maxfd) maxfd = fd; +} + +static void cb_report(void attribute((unused)) *u, + const char attribute((unused)) *msg) { +} + +static const disorder_eclient_callbacks callbacks = { + cb_comms_error, + cb_protocol_error, + cb_poll, + cb_report +}; + +/* cheap plastic event loop */ +static void loop(void) { + int n; + + while(!quit) { + fd_set r = rfd, w = wfd; + n = select(maxfd + 1, &r, &w, 0, 0); + if(n < 0) { + if(errno == EINTR) continue; + fatal(errno, "select"); + } + for(n = 0; n <= maxfd; ++n) + if(clients[n] && (FD_ISSET(n, &r) || FD_ISSET(n, &w))) + disorder_eclient_polled(clients[n], + ((FD_ISSET(n, &r) ? DISORDER_POLL_READ : 0) + |(FD_ISSET(n, &w) ? DISORDER_POLL_WRITE : 0))); + } + printf(". quit\n"); +} + +static void done(void) { + printf(". done\n"); + disorder_eclient_close(c); + quit = 1; +} + +static void play_completed(void *v) { + assert(v == tracks); + printf("* played: %s\n", *tracks); + ++tracks; + if(*tracks) { + if(disorder_eclient_play(c, *tracks, play_completed, tracks)) + exit(1); + } else + done(); +} + +static void version_completed(void *v, const char *value) { + printf("* version: %s\n", value); + if(v) { + if(*tracks) { + if(disorder_eclient_play(c, *tracks, play_completed, tracks)) + exit(1); + } else + done(); + } +} + +/* TODO: de-dupe with disorder.c */ +static void print_queue_entry(const struct queue_entry *q) { + if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track))); + if(q->id) xprintf(" id %s\n", nullcheck(utf82mb(q->id))); + if(q->submitter) xprintf(" submitted by %s at %s", + nullcheck(utf82mb(q->submitter)), ctime(&q->when)); + if(q->played) xprintf(" played at %s", ctime(&q->played)); + if(q->state == playing_started + || q->state == playing_paused) xprintf(" %lds so far", q->sofar); + else if(q->expected) xprintf(" might start at %s", ctime(&q->expected)); + if(q->scratched) xprintf(" scratched by %s\n", + nullcheck(utf82mb(q->scratched))); + else xprintf(" %s\n", playing_states[q->state]); + if(q->wstat) xprintf(" %s\n", wstat(q->wstat)); +} + +static void recent_completed(void *v, struct queue_entry *q) { + assert(v == 0); + for(; q; q = q->next) + print_queue_entry(q); + if(disorder_eclient_version(c, version_completed, (void *)"")) exit(1); +} + +int main(int argc, char **argv) { + assert(argc > 0); + mem_init(1); + debugging = 0; /* turn on for even more verbosity */ + if(config_read()) fatal(0, "config_read failed"); + tracks = &argv[1]; + c = disorder_eclient_new(&callbacks, &u_value); + assert(c != 0); + /* stack up several version commands to test pipelining */ + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_version(c, version_completed, 0)) exit(1); + if(disorder_eclient_recent(c, recent_completed, 0)) exit(1); + loop(); + exit(0); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:JRDbPXSlG3t9Urek88LwCg */ diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..4895914 --- /dev/null +++ b/configure.ac @@ -0,0 +1,383 @@ +# Process this file with autoconf to produce a configure script. +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +AC_INIT(disorder, 1.5.1+, richard+disorder@sfere.greenend.org.uk) +AC_CONFIG_AUX_DIR([config.aux]) +AM_INIT_AUTOMAKE(disorder, 1.5.1+) +AC_CONFIG_SRCDIR([server/disorderd.c]) +AM_CONFIG_HEADER([config.h]) + +# What we want to build +want_server=yes +want_gtk=yes +want_python=yes + +# Checks for programs. +AC_PROG_CC +AC_SET_MAKE +if test "x$GCC" = xyes; then + gcc_werror=-Werror +else + gcc_werror="" +fi + +AC_ARG_WITH([server], + [AS_HELP_STRING([--without-server], + [do not build server])], + [want_server=$withval]) +AC_ARG_WITH([gtk], + [AS_HELP_STRING([--without-gtk], + [do not build GTK+ client])], + [want_gtk=$withval]) +AC_ARG_WITH([python], + [AS_HELP_STRING([--without-python], + [do not build Python support])], + [want_python=$withval]) + +subdirs="scripts lib clients doc examples debian" + +if test $want_server = yes; then + subdirs="${subdirs} server plugins driver templates sounds images" +fi +if test $want_python = yes; then + AM_PATH_PYTHON + subdirs="${subdirs} python" +fi +if test $want_gtk = yes; then + subdirs="${subdirs} disobedience" + if test $want_server = no; then + subdirs="${subdirs} images" + fi +fi +AC_SUBST([subdirs]) + +# libtool config +AC_LIBTOOL_DLOPEN +AC_DISABLE_STATIC + +AC_PROG_LIBTOOL + +missing_libraries="" +missing_headers="" +missing_functions="" + +AC_DEFINE(_GNU_SOURCE, 1, [required for e.g. strsignal]) + +# Macs might have libraries under fink's root +AC_PATH_PROG([FINK],[fink],[none],[$PATH:/sw/bin]) +if test "x$FINK" != xnone; then + AC_CACHE_CHECK([fink install directory],[rjk_cv_finkprefix],[ + rjk_cv_finkprefix="`echo "$FINK" | sed 's,/bin/fink$,,'`" + ]) + CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include" + if test $want_server = yes; then + CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/db4" + fi + LDFLAGS="${LDFLAGS} -L${rjk_cv_finkprefix}/lib" +fi + +# Checks for libraries. +# We save up a list of missing libraries that we can't do without +# and report them all at once. +AC_CHECK_LIB(gc, GC_malloc, [AC_SUBST(LIBGC,[-lgc])], + [missing_libraries="$missing_libraries libgc"]) +AC_CHECK_LIB(gcrypt, gcry_md_open, + [AC_SUBST(LIBGCRYPT,[-lgcrypt])], + [missing_libraries="$missing_libraries libgcrypt"]) +AC_CHECK_LIB(pcre, pcre_compile, + [AC_SUBST(LIBPCRE,[-lpcre])], + [missing_libraries="$missing_libraries libpcre"]) +if test $want_server = yes; then + RJK_CHECK_LIB(db, db_create, [#include <db.h>], + [AC_SUBST(LIBDB,[-ldb])], + [missing_libraries="$missing_libraries libdb"]) + AC_CHECK_LIB(vorbis, vorbis_info_clear, + [:], + [missing_libraries="$missing_libraries libvorbis"]) + AC_CHECK_LIB(vorbisfile, ov_open, + [AC_SUBST(LIBVORBISFILE,["-lvorbisfile -lvorbis"])], + [missing_libraries="$missing_libraries libvorbisfile"], + [-lvorbis]) + AC_CHECK_LIB(mad, mad_stream_init, + [AC_SUBST(LIBMAD,[-lmad])], + [missing_libraries="$missing_libraries libmad"]) + AC_CHECK_LIB([ao], [ao_initialize], + [AC_SUBST(LIBAO,[-lao])], + [missing_libraries="$missing_libraries libao"]) + AC_CHECK_LIB([asound], [snd_pcm_open], + [AC_SUBST(LIBASOUND,[-lasound])], + [missing_libraries="$missing_libraries libasound"]) +fi + +if test $want_gtk = yes; then + AM_PATH_GLIB_2_0([],[],[missing_libraries="$missing_libraries libglib"]) + AM_PATH_GTK_2_0([],[],[missing_libraries="$missing_libraries libgtk"]) +fi + +# Some platforms have iconv already +AC_CHECK_FUNC(iconv_open, [:], + [RJK_CHECK_LIB(iconv, iconv_open, [#include <iconv.h>], + [AC_SUBST(LIBICONV,[-liconv])], + [missing_functions="$missing_functions iconv_open"])]) +AC_CHECK_FUNC([gethostbyname],[:],[ + AC_CHECK_LIB(nsl,gethostbyname, + [AC_SUBST(LIBNSL,[-lnsl])], + [missing_functions="$missing_functions gethostbyname"])]) +AC_CHECK_FUNC([socket],[:],[ + AC_CHECK_LIB(socket,socket, + [AC_SUBST(LIBSOCKET,[-lsocket])], + [missing_functions="$missing_functions socket"])]) +AC_CHECK_FUNC([dlopen],[:],[ + AC_CHECK_LIB(dl,dlopen, + [AC_SUBST(LIBDL,[-ldl])], + [missing_functions="$missing_functions dlopen"])]) + +if test ! -z "$missing_libraries"; then + AC_MSG_ERROR([missing libraries:$missing_libraries]) +fi + +# We require that libpcre support UTF-8 +RJK_REQUIRE_PCRE_UTF8([-lpcre]) + +# Checks for header files. +RJK_FIND_GC_H +AC_CHECK_HEADERS([inttypes.h]) +# Compilation will fail if any of these headers are missing, so we +# check for them here and fail early. +# We don't bother checking very standard stuff +if test $want_server = yes; then + AC_CHECK_HEADERS([db.h],[:],[ + missing_headers="$missing_headers $ac_header" + ]) + AC_CHECK_HEADERS([sys/soundcard.h]) dnl can cope without +fi +AC_CHECK_HEADERS([dlfcn.h gcrypt.h \ + getopt.h iconv.h langinfo.h \ + pcre.h sys/ioctl.h \ + syslog.h unistd.h],[:],[ + missing_headers="$missing_headers $ac_header" +]) + +if test ! -z "$missing_headers"; then + AC_MSG_ERROR([missing headers:$missing_headers]) +fi + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_SIZE_T +AC_C_INLINE +AC_CHECK_TYPES([struct sockaddr_in6],,,[AC_INCLUDES_DEFAULT +#include <netinet/in.h>]) + +# enable -Werror when we check for certain characteristics: + +old_CFLAGS="${CFLAGS}" +CFLAGS="${CFLAGS} $gcc_werror" +AC_CHECK_TYPES([long long,uint32_t,uint8_t,intmax_t,uintmax_t]) + +# Some GCC invocations warn for converting function pointers to void *. +# This is fair enough, as it's technically forbidden, but we use dlsym() +# which can pretty much only exist if object and function pointers are +# interconvertable. So we disable -Werror if need be. +if test ! -z "$gcc_werror"; then + AC_CACHE_CHECK([whether function pointers can be converted to void * without a warning], + [rjk_cv_function_pointer_cast],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT + void somefunction(void);], + [(void *)somefunction])], + [rjk_cv_function_pointer_cast=yes], + [rjk_cv_function_pointer_cast=no])]) + if test $rjk_cv_function_pointer_cast = no; then + gcc_werror="" + fi +fi + +CFLAGS="${old_CFLAGS}" + +# gcrypt maintainers keep changing everything. Design your interface +# first, then implement it once, rather than getting it wrong three or +# four times and shipping between each attempt. +AC_CACHE_CHECK([for hash handle type in <grypt.h>], + [rjk_cv_gcrypt_hash_handle],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [gcry_md_hd_t h;])], + [rjk_cv_gcrypt_hash_handle=gcry_md_hd_t],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [GcryMDHd h;])], + [rjk_cv_gcrypt_hash_handle=GcryMDHd], + [rjk_cv_gcrypt_hash_handle=GCRY_MD_HD])])]) +AC_DEFINE_UNQUOTED([gcrypt_hash_handle],[$rjk_cv_gcrypt_hash_handle], + [libgcrypt hash handle type]) + +AC_CACHE_CHECK([for gcry_error_t in <grypt.h>], + [rjk_cv_have_gcry_error_t],[ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT +#include <gcrypt.h> +], + [gcry_error_t e;])], + [rjk_cv_have_gcry_error_t=yes], + [rjk_cv_have_gcry_error_t=no])]) +if test $rjk_cv_have_gcry_error_t = yes; then + AC_DEFINE([HAVE_GCRY_ERROR_T],1,[define if <gcrypt.h> defines gcry_error_t]) +fi + +# Checks for functions +if test $ac_cv_type_long_long = yes; then + AC_CHECK_FUNCS([atoll strtoll],[:],[ + missing_functions="$missing_functions $ac_func" + ]) + # Darwin sometimes fails to declare strtoll (e.g. if you ask for -std=c99) + AC_CACHE_CHECK([whether strtoll is declared in <stdlib.h>], + [rjk_cv_strtoll_declared],[ + AC_EGREP_HEADER([strtoll], [stdlib.h], + [rjk_cv_strtoll_declared=yes], + [rjk_cv_strtoll_declared=no])]) + if test $rjk_cv_strtoll_declared = yes; then + AC_DEFINE([DECLARES_STRTOLL],[1],[define if <stdlib.h> declares strtoll]) + fi + AC_CACHE_CHECK([whether atoll is declared in <stdlib.h>], + [rjk_cv_atoll_declared],[ + AC_EGREP_HEADER([atoll], [stdlib.h], + [rjk_cv_atoll_declared=yes], + [rjk_cv_atoll_declared=no])]) + if test $rjk_cv_atoll_declared = yes; then + AC_DEFINE([DECLARES_ATOLL],[1],[define if <stdlib.h> declares atoll]) + fi +fi +AC_CHECK_FUNCS([ioctl nl_langinfo strsignal],[:],[ + missing_functions="$missing_functions $ac_func" +]) +# fsync will do if fdatasync not available +AC_CHECK_FUNCS([fdatasync],[:],[ + AC_CHECK_FUNCS([fsync], + [AC_DEFINE([fdatasync],[fsync],[define fdatasync to fsync if not available])], + [missing_functions="$missing_functions fdatasync"])]) +if test ! -z "$missing_functions"; then + AC_MSG_ERROR([missing functions:$missing_functions]) +fi +if test $want_server = yes; then + # <db.h> had better be version 3 or later + AC_CACHE_CHECK([db.h version],[rjk_cv_db_version],[ + AC_PREPROC_IFELSE([ + #include <db.h> + #ifndef DB_VERSION_MAJOR + # error cannot determine db version + #endif + #if DB_VERSION_MAJOR < 4 + # error inadequate db version + #endif + #if DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR < 2 + # error inadequate db version + #endif + ], + [rjk_cv_db_version=ok], + [rjk_cv_db_version=inadequate]) + ]) + if test $rjk_cv_db_version != ok; then + AC_MSG_ERROR([need db.h version at least 4.2]) + fi +fi + +if test "x$GCC" = xyes; then + # a reasonable default set of warnings + CC="${CC} -Wall -W -Wpointer-arith -Wbad-function-cast \ + -Wwrite-strings -Wmissing-prototypes \ + -Wmissing-declarations -Wnested-externs" + + # Fix up GTK+ and GLib compiler flags + GTK_CFLAGS="`echo \"$GTK_CFLAGS\"|sed 's/-I/-isystem /g'`" + GLIB_CFLAGS="`echo \"$GLIB_CFLAGS\"|sed 's/-I/-isystem /g'`" + + # GCC 2.95 doesn't know to ignore warnings from system headers + AC_CACHE_CHECK([whether -Werror is usable], + rjk_cv_werror, [ + save_CFLAGS="${CFLAGS}" + CFLAGS="${CFLAGS} ${GTK_CFLAGS} -Werror" + AC_TRY_COMPILE([#include <gtk/gtk.h>], + [], + [rjk_cv_werror=yes], + [rjk_cv_werror=no]) + CFLAGS="${save_CFLAGS}" + ]) + if test $rjk_cv_werror = no; then + gcc_werror='' + fi + CC="${CC} $gcc_werror" + + # for older GCCs that don't know %ju (etc) + AC_CACHE_CHECK([checking whether -Wno-format is required], + rjk_cv_noformat, + AC_TRY_COMPILE([#include <stdio.h> +#include <stdint.h> +], + [printf("%ju", (uintmax_t)0);], + [rjk_cv_noformat=no], + [rjk_cv_noformat=yes])) + if test $rjk_cv_noformat = yes; then + CC="${CC} -Wno-format" + fi + + AC_CACHE_CHECK([checking whether -Wshadow is OK], + rjk_cv_shadow, + oldCC="${CC}" + CC="${CC} -Wshadow" + [AC_TRY_COMPILE([ +#include <unistd.h> +#include <vorbis/vorbisfile.h> +], + [], + [rjk_cv_shadow=yes], + [rjk_cv_shadow=no]) + CC="${oldCC}"]) + if test $rjk_cv_shadow = yes; then + CC="${CC} -Wshadow" + fi + + +fi + +AH_BOTTOM([#ifdef __GNUC__ +# define attribute(x) __attribute__(x) +#else +# define attribute(x) +#endif]) + +AC_CONFIG_FILES([Makefile + templates/Makefile + images/Makefile + scripts/Makefile + lib/Makefile + server/Makefile + clients/Makefile + disobedience/Makefile + doc/Makefile + plugins/Makefile + driver/Makefile + debian/Makefile + sounds/Makefile + python/Makefile + examples/Makefile]) +AC_OUTPUT +# arch-tag:cb633d20520a61a924cd528cef926ec1 diff --git a/debian/Makefile.am b/debian/Makefile.am new file mode 100644 index 0000000..896e39c --- /dev/null +++ b/debian/Makefile.am @@ -0,0 +1,35 @@ +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +noinst_SCRIPTS=rules + +autorules_m4=${srcdir}/autorules.m4 +rules_m4=${srcdir}/rules.m4 + +rules: ${autorules_m4} ${rules_m4} + rm -f rules.tmp + m4 -P ${autorules_m4} ${rules_m4} > rules.tmp + chmod 555 rules.tmp + mv -f rules.tmp rules + +EXTRA_DIST=README.Debian autorules.m4 config control copyright \ + disorder.config htaccess postinst prerm rules.m4 templates \ + rules conffiles postrm changelog options.debian +# arch-tag:1b7bd457d3d740887311435ded0c5eac diff --git a/debian/README.Debian b/debian/README.Debian new file mode 100644 index 0000000..93b2c99 --- /dev/null +++ b/debian/README.Debian @@ -0,0 +1,37 @@ +Debian package for DisOrder +=========================== + +To get the web interface working: + +1) Make sure you /etc/apache/access.conf allows AuthConfig for + /usr/lib/cgi-bin. For instance: + + <Directory /usr/lib/cgi-bin> + AllowOverride AuthConfig + Options ExecCGI FollowSymLinks + </Directory> + +2) If you want to use digest authentication, make sure your Apache has + mod_auth_digest enabled and edit + /usr/lib/cgi-bin/disorder/.htaccess accordingly (it has been made a + conffile). + +3) Remember to reload apache if you've changed anything. + +4) Add users to /etc/disorder/http.users. For instance: + + # htpasswd -b /etc/disorder/http.users USERNAME PASSWORD + + Or with digest authentication: + + # htdigest /etc/disorder/http.users USER + Adding password for USER in realm jukebox. + New password: + Re-type new password: + +5) Test it at http://YOURHOSTNAME/cgi-bin/disorder/disorder + + If it doesn't work, always look at the web server error log. + + -- Richard Kettlewell <rjk@greenend.org.uk>, Sun May 22 14:14:11 2005 +arch-tag:e325cf2983484c130b1ca1df78331fa6 diff --git a/debian/autorules.m4 b/debian/autorules.m4 new file mode 100644 index 0000000..d04393b --- /dev/null +++ b/debian/autorules.m4 @@ -0,0 +1,119 @@ +#! /usr/bin/make -f +# +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +# This file was generated automatically - edit rules.m4 instead +# + +INSTALL=install +CONFIGURE=--prefix=/usr + +m4_divert(-1)m4_dnl + +m4_changequote([,]) + +m4_define([build], [[build]: +m4_syscmd([test -f ../configure || test -f ../config.status])m4_dnl +m4_ifelse(m4_sysval,0,[ ./configure ${CONFIGURE} +])m4_dnl + $(MAKE) prefix=/usr])m4_dnl + +m4_define([binary], [[binary]: [binary]-arch [binary]-indep +[binary]-arch: _archpkgs +[binary]-indep: _indeppkgs]) + +m4_define([anypkg], [m4_define([_package], $1)m4_dnl +m4_define([cleanup], cleanup [cleanpkg-$1])m4_dnl +cleanpkg-$1: + rm -rf debian/$1 + +pkg-$1: [build] + rm -rf debian/$1 + mkdir -p debian/$1 + mkdir -p debian/$1/DEBIAN + mkdir -p debian/$1/usr/share/doc/$1 + cp debian/copyright \ + debian/$1/usr/share/doc/$1/copyright + cp debian/changelog \ + debian/$1/usr/share/doc/$1/changelog.Debian + gzip -9 debian/$1/usr/share/doc/$1/copyright \ + debian/$1/usr/share/doc/$1/changelog.Debian +$2 dpkg-gencontrol -isp -p$1 -Pdebian/$1 -Tdebian/substvars.$1 + chown -R root:root debian/$1 + chmod -R g-ws debian/$1 + dpkg --[build] debian/$1 .. +]) + +m4_define([_target], + [m4_ifelse([$2],[],[$1],[$2])]) + +m4_define([install_usrbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/usr/bin/_target([$1],[$2])]) + +m4_define([install_usrsbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/usr/sbin/_target([$1],[$2])]) + +m4_define([install_bin], + [$(INSTALL) -m 755 $1 \ + debian/_package/bin/_target([$1],[$2])]) + +m4_define([install_sbin], + [$(INSTALL) -m 755 $1 \ + debian/_package/sbin/_target([$1],[$2])]) + +m4_define([_mansect], + [m4_patsubst([$1], [^.*\.\([^.]*\)], [\1])]) + +m4_define([install_usrman], + [$(INSTALL) -m 644 $1 \ + debian/_package/usr/share/man/man[]_mansect(_target([$1],[$2]))/_target([$1],[$2]) + gzip -9 debian/_package/usr/share/man/man[]_mansect(_target([$1],[$2]))/_target([$1],[$2])]) + +m4_define([install_manlink], + [ln -s ../man[]_mansect([$1])/$1.gz \ + debian/_package/usr/man/man[]_mansect([$2])/$2.gz]) + +m4_define([archpkg], [m4_define([_archpkgs], _archpkgs pkg-$1)m4_dnl +anypkg([$1],[$2])]) + +m4_define([indeppkg], [m4_define([_indeppkgs], _indeppkgs pkg-$1)m4_dnl +anypkg([$1],[$2])]) + +m4_define([clean], [[clean]: cleanup + -$(MAKE) distclean + rm -f config.cache + rm -f debian/files + rm -f debian/substvars.*]) + +m4_define([cleanup], []) + +m4_define([_archpkgs], []) + +m4_define([_indeppkgs], []) + +m4_define([regenerate], [debian/rules: debian/autorules.m4 debian/rules.m4 + rm -f debian/rules.tmp + m4 -P debian/autorules.m4 debian/rules.m4 > debian/rules.tmp + chmod 555 debian/rules.tmp + mv -f debian/rules.tmp debian/rules +]) + +m4_divert(0)m4_dnl +# arch-tag:c5871f9bd46d5d2e2ed7302cbcaccd4b diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..3cb4a8a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,90 @@ +disorder (1.5+dev) unstable; urgency=low + + * Intermediate version number + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 17 Apr 2006 19:19:58 +0100 + +disorder (1.5.1) unstable; urgency=low + + * Release 1.5.1 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 25 Mar 2006 18:22:42 +0000 + +disorder (1.5) unstable; urgency=low + + * Release 1.5 + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 20 Mar 2006 22:32:33 +0000 + +disorder (1.4) unstable; urgency=low + + * Release 1.4 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sun, 23 Oct 2005 13:55:17 +0100 + +disorder (1.3) unstable; urgency=low + + * release 1.3 + + -- Richard Kettlewell <rjk@greenend.org.uk> Thu, 16 Jun 2005 21:35:22 +0100 + +disorder (1.2) unstable; urgency=low + + * release 1.2 + + -- Richard Kettlewell <rjk@greenend.org.uk> Fri, 11 Mar 2005 20:00:23 +0000 + +disorder (1.1) unstable; urgency=low + + * release 1.1 + + -- Richard Kettlewell <rjk@greenend.org.uk> Wed, 2 Feb 2005 22:43:47 +0000 + +disorder (1.0) unstable; urgency=low + + * release 1.0 + + -- Richard Kettlewell <rjk@greenend.org.uk> Thu, 30 Dec 2004 12:02:51 +0000 + +disorder (0.13) unstable; urgency=low + + * release 0.13 + + -- Richard Kettlewell <rjk@greenend.org.uk> Wed, 1 Dec 2004 23:48:15 +0000 + +disorder (0.12) unstable; urgency=low + + * release 0.12 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 30 Oct 2004 19:50:33 +0100 + +disorder (0.11) unstable; urgency=low + + * release 0.11 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 25 Sep 2004 17:46:55 +0100 + +disorder (0.10) unstable; urgency=low + + * release 0.10 + + -- Richard Kettlewell <rjk@greenend.org.uk> Sat, 17 Apr 2004 15:29:19 +0100 + +disorder (0.9+dev) unstable; urgency=low + + * intermediate version + + -- Richard Kettlewell <rjk@greenend.org.uk> Fri, 2 Apr 2004 19:50:58 +0100 + +disorder (0.9) unstable; urgency=low + + * release 0.9 + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 15 Mar 2004 20:06:39 +0000 + +disorder (0.8.99+20040315) unstable; urgency=low + + * Debianized + + -- Richard Kettlewell <rjk@greenend.org.uk> Mon, 15 Mar 2004 13:04:02 +0000 +# arch-tag:eb29952826b3a0485b83a4996062a601 diff --git a/debian/conffiles b/debian/conffiles new file mode 100644 index 0000000..7d1aa67 --- /dev/null +++ b/debian/conffiles @@ -0,0 +1,4 @@ +/etc/disorder/config +/etc/disorder/options +/etc/init.d/disorder +/usr/lib/cgi-bin/disorder/.htaccess diff --git a/debian/config b/debian/config new file mode 100755 index 0000000..3d95bc7 --- /dev/null +++ b/debian/config @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +. /usr/share/debconf/confmodule + +db_input high disorder/roots || true +db_input high disorder/encoding || true +db_input medium disorder/scratches || true +db_input medium disorder/server-name || true +db_go || true + +db_get disorder/roots || true +roots="$RET" +db_get disorder/scratches || true +scratches="$RET" +db_get disorder/encoding || true +encoding="$RET" +db_get disorder/server-name || true +server_name="$RET" + +mkdir -p /etc/disorder +cat > /etc/disorder/conf.debconf.new <<EOF +# created automatically from debconf information +# do not edit manually +# run 'dpkg-reconfigure disorder' instead +EOF +for r in $roots; do + echo "collection fs $encoding $r" >> /etc/disorder/conf.debconf.new +done +for s in $scratches; do + echo "scratch $s" >> /etc/disorder/conf.debconf.new +done +echo "url http://$server_name/cgi-bin/disorder/disorder" >> /etc/disorder/conf.debconf.new + +mv /etc/disorder/conf.debconf.new /etc/disorder/conf.debconf +# arch-tag:80ca629b164394d9806d6e8378205951 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..1ea9dad --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Source: disorder +Maintainer: Richard Kettlewell <richard+disorder@sfere.greenend.org.uk> +Priority: optional +Standards-Version: 3.0.1.0 +Build-Depends: libgc6-dev | libgc-dev, libgcrypt-dev, libdb4.3-dev, libpcre3-dev, libvorbis-dev, libmad0-dev, libasound2-dev, libao-dev, python + +Package: disorder +Architecture: any +Section: sound +Priority: extra +Depends: httpd,pwgen,mpg321,vorbis-tools,sox,${shlibs:Depends} +Conflicts: jukebox +Description: Play random or selected digital audio files continuously + Controlled from the command line or via a web-based interface. +# arch-tag:747c10dcef548c0ba4ac60fc8864f4fa diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..6731eeb --- /dev/null +++ b/debian/copyright @@ -0,0 +1,25 @@ +Package home page: + http://www.greenend.org.uk/rjk/disorder/ + +DisOrder - select and play digital audio files +Copyright (C) 2003, 2004, 2005 Richard Kettlewell +Portions extracted from MPG321, http://mpg321.sourceforge.net/ + Copyright (C) 2001 Joe Drew + Copyright (C) (C) 2000-2001 Robert Leslie +Binaries may derive extra copyright owners through linkage +(binary distributors are expected to do their own legwork) + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# arch-tag:5e1b9b57d7568564f88a0a7894c9ca99 diff --git a/debian/disorder.config b/debian/disorder.config new file mode 100644 index 0000000..2f44f50 --- /dev/null +++ b/debian/disorder.config @@ -0,0 +1,50 @@ +# player programs +player *.mp3 execraw mpg321 -q -o disorder +player *.ogg execraw ogg123 -q -d disorder -o fragile:1 +player *.wav shell play + +# don't leave a gap between tracks +gap 0 + +# trust the web user and root +trust www-data root + +# run as user jukebox +user jukebox + +# volume control +mixer /dev/mixer +channel pcm + +# stopwords (i.e. ignored words) for the track search facility +stopword 01 02 03 04 05 06 07 08 09 10 +stopword 1 2 3 4 5 6 7 8 9 +stopword 11 12 13 14 15 16 17 18 19 20 +stopword 21 22 23 24 25 26 27 28 29 30 +stopword the a an and to too in on of we i am as im for is + +# namepart and transform are now filled in by default if you do not supply +# them. However if you supply any namepart directives then you will not +# get any defaults at all, so you must supply the full set. Similarly, +# if you supply any transform directives then you must supply the full set. + +# Parsing of track names for the currently playing track, the recently +# played list and the queue. +#namepart title "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#namepart title "/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort +#namepart album "/([^/]+)/[^/]+$" "$1" * +#namepart artist "/([^/]+)/[^/]+/[^/]+$" "$1" * +# used in alias construction +#namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +# Transformations of directory and filenames for the track choice screen +#transform track "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#transform track "^.*/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort + +#transform dir "^.*/([^/]+)$" "$1" * +#transform dir "^(the) ([^/]*)" "$2, $1" sort i +#transform dir "[[:punct:]]" "" sort g + +# include debconf configuration +include /etc/disorder/conf.debconf +# arch-tag:0c5987c73419c0551559cc72c3efa252 diff --git a/debian/htaccess b/debian/htaccess new file mode 100644 index 0000000..1f0c3a3 --- /dev/null +++ b/debian/htaccess @@ -0,0 +1,5 @@ +Require valid-user +AuthType basic +AuthName jukebox +AuthUserFile /etc/disorder/http.users +# arch-tag:2c90f33e2ed8bd26f2a0091113abfe0b diff --git a/debian/options.debian b/debian/options.debian new file mode 100644 index 0000000..ea164b2 --- /dev/null +++ b/debian/options.debian @@ -0,0 +1,27 @@ +# debian-specific disorder options +include options.labels + +# default columns +include options.columns + +# trackname transformations - supply your own or keep the default +include options.transform + +label images.enabled /disorder/tick.png +label images.disabled /disorder/cross.png +label images.scratch /disorder/cross.png +label images.noscratch /disorder/nocross.png +label images.up /disorder/up.png +label images.noup /disorder/noup.png +label images.down /disorder/down.png +label images.nodown /disorder/nodown.png +label images.edit /disorder/edit.png +label images.upall /disorder/upup.png +label images.noupall /disorder/noupup.png +label images.downall /disorder/downdown.png +label images.nodownall /disorder/nodowndown.png +label links.css /disorder/disorder.css + +# user overrides - you supply this +include options.user +# arch-tag:N/cCdPUdzmiFgcl69+Gj4g diff --git a/debian/postinst b/debian/postinst new file mode 100755 index 0000000..2ffe148 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,98 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +. /usr/share/debconf/confmodule + +add_jukebox_user() { + adduser --quiet --system --group --shell /bin/sh --home /var/lib/disorder \ + --no-create-home jukebox +} + +configure_init_d() { + update-rc.d disorder defaults 92 19 > /dev/null +} + +restart_server() { + /etc/init.d/disorder restart +} + +case "$1" in +configure ) + if grep -q ^jukebox: /etc/passwd; then + : + else + add_jukebox_user + fi + if test ! -f /etc/disorder/config.private; then + # pwgen in debian stable has insane exit status + set +e + rootpw=`pwgen 16 1` + webpw=`pwgen 16 1` + set -e + if test -z "$rootpw" || test -z "$webpw"; then + echo "$0: pwgen failed" 1>&2 + exit 1 + fi + u=`umask` + umask 077 + + echo allow root "$rootpw" > /etc/disorder/config.private.new + echo allow www-data "$webpw" >> /etc/disorder/config.private.new + chgrp jukebox /etc/disorder/config.private.new + chmod 640 /etc/disorder/config.private.new + mv /etc/disorder/config.private.new /etc/disorder/config.private + + if test ! -f /etc/disorder/config.www-data; then + echo password "$webpw" > /etc/disorder/config.www-data.new + chgrp www-data /etc/disorder/config.www-data.new + chmod 640 /etc/disorder/config.www-data.new + mv /etc/disorder/config.www-data.new /etc/disorder/config.www-data + fi + umask $u + fi + + if test ! -f /etc/disorder/http.users; then + u=`umask` + umask 077 + touch /etc/disorder/http.users + chgrp www-data /etc/disorder/http.users + chmod 640 /etc/disorder/http.users + umask $u + fi + chown jukebox:jukebox /var/lib/disorder + configure_init_d + restart_server + if test ! -e /etc/disorder/http.users; then + touch /etc/disorder/http.users + fi + db_stop + ldconfig -n /usr/lib + ;; +abort-upgrade ) + /etc/init.d/disorder restart + ;; +reconfigure ) + /etc/init.d/disorder reload + ;; +esac +# arch-tag:ec773fd84ef6c95eca2e7287d2db9878 diff --git a/debian/postrm b/debian/postrm new file mode 100755 index 0000000..1c09f1c --- /dev/null +++ b/debian/postrm @@ -0,0 +1,27 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +set -e +case "$1" in +remove ) + ldconfig -n /usr/lib + ;; +esac +# arch-tag:829be82824d0c6a903228934b29939e8 diff --git a/debian/prerm b/debian/prerm new file mode 100755 index 0000000..a15ed80 --- /dev/null +++ b/debian/prerm @@ -0,0 +1,23 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# +set -e +/etc/init.d/disorder stop +# arch-tag:99c16a42035fb0fb74a828543a666eb3 diff --git a/debian/rules.m4 b/debian/rules.m4 new file mode 100644 index 0000000..df6a051 --- /dev/null +++ b/debian/rules.m4 @@ -0,0 +1,73 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +CONFIGURE=--prefix=/usr --sysconfdir=/etc --localstatedir=/var/lib --mandir=/usr/share/man +LIBTOOL=./libtool + +build + +archpkg([disorder], [ m4_dnl + $(MAKE) DESTDIR=`pwd`/debian/disorder staticdir=/var/www/disorder installdirs install + mkdir -m 755 -p debian/disorder/etc/disorder + mkdir -m 755 -p debian/disorder/etc/init.d + mkdir -m 755 -p debian/disorder/usr/lib/cgi-bin/disorder + mkdir -m 755 -p debian/disorder/var/lib/disorder + mkdir -m 755 -p debian/disorder/usr/share/doc/disorder/ChangeLog.d + $(INSTALL) -m 755 examples/disorder.init \ + debian/disorder/etc/init.d/disorder + $(INSTALL) -m 644 debian/disorder.config \ + debian/disorder/etc/disorder/config + $(INSTALL) -m 644 debian/options.debian \ + debian/disorder/etc/disorder/options + $(LIBTOOL) --mode=install $(INSTALL) -m 555 server/disorder.cgi \ + $(shell pwd)/debian/disorder/usr/lib/cgi-bin/disorder/disorder + dpkg-shlibdeps -Tdebian/substvars.disorder \ + debian/disorder/usr/bin/* \ + debian/disorder/usr/lib/cgi-bin/disorder/* \ + debian/disorder/usr/sbin/* \ + debian/disorder/usr/lib/*.so* \ + debian/disorder/usr/lib/disorder/*.so* + $(INSTALL) -m 444 debian/htaccess \ + debian/disorder/usr/lib/cgi-bin/disorder/.htaccess + $(INSTALL) -m 444 CHANGES README debian/README.Debian \ + BUGS README.* \ + debian/disorder/usr/share/doc/disorder/. + $(INSTALL) -m 444 ChangeLog.d/*--* \ + debian/disorder/usr/share/doc/disorder/ChangeLog.d + $(INSTALL) -m 444 COPYING debian/disorder/usr/share/doc/disorder/GPL + gzip -9f debian/disorder/usr/share/doc/disorder/ChangeLog.d/*--* \ + debian/disorder/usr/share/doc/disorder/CHANGES \ + debian/disorder/usr/share/doc/disorder/README \ + debian/disorder/usr/share/doc/disorder/README.* \ + debian/disorder/usr/share/doc/disorder/BUGS \ + debian/disorder/usr/share/doc/disorder/GPL + $(INSTALL) -m 555 debian/postinst debian/prerm debian/postrm \ + debian/config \ + debian/conffiles \ + debian/disorder/DEBIAN/. + $(INSTALL) -m 444 debian/templates debian/disorder/DEBIAN/. +]) + +binary + +clean + +regenerate +# arch-tag:be5fb519cf2e3214d82bdae4c9af2b17 diff --git a/debian/templates b/debian/templates new file mode 100644 index 0000000..a6712d1 --- /dev/null +++ b/debian/templates @@ -0,0 +1,28 @@ +Template: disorder/roots +Type: string +Default: +Description: Audio directories + Enter the list of directories that contain audio files (e.g. MP3 or OGG + files) separated by spaces. + +Template: disorder/encoding +Type: string +Default: ISO-8859-1 +Description: Filesystem encoding + Enter your filesystem's character encoding. Check rather than + guessing if you are not sure. + +Template: disorder/scratches +Type: string +Default: /usr/share/disorder/slap.ogg /usr/share/disorder/scratch.ogg +Description: Scratch files + Enter a list of files to be played when a track is scratched, separated + by spaces. Leave this blank if you don't want any scratch sounds. + All filenames should be absolute path names. + +Template: disorder/server-name +Type: string +Default: localhost +Description: Web server hostname + Enter the hostname that the web interface will appear under. + diff --git a/disobedience/Makefile.am b/disobedience/Makefile.am new file mode 100644 index 0000000..cfbba08 --- /dev/null +++ b/disobedience/Makefile.am @@ -0,0 +1,47 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_PROGRAMS=disobedience + +AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib +AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS) + +disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c \ + choose.c misc.c style.h control.c properties.c menu.c +disobedience_LDADD=../lib/libdisorder.la +disobedience_LDFLAGS=$(GTK_LIBS) + +install-exec-hook: + $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir) + +check: check-help + +disobedience.o: style.h + +style.h: ${srcdir}/disobedience.rc ${top_srcdir}/scripts/text2c + ${top_srcdir}/scripts/text2c style ${srcdir}/disobedience.rc > style.h.tmp + mv style.h.tmp style.h + +EXTRA_DIST=disobedience.rc + +# check everything has working --help +check-help: all + ./disobedience --version > /dev/null +# arch-tag:SILYIkQB3aPIe8z+Ng994A diff --git a/disobedience/TODO b/disobedience/TODO new file mode 100644 index 0000000..fa1dee6 --- /dev/null +++ b/disobedience/TODO @@ -0,0 +1,6 @@ +properties + make return be the same as OK +search + select tracks by tag + +# arch-tag:MyZQrifgPbO8ip4FqZo1eg diff --git a/disobedience/choose.c b/disobedience/choose.c new file mode 100644 index 0000000..369c601 --- /dev/null +++ b/disobedience/choose.c @@ -0,0 +1,999 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Choose track ------------------------------------------------------------ */ + +/* We don't use the built-in tree widgets because they require that you know + * the children of a node on demand, and we have to wait for the server to tell + * us. */ + +/* Types */ + +struct choosenode; + +struct displaydata { + guint width; /* total width required */ + guint height; /* total height required */ +}; + +/* instantiate the node vector type */ +VECTOR_TYPE(nodevector, struct choosenode *, xrealloc) + +struct choosenode { + struct choosenode *parent; /* parent node */ + const char *path; /* full path or 0 */ + const char *sort; /* sort key */ + const char *display; /* display name */ + int pending; /* pending resolve queries */ + unsigned flags; +#define CN_EXPANDABLE 0x0001 /* node is expandable */ +#define CN_EXPANDED 0x0002 /* node is expanded */ +/* Expandable items are directories; non-expandable ones are files */ +#define CN_DISPLAYED 0x0004 /* widget is displayed in layout */ +#define CN_SELECTED 0x0008 /* node is selected */ + struct nodevector children; /* vector of children */ + void (*fill)(struct choosenode *); /* request child fill or 0 for leaf */ + GtkWidget *container; /* the container for this row */ + GtkWidget *hbox; /* the hbox for this row */ + GtkWidget *arrow; /* arrow widget or 0 */ + GtkWidget *label; /* text label for this node */ + GtkWidget *marker; /* queued marker */ +}; + +struct menuitem { + /* Parameters */ + const char *name; /* name */ + + /* Callbacks */ + void (*activate)(GtkMenuItem *menuitem, gpointer user_data); + /* Called to activate the menu item. The user data is the choosenode the + * pointer is over. */ + + gboolean (*sensitive)(struct choosenode *cn); + /* Called to determine whether the menu item should be sensitive. TODO */ + + /* State */ + gulong handlerid; /* signal handler ID */ + GtkWidget *w; /* menu item widget */ +}; + +/* Variables */ + +static GtkWidget *chooselayout; +static GtkWidget *searchentry; /* search terms */ +static struct choosenode *root; +static struct choosenode *realroot; +static GtkWidget *menu; /* our popup menu */ +static struct choosenode *last_click; /* last clicked node for selection */ +static int files_visible; /* total files visible */ +static int files_selected; /* total files selected */ +static int search_in_flight; /* a search is underway */ +static int search_obsolete; /* the current search is void */ +static char **searchresults; /* search results */ +static int nsearchresults; /* number of results */ + +/* Forward Declarations */ + +static struct choosenode *newnode(struct choosenode *parent, + const char *path, + const char *display, + const char *sort, + unsigned flags, + void (*fill)(struct choosenode *)); +static void fill_root_node(struct choosenode *cn); +static void fill_letter_node(struct choosenode *cn); +static void fill_directory_node(struct choosenode *cn); +static void got_files(void *v, int nvec, char **vec); +static void got_resolved_file(void *v, const char *track); +static void got_dirs(void *v, int nvec, char **vec); + +static void expand_node(struct choosenode *cn); +static void contract_node(struct choosenode *cn); +static void updated_node(struct choosenode *cn, int redisplay); + +static void display_selection(struct choosenode *cn); +static void clear_selection(struct choosenode *cn); + +static void redisplay_tree(void); +static struct displaydata display_tree(struct choosenode *cn, int x, int y); +static void undisplay_tree(struct choosenode *cn); +static void initiate_search(void); +static void delete_widgets(struct choosenode *cn); + +static void clicked_choosenode(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data); + +static void activate_play(GtkMenuItem *menuitem, gpointer user_data); +static void activate_remove(GtkMenuItem *menuitem, gpointer user_data); +static void activate_properties(GtkMenuItem *menuitem, gpointer user_data); + +static gboolean sensitive_play(struct choosenode *cn); +static gboolean sensitive_remove(struct choosenode *cn); +static gboolean sensitive_properties(struct choosenode *cn); + +static struct menuitem menuitems[] = { + { "Play", activate_play, sensitive_play, 0, 0 }, + { "Remove", activate_remove, sensitive_remove, 0, 0 }, + { "Properties", activate_properties, sensitive_properties, 0, 0 }, +}; + +#define NMENUITEMS (int)(sizeof menuitems / sizeof *menuitems) + +/* Maintaining the data structure ------------------------------------------ */ + +/* Create a new node */ +static struct choosenode *newnode(struct choosenode *parent, + const char *path, + const char *display, + const char *sort, + unsigned flags, + void (*fill)(struct choosenode *)) { + struct choosenode *const n = xmalloc(sizeof *n); + + D(("newnode %s %s", path, display)); + if(flags & CN_EXPANDABLE) + assert(fill); + else + assert(!fill); + n->parent = parent; + n->path = path; + n->display = display; + n->sort = sort; + n->flags = flags; + nodevector_init(&n->children); + n->fill = fill; + if(parent) + nodevector_append(&parent->children, n); + return n; +} + +/* Fill the root */ +static void fill_root_node(struct choosenode *cn) { + int ch; + char *name; + struct callbackdata *cbd; + + D(("fill_root_node")); + if(choosealpha) { + if(!cn->children.nvec) { /* Only need to do this once */ + for(ch = 'A'; ch <= 'Z'; ++ch) { + byte_xasprintf(&name, "%c", ch); + newnode(cn, "<letter>", name, name, CN_EXPANDABLE, fill_letter_node); + } + newnode(cn, "<letter>", "*", "~", CN_EXPANDABLE, fill_letter_node); + } + updated_node(cn, 1); + } else { + /* More de-duping possible here */ + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, "", 0, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, "", 0, cbd); + } +} + +/* Clear all the children of CN */ +static void clear_children(struct choosenode *cn) { + int n; + + D(("clear_children %s", cn->path)); + /* Recursively clear subtrees */ + for(n = 0; n < cn->children.nvec; ++n) { + clear_children(cn->children.vec[n]); + if(cn->children.vec[n]->container) { + if(cn->children.vec[n]->arrow) + gtk_widget_destroy(cn->children.vec[n]->arrow); + gtk_widget_destroy(cn->children.vec[n]->label); + if(cn->children.vec[n]->marker) + gtk_widget_destroy(cn->children.vec[n]->marker); + gtk_widget_destroy(cn->children.vec[n]->hbox); + gtk_widget_destroy(cn->children.vec[n]->container); + } + } + cn->children.nvec = 0; +} + +/* Fill a child node */ +static void fill_letter_node(struct choosenode *cn) { + const char *regexp; + struct callbackdata *cbd; + + D(("fill_letter_node %s", cn->display)); + switch(cn->display[0]) { + default: + byte_xasprintf((char **)®exp, "^(the )?%c", tolower(cn->display[0])); + break; + case 'T': + regexp = "^(?!the [^t])t"; + break; + case '*': + regexp = "^[^a-z]"; + break; + } + /* TODO: caching */ + /* TODO: de-dupe against fill_directory_node */ + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + clear_children(cn); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, "", regexp, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, "", regexp, cbd); +} + +/* Called with a list of files just below some node */ +static void got_files(void *v, int nvec, char **vec) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode; + int n; + + D(("got_files %d files for %s", nvec, cn->path)); + /* Complicated by the need to resolve aliases. We can save a bit of effort + * by re-using cbd though. */ + cn->pending = nvec; + for(n = 0; n < nvec; ++n) + disorder_eclient_resolve(client, got_resolved_file, vec[n], cbd); +} + +static void got_resolved_file(void *v, const char *track) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode, *file_cn; + + file_cn = newnode(cn, track, + trackname_transform("track", track, "display"), + trackname_transform("track", track, "sort"), + 0/*flags*/, 0/*fill*/); + /* Only bother updating when we've got the lot */ + if(--cn->pending == 0) + updated_node(cn, 1); +} + +/* Called with a list of directories just below some node */ +static void got_dirs(void *v, int nvec, char **vec) { + struct callbackdata *cbd = v; + struct choosenode *cn = cbd->u.choosenode; + int n; + + D(("got_dirs %d dirs for %s", nvec, cn->path)); + for(n = 0; n < nvec; ++n) + newnode(cn, vec[n], + trackname_transform("dir", vec[n], "display"), + trackname_transform("dir", vec[n], "sort"), + CN_EXPANDABLE, fill_directory_node); + updated_node(cn, 1); +} + +/* Fill a child node */ +static void fill_directory_node(struct choosenode *cn) { + struct callbackdata *cbd; + + D(("fill_directory_node %s", cn->path)); + /* TODO: caching */ + /* TODO: de-dupe against fill_letter_node */ + assert(report_label != 0); + gtk_label_set_text(GTK_LABEL(report_label), "getting files"); + cn->children.nvec = 0; + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_dirs(client, got_dirs, cn->path, 0, cbd); + cbd = xmalloc(sizeof *cbd); + cbd->u.choosenode = cn; + disorder_eclient_files(client, got_files, cn->path, 0, cbd); +} + +/* Expand a node */ +static void expand_node(struct choosenode *cn) { + D(("expand_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* If node is already expanded do nothing. */ + if(cn->flags & CN_EXPANDED) return; + /* We mark the node as expanded and request that it fill itself. When it has + * completed it will called updated_node() and we can redraw at that + * point. */ + cn->flags |= CN_EXPANDED; + /* TODO: visual feedback */ + cn->fill(cn); +} + +/* Contract a node */ +static void contract_node(struct choosenode *cn) { + D(("contract_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* If node is already contracted do nothing. */ + if(!(cn->flags & CN_EXPANDED)) return; + cn->flags &= ~CN_EXPANDED; + /* Clear selection below this node */ + clear_selection(cn); + /* We can contract a node immediately. */ + redisplay_tree(); +} + +/* qsort callback for ordering choosenodes */ +static int compare_choosenode(const void *av, const void *bv) { + const struct choosenode *const *aa = av, *const *bb = bv; + const struct choosenode *a = *aa, *b = *bb; + + return compare_tracks(a->sort, b->sort, + a->display, b->display, + a->path, b->path); +} + +/* Called when an expandable node is updated. */ +static void updated_node(struct choosenode *cn, int redisplay) { + D(("updated_node %s", cn->path)); + assert(cn->flags & CN_EXPANDABLE); + /* It might be that the node has been de-expanded since we requested the + * update. In that case we ignore this notification. */ + if(!(cn->flags & CN_EXPANDED)) return; + /* Sort children */ + qsort(cn->children.vec, cn->children.nvec, sizeof (struct choosenode *), + compare_choosenode); + if(redisplay) + redisplay_tree(); +} + +/* Searching --------------------------------------------------------------- */ + +static int compare_track_for_qsort(const void *a, const void *b) { + return compare_path(*(char **)a, *(char **)b); +} + +/* Return true iff FILE is a child of DIR */ +static int is_child(const char *dir, const char *file) { + const size_t dlen = strlen(dir); + + return (!strncmp(file, dir, dlen) + && file[dlen] == '/' + && strchr(file + dlen + 1, '/') == 0); +} + +/* Return true iff FILE is a descendant of DIR */ +static int is_descendant(const char *dir, const char *file) { + const size_t dlen = strlen(dir); + + return !strncmp(file, dir, dlen) && file[dlen] == '/'; +} + +/* Called to fill a node in the search results tree */ +static void fill_search_node(struct choosenode *cn) { + int n; + const size_t plen = strlen(cn->path); + const char *s; + char *dir, *last = 0; + + D(("fill_search_node %s", cn->path)); + /* We depend on the search results being sorted as by compare_path(). */ + cn->children.nvec = 0; + for(n = 0; n < nsearchresults; ++n) { + /* We only care about descendants of CN */ + if(!is_descendant(cn->path, searchresults[n])) + continue; + s = strchr(searchresults[n] + plen + 1, '/'); + if(s) { + /* We've identified a subdirectory of CN. */ + dir = xstrndup(searchresults[n], s - searchresults[n]); + if(!last || strcmp(dir, last)) { + /* Not a duplicate */ + last = dir; + newnode(cn, dir, + trackname_transform("dir", dir, "display"), + trackname_transform("dir", dir, "sort"), + CN_EXPANDABLE, fill_search_node); + } + } else { + /* We've identified a file in CN */ + newnode(cn, searchresults[n], + trackname_transform("track", searchresults[n], "display"), + trackname_transform("track", searchresults[n], "sort"), + 0/*flags*/, 0/*fill*/); + } + } + updated_node(cn, 1); +} + +/* This is called from eclient with a (possibly empty) list of search results, + * and also from initiate_seatch with an always empty list to indicate that + * we're not searching for anything in particular. */ +static void search_completed(void attribute((unused)) *v, + int nvec, char **vec) { + struct choosenode *cn; + int n; + const char *dir; + + search_in_flight = 0; + if(search_obsolete) { + /* This search has been obsoleted by user input since it started. + * Therefore we throw away the result and search again. */ + search_obsolete = 0; + initiate_search(); + } else { + if(nvec) { + /* We will replace the choose tree with a tree structured view of search + * results. First we must disabled the choose tree's widgets. */ + delete_widgets(root); + /* Put the tracks into order, grouped by directory. They'll probably + * come back this way anyway in current versions of the server, but it's + * cheap not to rely on it (compared with the massive effort we expend + * later on) */ + qsort(vec, nvec, sizeof(char *), compare_track_for_qsort); + searchresults = vec; + nsearchresults = nvec; + cn = root = newnode(0/*parent*/, "", "Search results", "", + CN_EXPANDABLE|CN_EXPANDED, fill_search_node); + /* Construct the initial tree. We do this in a single pass and expand + * everything, so you can actually see your search results. */ + for(n = 0; n < nsearchresults; ++n) { + /* Firstly we might need to go up a few directories to each an ancestor + * of this track */ + while(!is_descendant(cn->path, searchresults[n])) { + /* We report the update on each node the last time we see it (With + * display=0, the main purpose of this is to get the order of the + * children right.) */ + updated_node(cn, 0); + cn = cn->parent; + } + /* Secondly we might need to insert some new directories */ + while(!is_child(cn->path, searchresults[n])) { + /* Figure out the subdirectory */ + dir = xstrndup(searchresults[n], + strchr(searchresults[n] + strlen(cn->path) + 1, + '/') - searchresults[n]); + cn = newnode(cn, dir, + trackname_transform("dir", dir, "display"), + trackname_transform("dir", dir, "sort"), + CN_EXPANDABLE|CN_EXPANDED, fill_search_node); + } + /* Finally we can insert the track as a child of the current + * directory */ + newnode(cn, searchresults[n], + trackname_transform("track", searchresults[n], "display"), + trackname_transform("track", searchresults[n], "sort"), + 0/*flags*/, 0/*fill*/); + } + while(cn) { + /* Update all the nodes back up to the root */ + updated_node(cn, 0); + cn = cn->parent; + } + /* Now it's worth displaying the tree */ + redisplay_tree(); + } else if(root != realroot) { + delete_widgets(root); + root = realroot; + redisplay_tree(); + } + } +} + +static void initiate_search(void) { + char *terms, *e; + + /* Find out what the user is after */ + terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(searchentry))); + /* Strip leading and trailing space */ + while(*terms == ' ') ++terms; + e = terms + strlen(terms); + while(e > terms && e[-1] == ' ') --e; + *e = 0; + /* If a search is already underway then mark it as obsolete. We'll revisit + * when it returns. */ + if(search_in_flight) { + search_obsolete = 1; + return; + } + if(*terms) { + /* There's still something left. Initiate the search. */ + if(disorder_eclient_search(client, search_completed, terms, 0)) { + /* The search terms are bad! We treat this as if there were no search + * terms at all. Some kind of feedback would be handy. */ + fprintf(stderr, "bad terms [%s]\n", terms); /* TODO */ + search_completed(0, 0, 0); + } else { + search_in_flight = 1; + } + } else { + /* No search terms - we want to see all tracks */ + search_completed(0, 0, 0); + } +} + +/* Called when the cancel search button is clicked */ +static void clearsearch_clicked(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_entry_set_text(GTK_ENTRY(searchentry), ""); +} + +/* Display functions ------------------------------------------------------- */ + +/* Delete all the widgets in the tree */ +static void delete_widgets(struct choosenode *cn) { + int n; + + if(cn->container) { + gtk_widget_destroy(cn->container); + cn->container = 0; + } + for(n = 0; n < cn->children.nvec; ++n) + delete_widgets(cn->children.vec[n]); + cn->flags &= ~(CN_DISPLAYED|CN_SELECTED); + files_selected = 0; +} + +/* Update the display */ +static void redisplay_tree(void) { + struct displaydata d; + guint oldwidth, oldheight; + + D(("redisplay_tree")); + /* We'll count these up empirically each time */ + files_selected = 0; + files_visible = 0; + /* Correct the layout and find out how much space it uses */ + d = display_tree(root, 0, 0); + /* We must set the total size or scrolling will not work (it wouldn't be hard + * for GtkLayout to figure it out for itself but presumably you're supposed + * to be able to have widgets off the edge of the layuot.) + * + * There is a problem: if we shrink the size then the part of the screen that + * is outside the new size but inside the old one is not updated. I think + * this is arguably bug in GTK+ but it's easy to force a redraw if this + * region is nonempty. + */ + gtk_layout_get_size(GTK_LAYOUT(chooselayout), &oldwidth, &oldheight); + if(oldwidth > d.width || oldheight > d.height) + gtk_widget_queue_draw(chooselayout); + gtk_layout_set_size(GTK_LAYOUT(chooselayout), d.width, d.height); + /* Notify the main menu of any recent changes */ + menu_update(-1); +} + +/* Make sure all displayed widgets from CN down exist and are in their proper + * place and return the vertical space used. */ +static struct displaydata display_tree(struct choosenode *cn, int x, int y) { + int n, aw; + GtkRequisition req; + struct displaydata d, cd; + GdkPixbuf *pb; + + D(("display_tree %s %d,%d", cn->path, x, y)); + + /* An expandable item contains an arrow and a text label. When you press the + * button it flips its expand state. + * + * A non-expandable item has just a text label and no arrow. + */ + if(!cn->container) { + /* Widgets need to be created */ + cn->hbox = gtk_hbox_new(FALSE, 1); + if(cn->flags & CN_EXPANDABLE) { + cn->arrow = gtk_arrow_new(cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN + : GTK_ARROW_RIGHT, + GTK_SHADOW_NONE); + cn->marker = 0; + } else { + cn->arrow = 0; + if((pb = find_image("notes.png"))) + cn->marker = gtk_image_new_from_pixbuf(pb); + } + cn->label = gtk_label_new(cn->display); + if(cn->arrow) + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->arrow); + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->label); + if(cn->marker) + gtk_container_add(GTK_CONTAINER(cn->hbox), cn->marker); + cn->container = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(cn->container), cn->hbox); + g_signal_connect(cn->container, "button-release-event", + G_CALLBACK(clicked_choosenode), cn); + g_signal_connect(cn->container, "button-press-event", + G_CALLBACK(clicked_choosenode), cn); + g_object_ref(cn->container); + gtk_widget_set_name(cn->label, "choose"); + gtk_widget_set_name(cn->container, "choose"); + /* Show everything by default */ + gtk_widget_show_all(cn->container); + } + assert(cn->container); + /* Make sure the icon is right */ + if(cn->flags & CN_EXPANDABLE) + gtk_arrow_set(GTK_ARROW(cn->arrow), + cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN : GTK_ARROW_RIGHT, + GTK_SHADOW_NONE); + else if(cn->marker) + /* Make sure the queued marker is right */ + /* TODO: doesn't always work */ + (queued(cn->path) ? gtk_widget_show : gtk_widget_hide)(cn->marker); + /* Put the widget in the right place */ + if(cn->flags & CN_DISPLAYED) + gtk_layout_move(GTK_LAYOUT(chooselayout), cn->container, x, y); + else { + gtk_layout_put(GTK_LAYOUT(chooselayout), cn->container, x, y); + cn->flags |= CN_DISPLAYED; + } + /* Set the widget's selection status */ + if(!(cn->flags & CN_EXPANDABLE)) + display_selection(cn); + /* Find the size used so we can get vertical positioning right. */ + gtk_widget_size_request(cn->container, &req); + d.width = x + req.width; + d.height = y + req.height; + if(cn->flags & CN_EXPANDED) { + /* We'll offset children by the size of the arrow whatever it might be. */ + assert(cn->arrow); + gtk_widget_size_request(cn->arrow, &req); + aw = req.width; + for(n = 0; n < cn->children.nvec; ++n) { + cd = display_tree(cn->children.vec[n], x + aw, d.height); + if(cd.width > d.width) + d.width = cd.width; + d.height = cd.height; + } + } else { + for(n = 0; n < cn->children.nvec; ++n) + undisplay_tree(cn->children.vec[n]); + } + if(!(cn->flags & CN_EXPANDABLE)) { + ++files_visible; + if(cn->flags & CN_SELECTED) + ++files_selected; + } + /* report back how much space we used */ + D(("display_tree %s %d,%d total size %dx%d", cn->path, x, y, + d.width, d.height)); + return d; +} + +/* Remove widgets for newly hidden nodes */ +static void undisplay_tree(struct choosenode *cn) { + int n; + + D(("undisplay_tree %s", cn->path)); + /* Remove this widget from the display */ + if(cn->flags & CN_DISPLAYED) { + gtk_container_remove(GTK_CONTAINER(chooselayout), cn->container); + cn->flags ^= CN_DISPLAYED; + } + /* Remove children too */ + for(n = 0; n < cn->children.nvec; ++n) + undisplay_tree(cn->children.vec[n]); +} + +/* Selection --------------------------------------------------------------- */ + +static void display_selection(struct choosenode *cn) { + /* Need foreground and background colors */ + gtk_widget_set_state(cn->label, (cn->flags & CN_SELECTED + ? GTK_STATE_SELECTED : GTK_STATE_NORMAL)); + gtk_widget_set_state(cn->container, (cn->flags & CN_SELECTED + ? GTK_STATE_SELECTED : GTK_STATE_NORMAL)); +} + +/* Set the selection state of a widget. Directories can never be selected, we + * just ignore attempts to do so. */ +static void set_selection(struct choosenode *cn, int selected) { + unsigned f = selected ? CN_SELECTED : 0; + + D(("set_selection %d %s", selected, cn->path)); + if(!(cn->flags & CN_EXPANDABLE) && (cn->flags & CN_SELECTED) != f) { + cn->flags ^= CN_SELECTED; + /* Maintain selection count */ + if(selected) + ++files_selected; + else + --files_selected; + display_selection(cn); + /* Update main menu sensitivity */ + menu_update(-1); + } +} + +/* Recursively clear all selection bits from CN down */ +static void clear_selection(struct choosenode *cn) { + int n; + + set_selection(cn, 0); + for(n = 0; n < cn->children.nvec; ++n) + clear_selection(cn->children.vec[n]); +} + +/* User actions ------------------------------------------------------------ */ + +/* Clicked on something */ +static void clicked_choosenode(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data) { + struct choosenode *cn = user_data; + int ind, last_ind, n; + + D(("clicked_choosenode %s", cn->path)); + if(event->type == GDK_BUTTON_RELEASE + && event->button == 1) { + /* Left click */ + if(cn->flags & CN_EXPANDABLE) { + /* This is a directory. Flip its expansion status. */ + if(cn->flags & CN_EXPANDED) + contract_node(cn); + else + expand_node(cn); + last_click = 0; + } else { + /* This is a file. Adjust selection status */ + /* TODO the basic logic here is essentially the same as that in queue.c. + * Can we share code at all? */ + switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + case 0: + clear_selection(root); + set_selection(cn, 1); + last_click = cn; + break; + case GDK_CONTROL_MASK: + set_selection(cn, !(cn->flags & CN_SELECTED)); + last_click = cn; + break; + case GDK_SHIFT_MASK: + case GDK_SHIFT_MASK|GDK_CONTROL_MASK: + if(last_click && last_click->parent == cn->parent) { + /* Figure out where the current and last clicks are in the list */ + ind = last_ind = -1; + for(n = 0; n < cn->parent->children.nvec; ++n) { + if(cn->parent->children.vec[n] == cn) + ind = n; + if(cn->parent->children.vec[n] == last_click) + last_ind = n; + } + /* Test shouldn't ever fail, but still */ + if(ind >= 0 && last_ind >= 0) { + if(!(event->state & GDK_CONTROL_MASK)) { + for(n = 0; n < cn->parent->children.nvec; ++n) + set_selection(cn->parent->children.vec[n], 0); + } + if(ind > last_ind) + for(n = last_ind; n <= ind; ++n) + set_selection(cn->parent->children.vec[n], 1); + else + for(n = ind; n <= last_ind; ++n) + set_selection(cn->parent->children.vec[n], 1); + if(event->state & GDK_CONTROL_MASK) + last_click = cn; + } + } + break; + } + } + } else if(event->type == GDK_BUTTON_RELEASE + && event->button == 2) { + /* Middle click - play the pointed track */ + if(!(cn->flags & CN_EXPANDABLE)) { + clear_selection(root); + set_selection(cn, 1); + gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue"); + disorder_eclient_play(client, cn->path, 0, 0); + last_click = 0; + } + } else if(event->type == GDK_BUTTON_PRESS + && event->button == 3) { + /* Right click. Pop up a menu. */ + /* If the current file isn't selected, switch the selection to just that. + * (If we're looking at a directory then leave the selection alone.) */ + if(!(cn->flags & CN_EXPANDABLE) && !(cn->flags & CN_SELECTED)) { + clear_selection(root); + set_selection(cn, 1); + last_click = cn; + } + /* Set the item sensitivity and callbacks */ + for(n = 0; n < NMENUITEMS; ++n) { + if(menuitems[n].handlerid) + g_signal_handler_disconnect(menuitems[n].w, + menuitems[n].handlerid); + gtk_widget_set_sensitive(menuitems[n].w, + menuitems[n].sensitive(cn)); + menuitems[n].handlerid = g_signal_connect + (menuitems[n].w, "activate", G_CALLBACK(menuitems[n].activate), cn); + } + /* Pop up the menu */ + gtk_widget_show_all(menu); + gtk_menu_popup(GTK_MENU(menu), 0, 0, 0, 0, + event->button, event->time); + } +} + +static void searchentry_changed(GtkEditable attribute((unused)) *editable, + gpointer attribute((unused)) user_data) { + initiate_search(); +} + +/* Menu items -------------------------------------------------------------- */ + +static void recurse_selected(struct choosenode *cn, struct vector *v) { + int n; + + if(cn->flags & CN_EXPANDABLE) { + if(cn->flags & CN_EXPANDED) + for(n = 0; n < cn->children.nvec; ++n) + recurse_selected(cn->children.vec[n], v); + } else { + if((cn->flags & CN_SELECTED) && cn->path) + vector_append(v, (char *)cn->path); + } +} + +static char **gather_selected(int *ntracks) { + struct vector v; + + vector_init(&v); + recurse_selected(root, &v); + vector_terminate(&v); + if(ntracks) *ntracks = v.nvec; + return v.vec; +} + +static void activate_play(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + char **tracks = gather_selected(0); + int n; + + gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue"); + for(n = 0; tracks[n]; ++n) + disorder_eclient_play(client, tracks[n], 0, 0); +} + +static void activate_remove(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + /* TODO remove all selected tracks */ +} + +static void activate_properties(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + int ntracks; + char **tracks = gather_selected(&ntracks); + + properties(ntracks, tracks); +} + +static gboolean sensitive_play(struct choosenode attribute((unused)) *cn) { + return !!files_selected; +} + +static gboolean sensitive_remove(struct choosenode attribute((unused)) *cn) { + return FALSE; /* not implemented yet */ +} + +static gboolean sensitive_properties(struct choosenode attribute((unused)) *cn) { + return !!files_selected; +} + +/* Main menu plumbing ------------------------------------------------------ */ + +static int choose_properties_sensitive(GtkWidget attribute((unused)) *w) { + return !!files_selected; +} + +static int choose_selectall_sensitive(GtkWidget attribute((unused)) *w) { + return FALSE; /* TODO */ +} + +static void choose_properties_activate(GtkWidget attribute((unused)) *w) { + activate_properties(0, 0); +} + +static void choose_selectall_activate(GtkWidget attribute((unused)) *w) { + /* TODO */ +} + +static const struct tabtype tabtype_choose = { + choose_properties_sensitive, + choose_selectall_sensitive, + choose_properties_activate, + choose_selectall_activate, +}; + +/* Public entry points ----------------------------------------------------- */ + +/* Create a track choice widget */ +GtkWidget *choose_widget(void) { + int n; + GtkWidget *scrolled; + GtkWidget *vbox, *hbox, *clearsearch; + + /* + * +--vbox-------------------------------------------------------+ + * | +-hbox----------------------------------------------------+ | + * | | searchentry | clearsearch | | + * | +---------------------------------------------------------+ | + * | +-scrolled------------------------------------------------+ | + * | | +-chooselayout------------------------------------++--+ | | + * | | | Tree structure is manually layed out in here ||^^| | | + * | | | || | | | + * | | | || | | | + * | | | || | | | + * | | | ||vv| | | + * | | +-------------------------------------------------++--+ | | + * | | +-------------------------------------------------+ | | + * | | |< >| | | + * | | +-------------------------------------------------+ | | + * | +---------------------------------------------------------+ | + * +-------------------------------------------------------------+ + */ + + /* Text entry box for search terms */ + searchentry = gtk_entry_new(); + g_signal_connect(searchentry, "changed", G_CALLBACK(searchentry_changed), 0); + + /* Cancel button to clear the search */ + clearsearch = gtk_button_new_from_stock(GTK_STOCK_CANCEL); + g_signal_connect(G_OBJECT(clearsearch), "clicked", + G_CALLBACK(clearsearch_clicked), 0); + + /* hbox packs the search box and the cancel button together on a line */ + hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/); + gtk_box_pack_start(GTK_BOX(hbox), searchentry, + TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/); + gtk_box_pack_end(GTK_BOX(hbox), clearsearch, + FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/); + + /* chooselayout contains the currently visible subset of the track + * namespace */ + chooselayout = gtk_layout_new(0, 0); + root = newnode(0/*parent*/, "<root>", "All files", "", + CN_EXPANDABLE, fill_root_node); + realroot = root; + expand_node(root); /* will call redisplay_tree */ + /* Create the popup menu */ + menu = gtk_menu_new(); + g_signal_connect(menu, "destroy", G_CALLBACK(gtk_widget_destroyed), &menu); + for(n = 0; n < NMENUITEMS; ++n) { + menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name); + gtk_menu_attach(GTK_MENU(menu), menuitems[n].w, 0, 1, n, n + 1); + } + /* The layout is scrollable */ + scrolled = scroll_widget(GTK_WIDGET(chooselayout), "choose"); + + /* The scrollable layout and the search hbox go together in a vbox */ + vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/); + gtk_box_pack_start(GTK_BOX(vbox), hbox, + FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/); + gtk_box_pack_end(GTK_BOX(vbox), scrolled, + TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/); + + g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_choose); + return vbox; +} + +/* Called when something we care about here might have changed */ +void choose_update(void) { + redisplay_tree(); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:A5KX3X9SR8Pl57VRLSnCng */ diff --git a/disobedience/client.c b/disobedience/client.c new file mode 100644 index 0000000..d03e9de --- /dev/null +++ b/disobedience/client.c @@ -0,0 +1,176 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* GSource subclass for disorder_eclient */ +struct eclient_source { + GSource gsource; + disorder_eclient *client; + time_t last_poll; + GPollFD pollfd; +}; + +/* Called before FDs are polled to choose a timeout. We ask for a 3s + * timeout and every 10s or so we force a dispatch. */ +static gboolean gtkclient_prepare(GSource *source, + gint *timeout) { + const struct eclient_source *esource = (struct eclient_source *)source; + D(("gtkclient_prepare")); + if(time(0) > esource->last_poll + 10) + return TRUE; /* timed out */ + *timeout = 3000/*milliseconds*/; + return FALSE; /* please poll */ +} + +/* Check whether we're ready to dispatch. */ +static gboolean gtkclient_check(GSource *source) { + const struct eclient_source *esource = (struct eclient_source *)source; + D(("gtkclient_check fd=%d events=%x revents=%x", + esource->pollfd.fd, esource->pollfd.events, esource->pollfd.revents)); + return esource->pollfd.fd != -1 && esource->pollfd.revents != 0; +} + +/* Dispatch */ +static gboolean gtkclient_dispatch(GSource *source, + GSourceFunc attribute((unused)) callback, + gpointer attribute((unused)) user_data) { + struct eclient_source *esource = (struct eclient_source *)source; + unsigned revents, mode; + + D(("gtkclient_dispatch")); + revents = esource->pollfd.revents & esource->pollfd.events; + mode = 0; + if(revents & (G_IO_IN|G_IO_HUP|G_IO_ERR)) + mode |= DISORDER_POLL_READ; + if(revents & (G_IO_OUT|G_IO_HUP|G_IO_ERR)) + mode |= DISORDER_POLL_WRITE; + time(&esource->last_poll); + disorder_eclient_polled(esource->client, mode); + return TRUE; /* ??? not documented */ +} + +/* Table of callbacks for GSource subclass */ +static const GSourceFuncs sourcefuncs = { + gtkclient_prepare, + gtkclient_check, + gtkclient_dispatch, + 0, + 0, + 0, +}; + +/* Tell the mainloop what we need */ +static void gtkclient_poll(void *u, + disorder_eclient attribute((unused)) *c, + int fd, unsigned mode) { + struct eclient_source *esource = u; + GSource *source = u; + + D(("gtkclient_poll fd=%d mode=%x", fd, mode)); + /* deconfigure the source if currently configured */ + if(esource->pollfd.fd != -1) { + D(("calling g_source_remove_poll")); + g_source_remove_poll(source, &esource->pollfd); + esource->pollfd.fd = -1; + esource->pollfd.events = 0; + } + /* install new settings */ + if(fd != -1 && mode) { + esource->pollfd.fd = fd; + esource->pollfd.events = 0; + if(mode & DISORDER_POLL_READ) + esource->pollfd.events |= G_IO_IN | G_IO_HUP | G_IO_ERR; + if(mode & DISORDER_POLL_WRITE) + esource->pollfd.events |= G_IO_OUT | G_IO_ERR; + /* reconfigure the source */ + D(("calling g_source_add_poll")); + g_source_add_poll(source, &esource->pollfd); + } +} + +/* Report a communication-level error. It will be automatically retried. */ +static void gtkclient_comms_error(void attribute((unused)) *u, + const char *msg) { + D(("gtkclient_comms_error %s", msg)); + gtk_label_set_text(GTK_LABEL(report_label), msg); +} + +/* Report a protocol error. It will not be retried. We offer a callback to + * the submitter of the original command and if none is supplied we pop up an + * error box. */ +static void gtkclient_protocol_error(void attribute((unused)) *u, + void *v, + int code, + const char *msg) { + struct callbackdata *cbd = v; + + D(("gtkclient_protocol_error %s", msg)); + if(cbd && cbd->onerror) + cbd->onerror(cbd, code, msg); + else + popup_protocol_error(code, msg); +} + +static void gtkclient_report(void attribute((unused)) *u, + const char *msg) { + if(!msg) + /* We're idle - clear the report line */ + gtk_label_set_text(GTK_LABEL(report_label), ""); +} + +void popup_protocol_error(int attribute((unused)) code, + const char *msg) { + gtk_label_set_text(GTK_LABEL(report_label), msg); + popup_error(msg); +} + +/* Table of eclient callbacks */ +static const disorder_eclient_callbacks gtkclient_callbacks = { + gtkclient_comms_error, + gtkclient_protocol_error, + gtkclient_poll, + gtkclient_report +}; + +/* Create an eclient using the GLib main loop */ +disorder_eclient *gtkclient(void) { + GSource *source; + struct eclient_source *esource; + + D(("gtkclient")); + source = g_source_new((GSourceFuncs *)&sourcefuncs, + sizeof (struct eclient_source)); + esource = (struct eclient_source *)source; + esource->pollfd.fd = -1; + esource->client = disorder_eclient_new(>kclient_callbacks, source); + g_source_attach(source, 0); + return esource->client; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:32Qju8BYS5FZvqbPHElgcg */ diff --git a/disobedience/control.c b/disobedience/control.c new file mode 100644 index 0000000..159ab5d --- /dev/null +++ b/disobedience/control.c @@ -0,0 +1,316 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Forward declartions ----------------------------------------------------- */ + +struct icon; + +static void update_pause(const struct icon *); +static void update_play(const struct icon *); +static void update_scratch(const struct icon *); +static void update_random_enable(const struct icon *); +static void update_random_disable(const struct icon *); +static void update_enable(const struct icon *); +static void update_disable(const struct icon *); +static void clicked_icon(GtkButton *, gpointer); + +static double left(double v, double b); +static double right(double v, double b); +static double volume(double l, double r); +static double balance(double l, double r); + +static void volume_adjusted(GtkAdjustment *a, gpointer user_data); +static gchar *format_volume(GtkScale *scale, gdouble value); +static gchar *format_balance(GtkScale *scale, gdouble value); + +/* Control bar ------------------------------------------------------------- */ + +static int suppress_set_volume; +/* Guard against feedback loop in volume control */ + +static struct icon { + const char *icon; + const char *tip; + void (*clicked)(GtkButton *button, gpointer userdata); + void (*update)(const struct icon *i); + int (*action)(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); + GtkWidget *button; +} icons[] = { + { "pause.png", "Pause playing track", clicked_icon, update_pause, + disorder_eclient_pause, 0 }, + { "play.png", "Resume playing track", clicked_icon, update_play, + disorder_eclient_resume, 0 }, + { "cross.png", "Cancel playing track", clicked_icon, update_scratch, + disorder_eclient_scratch_playing, 0 }, + { "random.png", "Enable random play", clicked_icon, update_random_enable, + disorder_eclient_random_enable, 0 }, + { "randomcross.png", "Disable random play", clicked_icon, update_random_disable, + disorder_eclient_random_disable, 0 }, + { "notes.png", "Enable play", clicked_icon, update_enable, + disorder_eclient_enable, 0 }, + { "notescross.png", "Disable play", clicked_icon, update_disable, + disorder_eclient_disable, 0 }, +}; +#define NICONS (int)(sizeof icons / sizeof *icons) + +GtkAdjustment *volume_adj, *balance_adj; + +/* Create the control bar */ + GtkWidget *control_widget(void) { + GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox; + GtkWidget *content; + GdkPixbuf *pb; + GtkWidget *v, *b; + GtkTooltips *tips = gtk_tooltips_new(); + int n; + + D(("control_widget")); + for(n = 0; n < NICONS; ++n) { + icons[n].button = gtk_button_new(); + if((pb = find_image(icons[n].icon))) + content = gtk_image_new_from_pixbuf(pb); + else + content = gtk_label_new(icons[n].icon); + gtk_container_add(GTK_CONTAINER(icons[n].button), content); + gtk_tooltips_set_tip(tips, icons[n].button, icons[n].tip, ""); + g_signal_connect(G_OBJECT(icons[n].button), "clicked", + G_CALLBACK(icons[n].clicked), &icons[n]); + /* pop the icon in a vbox so it doesn't get vertically stretch if there are + * taller things in the control bar */ + vbox = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), icons[n].button, TRUE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0); + } + /* create the adjustments for the volume control */ + volume_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, goesupto, + goesupto / 20, goesupto / 20, + 0)); + balance_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, -1, 1, + 0.2, 0.2, 0)); + /* the volume control */ + v = gtk_hscale_new(volume_adj); + b = gtk_hscale_new(balance_adj); + gtk_scale_set_digits(GTK_SCALE(v), 10); + gtk_scale_set_digits(GTK_SCALE(b), 10); + gtk_widget_set_size_request(v, 192, -1); + gtk_widget_set_size_request(b, 192, -1); + gtk_tooltips_set_tip(tips, v, "Volume", ""); + gtk_tooltips_set_tip(tips, b, "Balance", ""); + gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(hbox), b, FALSE, TRUE, 0); + /* space updates rather than hammering the server */ + gtk_range_set_update_policy(GTK_RANGE(v), GTK_UPDATE_DELAYED); + gtk_range_set_update_policy(GTK_RANGE(b), GTK_UPDATE_DELAYED); + /* notice when the adjustments are changed */ + g_signal_connect(G_OBJECT(volume_adj), "value-changed", + G_CALLBACK(volume_adjusted), 0); + g_signal_connect(G_OBJECT(balance_adj), "value-changed", + G_CALLBACK(volume_adjusted), 0); + /* format the volume/balance values ourselves */ + g_signal_connect(G_OBJECT(v), "format-value", + G_CALLBACK(format_volume), 0); + g_signal_connect(G_OBJECT(b), "format-value", + G_CALLBACK(format_balance), 0); + return hbox; +} + +/* Update the control bar after some kind of state change */ +void control_update(void) { + int n; + double l, r; + + D(("control_update")); + for(n = 0; n < NICONS; ++n) + icons[n].update(&icons[n]); + l = volume_l / 100.0; + r = volume_r / 100.0; + ++suppress_set_volume;; + gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto); + gtk_adjustment_set_value(balance_adj, balance(l, r)); + --suppress_set_volume; +} + +static void update_icon(GtkWidget *button, + int visible, int attribute((unused)) usable) { + (visible ? gtk_widget_show : gtk_widget_hide)(button); + /* TODO: show usability */ +} + +static void update_pause(const struct icon *icon) { + int visible = !(last_state & DISORDER_TRACK_PAUSED); + int usable = playing; /* TODO: might be a lie */ + update_icon(icon->button, visible, usable); +} + +static void update_play(const struct icon *icon) { + int visible = !!(last_state & DISORDER_TRACK_PAUSED); + int usable = playing; + update_icon(icon->button, visible, usable); +} + +static void update_scratch(const struct icon *icon) { + int visible = 1; + int usable = playing; + update_icon(icon->button, visible, usable); +} + +static void update_random_enable(const struct icon *icon) { + int visible = !(last_state & DISORDER_RANDOM_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_random_disable(const struct icon *icon) { + int visible = !!(last_state & DISORDER_RANDOM_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_enable(const struct icon *icon) { + int visible = !(last_state & DISORDER_PLAYING_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void update_disable(const struct icon *icon) { + int visible = !!(last_state & DISORDER_PLAYING_ENABLED); + int usable = 1; + update_icon(icon->button, visible, usable); +} + +static void clicked_icon(GtkButton attribute((unused)) *button, + gpointer userdata) { + const struct icon *icon = userdata; + + icon->action(client, 0, 0); +} + +static void volume_adjusted(GtkAdjustment attribute((unused)) *a, + gpointer attribute((unused)) user_data) { + double v = gtk_adjustment_get_value(volume_adj) / goesupto; + double b = gtk_adjustment_get_value(balance_adj); + + if(suppress_set_volume) + /* This is the result of an update from the server, not a change from the + * user. Don't feedback! */ + return; + D(("volume_adjusted")); + /* force to 'stereotypical' values */ + v = nearbyint(100 * v) / 100; + b = nearbyint(5 * b) / 5; + /* Set the volume. We don't want a reply, we'll get the actual new volume + * from the log. */ + disorder_eclient_volume(client, 0, + nearbyint(left(v, b) * 100), + nearbyint(right(v, b) * 100), + 0); +} + +/* Called to format the volume value */ +static gchar *format_volume(GtkScale attribute((unused)) *scale, + gdouble value) { + char s[32]; + + snprintf(s, sizeof s, "%.1f", (double)value); + return xstrdup(s); +} + +/* Called to format the balance value. */ +static gchar *format_balance(GtkScale attribute((unused)) *scale, + gdouble value) { + char s[32]; + + if(fabs(value) < 0.1) + return xstrdup("0"); + snprintf(s, sizeof s, "%+.1f", (double)value); + return xstrdup(s); +} + +/* Volume mapping. We consider left, right, volume to be in [0,1] + * and balance to be in [-1,1]. + * + * First, we just have volume = max(left, right). + * + * Balance we consider to linearly represent the amount by which the quieter + * channel differs from the louder. In detail: + * + * if right > left then balance > 0: + * balance = 0 => left = right (as an endpoint, not an instance) + * balance = 1 => left = 0 + * fitting to linear, left = right * (1 - balance) + * so balance = 1 - left / right + * (right > left => right > 0 so no division by 0.) + * + * if left > right then balance < 0: + * balance = 0 => right = left (same caveat as above) + * balance = -1 => right = 0 + * again fitting to linear, right = left * (1 + balance) + * so balance = right / left - 1 + * (left > right => left > 0 so no division by 0.) + * + * if left = right then we just have balance = 0. + * + * Thanks to Clive and Andrew. + */ + +static double max(double x, double y) { + return x > y ? x : y; +} + +static double left(double v, double b) { + if(b > 0) /* volume = right */ + return v * (1 - b); + else /* volume = left */ + return v; +} + +static double right(double v, double b) { + if(b > 0) /* volume = right */ + return v; + else /* volume = left */ + return v * (1 + b); +} + +static double volume(double l, double r) { + return max(l, r); +} + +static double balance(double l, double r) { + if(l > r) + return r / l - 1; + else if(r > l) + return 1 - l / r; + else /* left = right */ + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:IEbGnYlX8cqOFjY1EXlXBA */ diff --git a/disobedience/disobedience.c b/disobedience/disobedience.c new file mode 100644 index 0000000..dc8c27e --- /dev/null +++ b/disobedience/disobedience.c @@ -0,0 +1,379 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +#include <getopt.h> +#include <locale.h> + +/* Apologies for the numerous de-consting casts, but GLib et al do not seem to + * have heard of const. */ + +#include "style.h" /* generated style */ + +/* Functions --------------------------------------------------------------- */ + +static void log_connected(void *v); +static void log_completed(void *v, const char *track); +static void log_failed(void *v, const char *track, const char *status); +static void log_moved(void *v, const char *user); +static void log_playing(void *v, const char *track, const char *user); +static void log_queue(void *v, struct queue_entry *q); +static void log_recent_added(void *v, struct queue_entry *q); +static void log_recent_removed(void *v, const char *id); +static void log_removed(void *v, const char *id, const char *user); +static void log_scratched(void *v, const char *track, const char *user); +static void log_state(void *v, unsigned long state); +static void log_volume(void *v, int l, int r); + +/* Variables --------------------------------------------------------------- */ + +GMainLoop *mainloop; /* event loop */ +GtkWidget *toplevel; /* top level window */ +GtkWidget *report_label; /* label for progress indicator */ +GtkWidget *tabs; /* main tabs */ + +disorder_eclient *client; /* main client */ + +unsigned long last_state; /* last reported state */ +int playing; /* true if playing some track */ +int volume_l, volume_r; /* volume */ +double goesupto = 10; /* volume upper bound */ +int choosealpha; /* break up choose by letter */ + +static const GMemVTable glib_memvtable = { + xmalloc, + xrealloc, + xfree, + 0, /* calloc */ + 0, /* try_malloc */ + 0 /* try_realloc */ +}; + +static const disorder_eclient_log_callbacks gdisorder_log_callbacks = { + log_connected, + log_completed, + log_failed, + log_moved, + log_playing, + log_queue, + log_recent_added, + log_recent_removed, + log_removed, + log_scratched, + log_state, + log_volume +}; + +/* Window creation --------------------------------------------------------- */ + +/* Note that all the client operations kicked off from here will only complete + * after we've entered the event loop. */ + +static gboolean delete_event(GtkWidget attribute((unused)) *widget, + GdkEvent attribute((unused)) *event, + gpointer attribute((unused)) data) { + D(("delete_event")); + exit(0); /* die immediately */ +} + +/* Called when the current tab is switched. */ +static void tab_switched(GtkNotebook attribute((unused)) *notebook, + GtkNotebookPage attribute((unused)) *page, + guint page_num, + gpointer attribute((unused)) user_data) { + menu_update(page_num); +} + +static GtkWidget *report_box(void) { + GtkWidget *vbox = gtk_vbox_new(FALSE, 0); + + report_label = gtk_label_new(""); + gtk_label_set_ellipsize(GTK_LABEL(report_label), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment(GTK_MISC(report_label), 0, 0); + gtk_container_add(GTK_CONTAINER(vbox), gtk_hseparator_new()); + gtk_container_add(GTK_CONTAINER(vbox), report_label); + return vbox; +} + +static GtkWidget *notebook(void) { + tabs = gtk_notebook_new(); + g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(), + gtk_label_new("Queue")); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(), + gtk_label_new("Recent")); + gtk_notebook_append_page(GTK_NOTEBOOK(tabs), choose_widget(), + gtk_label_new("Choose")); + return tabs; +} + +static void make_toplevel_window(void) { + GtkWidget *const vbox = gtk_vbox_new(FALSE, 1); + GtkWidget *const rb = report_box(); + + D(("top_window")); + toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL); + /* default size is too small */ + gtk_window_set_default_size(GTK_WINDOW(toplevel), 640, 480); + /* terminate on close */ + g_signal_connect(G_OBJECT(toplevel), "delete_event", + G_CALLBACK(delete_event), NULL); + /* lay out the window */ + gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience"); + gtk_container_add(GTK_CONTAINER(toplevel), vbox); + /* lay out the vbox */ + gtk_box_pack_start(GTK_BOX(vbox), + menubar(toplevel), + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_box_pack_start(GTK_BOX(vbox), + control_widget(), + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_container_add(GTK_CONTAINER(vbox), notebook()); + gtk_box_pack_end(GTK_BOX(vbox), + rb, + FALSE, /* expand */ + FALSE, /* fill */ + 0); + gtk_widget_set_name(toplevel, "disobedience"); +} + +/* State monitoring -------------------------------------------------------- */ + +static void all_update(void) { + playing_update(); + queue_update(); + recent_update(); + control_update(); +} + +static void log_connected(void attribute((unused)) *v) { + struct callbackdata *cbd; + + /* Don't know what we might have missed while disconnected so update + * everything. We get this at startup too and this is how we do the initial + * state fetch. */ + all_update(); + /* Re-get the volume */ + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + disorder_eclient_volume(client, log_volume, -1, -1, cbd); +} + +static void log_completed(void attribute((unused)) *v, + const char attribute((unused)) *track) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_failed(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *status) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_moved(void attribute((unused)) *v, + const char attribute((unused)) *user) { + queue_update(); +} + +static void log_playing(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *user) { + playing = 1; + playing_update(); + control_update(); + /* we get a log_removed() anyway so we don't need to update_queue() from + * here */ +} + +static void log_queue(void attribute((unused)) *v, + struct queue_entry attribute((unused)) *q) { + queue_update(); +} + +static void log_recent_added(void attribute((unused)) *v, + struct queue_entry attribute((unused)) *q) { + recent_update(); +} + +static void log_recent_removed(void attribute((unused)) *v, + const char attribute((unused)) *id) { + /* nothing - log_recent_added() will trigger the relevant update */ +} + +static void log_removed(void attribute((unused)) *v, + const char attribute((unused)) *id, + const char attribute((unused)) *user) { + queue_update(); +} + +static void log_scratched(void attribute((unused)) *v, + const char attribute((unused)) *track, + const char attribute((unused)) *user) { + playing = 0; + playing_update(); + control_update(); +} + +static void log_state(void attribute((unused)) *v, + unsigned long state) { + last_state = state; + control_update(); + /* If the track is paused or resume then the currently playing track is + * refetched so that we can continue to correctly calculate the played so-far + * field */ + playing_update(); +} + +static void log_volume(void attribute((unused)) *v, + int l, int r) { + if(volume_l != l || volume_r != r) { + volume_l = l; + volume_r = r; + control_update(); + } +} + +/* Called once every 10 minutes */ +static gboolean periodic(gpointer attribute((unused)) data) { + D(("periodic")); + /* Expire cached data */ + cache_expire(); + /* Update everything to be sure that the connection to the server hasn't + * mysteriously gone stale on us. */ + all_update(); + return TRUE; /* don't remove me */ +} + +static gboolean heartbeat(gpointer attribute((unused)) data) { + static struct timeval last; + struct timeval now; + double delta; + + xgettimeofday(&now, 0); + if(last.tv_sec) { + delta = (now.tv_sec + now.tv_sec / 1.0E6) + - (last.tv_sec + last.tv_sec / 1.0E6); + if(delta >= 1.0625) + fprintf(stderr, "%f: %fs between 1s heartbeats\n", + now.tv_sec + now.tv_sec / 1.0E6, + delta); + } + last = now; + return TRUE; +} + +/* main -------------------------------------------------------------------- */ + +static const struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "config", required_argument, 0, 'c' }, + { "tufnel", no_argument, 0, 't' }, + { "choosealpha", no_argument, 0, 'C' }, + { "debug", no_argument, 0, 'd' }, + { 0, 0, 0, 0 } +}; + +/* display usage message and terminate */ +static void help(void) { + xprintf("Disobedience - GUI client for DisOrder\n" + "\n" + "Usage:\n" + " disobedience [OPTIONS]\n" + "Options:\n" + " --help, -h Display usage message\n" + " --version, -V Display version number\n" + " --config PATH, -c PATH Set configuration file\n" + " --debug, -d Turn on debugging\n" + "\n" + "Also GTK+ options will work.\n"); + xfclose(stdout); + exit(0); +} + +/* display version number and terminate */ +static void version(void) { + xprintf("disorder version %s\n", disorder_version_string); + xfclose(stdout); + exit(0); +} + +int main(int argc, char **argv) { + int n; + disorder_eclient *logclient; + + mem_init(1); + if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + /* GLib sucks - not const-correct */ + g_mem_set_vtable((GMemVTable *)&glib_memvtable); + gtk_init(&argc, &argv); + gtk_rc_parse_string(style); + while((n = getopt_long(argc, argv, "hVc:dtH", options, 0)) >= 0) { + switch(n) { + case 'h': help(); + case 'V': version(); + case 'c': configfile = optarg; break; + case 'd': debugging = 1; break; + case 't': goesupto = 11; break; + case 'C': choosealpha = 1; break; /* not well tested any more */ + default: fatal(0, "invalid option"); + } + } + /* create the event loop */ + D(("create main loop")); + mainloop = g_main_loop_new(0, 0); + if(config_read()) fatal(0, "cannot read configuration"); + /* create the clients */ + client = gtkclient(); + logclient = gtkclient(); + disorder_eclient_log(logclient, &gdisorder_log_callbacks, 0); + /* periodic operations (e.g. expiring the cache) */ + g_timeout_add(600000/*milliseconds*/, periodic, 0); + /* The point of this is to try and get a handle on mysterious + * unresponsiveness. It's not very useful in production use. */ + if(0) + g_timeout_add(1000/*milliseconds*/, heartbeat, 0); + make_toplevel_window(); + /* reset styles now everything has its name */ + gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default())); + gtk_widget_show_all(toplevel); + D(("enter main loop")); + g_main_loop_run(mainloop); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:dJmEdDzrCQktkNJWAmdQAQ */ diff --git a/disobedience/disobedience.h b/disobedience/disobedience.h new file mode 100644 index 0000000..a63cf44 --- /dev/null +++ b/disobedience/disobedience.h @@ -0,0 +1,180 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DISOBEDIENCE_H +#define DISOBEDIENCE_H + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <time.h> +#include <string.h> +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <math.h> + +#include "mem.h" +#include "log.h" +#include "eclient.h" +#include "printf.h" +#include "cache.h" +#include "queue.h" +#include "printf.h" +#include "vector.h" +#include "trackname.h" +#include "syscalls.h" +#include "defs.h" +#include "configuration.h" +#include "hash.h" +#include "selection.h" + +#include <glib.h> +#include <gtk/gtk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +/* Types ------------------------------------------------------------------- */ + +struct queuelike; +struct choosenode; + +struct callbackdata { + void (*onerror)(struct callbackdata *cbd, + int code, + const char *msg); /* called on error */ + union { + const char *key; /* gtkqueue.c op_part_lookup */ + struct choosenode *choosenode; /* gtkchoose.c got_files/got_dirs */ + struct queuelike *ql; /* gtkqueue.c queuelike_completed */ + struct prefdata *f; /* properties.c */ + } u; +}; + +struct tabtype { + int (*properties_sensitive)(GtkWidget *tab); + int (*selectall_sensitive)(GtkWidget *tab); + void (*properties_activate)(GtkWidget *tab); + void (*selectall_activate)(GtkWidget *tab); +}; + +/* Variables --------------------------------------------------------------- */ + +extern GMainLoop *mainloop; +extern GtkWidget *toplevel; /* top level window */ +extern GtkWidget *report_label; /* label for progress indicator */ +extern GtkWidget *tabs; /* main tabs */ +extern disorder_eclient *client; /* main client */ + +extern unsigned long last_state; /* last reported state */ +extern int playing; /* true if playing some track */ +extern int volume_l, volume_r; /* current volume */ +extern double goesupto; /* volume upper bound */ +extern int choosealpha; /* break up choose by letter */ + +/* Functions --------------------------------------------------------------- */ + +disorder_eclient *gtkclient(void); +/* Configure C for use in GTK+ programs */ + +void popup_protocol_error(int code, + const char *msg); +/* Report an error */ + +void properties(int ntracks, char **tracks); +/* Pop up a properties window for a list of tracks */ + +GtkWidget *scroll_widget(GtkWidget *child, const char *name); +/* Wrap a widget up for scrolling */ + +GdkPixbuf *find_image(const char *name); +/* Get the pixbuf for an image. Returns a null pointer if it cannot be + * found. */ + +void popup_error(const char *msg); +/* Pop up an error message */ + + +/* Main menu */ + +GtkWidget *menubar(GtkWidget *w); +/* Create the menu bar */ + +void menu_update(int page); +/* Called whenever the main menu might need to change. PAGE is the current + * page if known or -1 otherwise. */ + + +/* Controls */ + +GtkWidget *control_widget(void); +/* Make the controls widget */ + +void control_update(void); +/* Called whenever we think the control widget needs changing */ + + +/* Queue/Recent */ + +GtkWidget *queue_widget(void); +GtkWidget *recent_widget(void); +/* Create widgets for displaying the queue and the recently played list */ + +void queue_update(void); +void recent_update(void); +/* Called whenever we think the queue or recent list might have chanegd */ + +void queue_select_all(struct queuelike *ql); +/* Select all on some queue */ + +void queue_properties(struct queuelike *ql); +/* Pop up properties of selected items in some queue */ + +void playing_update(void); +/* Called whenever we think the currently playing track might have changed */ + +int queued(const char *track); +/* Return nonzero iff TRACK is queued or playing */ + +void namepart_update(const char *track, + const char *context, + const char *part); +/* Called when a namepart might have changed */ + + +/* Choose */ + +GtkWidget *choose_widget(void); +/* Create a widget for choosing tracks */ + +void choose_update(void); +/* Called when we think the choose tree might need updating */ + +#endif /* DISOBEDIENCE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:5DbN8e67AvkhPmNqSQyZFQ */ diff --git a/disobedience/disobedience.rc b/disobedience/disobedience.rc new file mode 100644 index 0000000..ed2710c --- /dev/null +++ b/disobedience/disobedience.rc @@ -0,0 +1,78 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +# Default style for Disobedience +# +# See e.g. http://developer.gnome.org/doc/API/2.0/gtk/gtk-Resource-Files.html +# for syntax documentation. + +style "disobedience-default" +{ + bg[NORMAL] = { 1.0, 1.0, 1.0 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } + bg[PRELIGHT] = { 1.0, 1.0, 1.0 } + fg[PRELIGHT] = { 1.0, 1.0, 1.0 } +} + +style "disobedience-playing" +{ + bg[NORMAL] = { 0.875, 1.0, 0.875 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-even" +{ + bg[NORMAL] = { 1.0, 0.921875, 0.921875 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-odd" +{ + bg[NORMAL] = { 1.0, 1.0, 1.0 } + fg[NORMAL] = { 0.0, 0.0, 0.0 } +} + +style "disobedience-title" +{ + bg[NORMAL] = { 0.0, 0.0, 0.0 } + fg[NORMAL] = { 1.0, 1.0, 1.0 } + font_name = "Bold" +} + +style "disobedience-drag" +{ + bg[NORMAL] = { 0.4, 0.4, 0.4 } + fg[NORMAL] = { 1.0, 1.0, 1.0 } +} + +# The main tabs +widget "disobedience.*.choose" style : application "disobedience-default" +widget "disobedience.*.queue" style : application "disobedience-default" +widget "disobedience.*.recent" style : application "disobedience-default" + +# Drag target +widget "disobedience.*.queue-drag" style : application "disobedience-drag" + +# Rows in the queue/recent tabs +widget "disobedience.*.row-playing" style : application "disobedience-playing" +widget "disobedience.*.row-even" style : application "disobedience-even" +widget "disobedience.*.row-odd" style : application "disobedience-odd" +widget "disobedience.*.row-title" style : application "disobedience-title" +# arch-tag:kFBxSkTwM9fRlwC/NTqR4A diff --git a/disobedience/menu.c b/disobedience/menu.c new file mode 100644 index 0000000..6eaea06 --- /dev/null +++ b/disobedience/menu.c @@ -0,0 +1,150 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +static GtkWidget *selectall_widget; +static GtkWidget *properties_widget; + +static void about_popup_got_version(void *v, const char *value); + +static void quit_program(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + D(("quit_program")); + exit(0); +} + +/* TODO can we have a single parameterized callback for all these */ +static void select_all(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs))); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + t->selectall_activate(tab); +} + +static void properties_item(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs))); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + t->properties_activate(tab); +} + +void menu_update(int page) { + GtkWidget *tab = gtk_notebook_get_nth_page + (GTK_NOTEBOOK(tabs), + page < 0 ? gtk_notebook_current_page(GTK_NOTEBOOK(tabs)) : page); + const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + + assert(t != 0); + gtk_widget_set_sensitive(properties_widget, + t->properties_sensitive(tab)); + gtk_widget_set_sensitive(selectall_widget, + t->selectall_sensitive(tab)); +} + +static void about_popup(gpointer attribute((unused)) callback_data, + guint attribute((unused)) callback_action, + GtkWidget attribute((unused)) *menu_item) { + D(("about_popup")); + + gtk_label_set_text(GTK_LABEL(report_label), "getting server version"); + disorder_eclient_version(client, + about_popup_got_version, + 0); +} + +static void about_popup_got_version(void attribute((unused)) *v, + const char *value) { + GtkWidget *w; + char *server_version_string; + + byte_xasprintf(&server_version_string, "Server version %s", value); + w = gtk_dialog_new_with_buttons("About DisOrder", + GTK_WINDOW(toplevel), + (GTK_DIALOG_MODAL + |GTK_DIALOG_DESTROY_WITH_PARENT), + GTK_STOCK_OK, + GTK_RESPONSE_ACCEPT, + (char *)NULL); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new("DisOrder client " VERSION)); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new(server_version_string)); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(w)->vbox), + gtk_label_new("(c) 2004-2006 Richard Kettlewell")); + gtk_widget_show_all(w); + gtk_dialog_run(GTK_DIALOG(w)); + gtk_widget_destroy(w); +} + +GtkWidget *menubar(GtkWidget *w) { + static const GtkItemFactoryEntry entries[] = { + { (char *)"/File", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/File/Quit", (char *)"<CTRL>Q", quit_program, 0, + (char *)"<StockItem>", GTK_STOCK_QUIT }, + { (char *)"/Edit", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/Edit/Select All", (char *)"<CTRL>A", select_all, 0, + 0, 0 }, + { (char *)"/Edit/Properties", 0, properties_item, 0, + 0, 0 }, + { (char *)"/Help", 0, 0, 0, (char *)"<Branch>", 0 }, + { (char *)"/Help/About DisOrder", 0, about_popup, 0, + (char *)"<StockItem>", GTK_STOCK_ABOUT }, + }; + + GtkItemFactory *itemfactory; + GtkAccelGroup *accel = gtk_accel_group_new(); + + D(("add_menubar")); + /* TODO: item factories are deprecated in favour of some XML thing */ + itemfactory = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<GdisorderMain>", + accel); + gtk_item_factory_create_items(itemfactory, + sizeof entries / sizeof *entries, + (GtkItemFactoryEntry *)entries, + 0); + gtk_window_add_accel_group(GTK_WINDOW(w), accel); + selectall_widget = gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>/Edit/Select All"); + properties_widget = gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>/Edit/Properties"); + assert(selectall_widget != 0); + assert(properties_widget != 0); + return gtk_item_factory_get_widget(itemfactory, + "<GdisorderMain>"); + /* menu bar had better not expand vertically if the window is too big */ +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:3vGhvsh3YABCyUS65pvmVA */ diff --git a/disobedience/misc.c b/disobedience/misc.c new file mode 100644 index 0000000..26222a9 --- /dev/null +++ b/disobedience/misc.c @@ -0,0 +1,97 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Miscellaneous GTK+ stuff ------------------------------------------------ */ + +/* Functions */ + +GtkWidget *scroll_widget(GtkWidget *child, + const char *widgetname) { + GtkWidget *scroller = gtk_scrolled_window_new(0, 0); + GtkAdjustment *adj; + + D(("scroll_widget")); + /* Why isn't _AUTOMATIC the default? */ + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + if(GTK_IS_LAYOUT(child)) { + /* Child widget has native scroll support */ + gtk_container_add(GTK_CONTAINER(scroller), child); + /* Fix up the step increments if they are 0 (seems like an odd default?) */ + if(GTK_IS_LAYOUT(child)) { + adj = gtk_layout_get_hadjustment(GTK_LAYOUT(child)); + if(!adj->step_increment) adj->step_increment = 16; + adj = gtk_layout_get_vadjustment(GTK_LAYOUT(child)); + if(!adj->step_increment) adj->step_increment = 16; + } + } else { + /* Child widget requires a viewport */ + gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scroller), + child); + } + /* Apply a name to the widget so it can be recolored */ + gtk_widget_set_name(GTK_BIN(scroller)->child, widgetname); + gtk_widget_set_name(scroller, widgetname); + return scroller; +} + +GdkPixbuf *find_image(const char *name) { + static const struct cache_type image_cache_type = { INT_MAX }; + + GdkPixbuf *pb; + char *path; + GError *err = 0; + + if(!(pb = (GdkPixbuf *)cache_get(&image_cache_type, name))) { + byte_xasprintf(&path, "%s/static/%s", pkgdatadir, name); + if(!(pb = gdk_pixbuf_new_from_file(path, &err))) { + error(0, "%s", err->message); + return 0; + } + cache_put(&image_cache_type, name, pb); + } + return pb; +} + +void popup_error(const char *msg) { + GtkWidget *w; + + w = gtk_message_dialog_new(GTK_WINDOW(toplevel), + GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", msg); + gtk_dialog_run(GTK_DIALOG(w)); + gtk_widget_destroy(w); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ + +/* arch-tag:tV2wkVwV3p2A66vyViQJSw */ diff --git a/disobedience/properties.c b/disobedience/properties.c new file mode 100644 index 0000000..ae945be --- /dev/null +++ b/disobedience/properties.c @@ -0,0 +1,438 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +/* Track properties -------------------------------------------------------- */ + +struct prefdata; + +static void kickoff_namepart(struct prefdata *f); +static void completed_namepart(struct prefdata *f); +static const char *get_edited_namepart(struct prefdata *f); +static void set_namepart(struct prefdata *f, const char *value); +static void set_namepart_completed(void *v); + +static void kickoff_string(struct prefdata *f); +static void completed_string(struct prefdata *f); +static const char *get_edited_string(struct prefdata *f); +static void set_string(struct prefdata *f, const char *value); + +static void kickoff_boolean(struct prefdata *f); +static void completed_boolean(struct prefdata *f); +static const char *get_edited_boolean(struct prefdata *f); +static void set_boolean(struct prefdata *f, const char *value); + +static void prefdata_completed(void *v, const char *value); +static void prefdata_onerror(struct callbackdata *cbd, + int code, + const char *msg); +static struct callbackdata *make_callbackdata(struct prefdata *f); +static void prefdata_completed_common(struct prefdata *f, + const char *value); + +static void properties_ok(GtkButton *button, gpointer userdata); +static void properties_apply(GtkButton *button, gpointer userdata); +static void properties_cancel(GtkButton *button, gpointer userdata); + +/* Data for a single preference */ +struct prefdata { + const char *track; + int row; + const struct pref *p; + const char *value; + GtkWidget *widget; +}; + +/* The type of a preference is the collection of callbacks needed to get, + * display and set it */ +struct preftype { + void (*kickoff)(struct prefdata *f); + /* Kick off the request to fetch the pref from the server. */ + + void (*completed)(struct prefdata *f); + /* Called when the value comes back in; creates the widget. */ + + const char *(*get_edited)(struct prefdata *f); + /* Get the edited value from the widget. */ + + void (*set)(struct prefdata *f, const char *value); + /* Set the new value and (if necessary) arrange for our display to update. */ +}; + +/* A namepart pref */ +static const struct preftype preftype_namepart = { + kickoff_namepart, + completed_namepart, + get_edited_namepart, + set_namepart +}; + +/* A string pref */ +static const struct preftype preftype_string = { + kickoff_string, + completed_string, + get_edited_string, + set_string +}; + +/* A boolean pref */ +static const struct preftype preftype_boolean = { + kickoff_boolean, + completed_boolean, + get_edited_boolean, + set_boolean +}; + +/* The known prefs for each track */ +static const struct pref { + const char *label; + const char *part; + const char *default_value; + const struct preftype *type; +} prefs[] = { + { "Artist", "artist", 0, &preftype_namepart }, + { "Album", "album", 0, &preftype_namepart }, + { "Title", "title", 0, &preftype_namepart }, + { "Tags", "tags", "", &preftype_string }, + { "Random", "pick_at_random", "1", &preftype_boolean }, +}; + +#define NPREFS (int)(sizeof prefs / sizeof *prefs) + +/* Buttons that appear at the bottom of the window */ +static const struct button { + const gchar *stock; + void (*clicked)(GtkButton *button, gpointer userdata); +} buttons[] = { + { GTK_STOCK_OK, properties_ok }, + { GTK_STOCK_APPLY, properties_apply }, + { GTK_STOCK_CANCEL, properties_cancel }, +}; + +#define NBUTTONS (int)(sizeof buttons / sizeof *buttons) + +static int prefs_unfilled; /* Prefs remaining to get */ +static int prefs_total; /* Total prefs */ +static struct prefdata *prefdatas; /* Current prefdatas */ +static GtkWidget *properties_window; +static GtkWidget *properties_table; +static GtkWidget *progress_window, *progress_bar; + +void properties(int ntracks, char **tracks) { + int n, m; + struct prefdata *f; + GtkWidget *hbox, *vbox, *button, *label, *entry; + + /* If there is a properties window open then just bring it to the + * front. It might not have the right values in... */ + if(properties_window) { + if(!prefs_unfilled) + gtk_window_present(GTK_WINDOW(properties_window)); + return; + } + assert(properties_table == 0); + if(ntracks > INT_MAX / NPREFS) { + popup_error("Too many tracks selected"); + return; + } + /* Create a new properties window */ + properties_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(properties_window, "destroy", + G_CALLBACK(gtk_widget_destroyed), &properties_window); + /* Most of the action is the table of preferences */ + properties_table = gtk_table_new((NPREFS + 1) * ntracks, 2, FALSE); + g_signal_connect(properties_table, "destroy", + G_CALLBACK(gtk_widget_destroyed), &properties_table); + gtk_window_set_title(GTK_WINDOW(properties_window), "Track Properties"); + /* Create labels for each pref of each track and kick off requests to the + * server to fill in the values */ + prefs_total = NPREFS * ntracks; + prefdatas = xcalloc(prefs_total, sizeof *prefdatas); + for(n = 0; n < ntracks; ++n) { + label = gtk_label_new("Track"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0); + gtk_table_attach(GTK_TABLE(properties_table), + label, + 0, 1, + (NPREFS + 1) * n, (NPREFS + 1) * n + 1, + GTK_FILL, 0, + 1, 1); + entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(entry), tracks[n]); + gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE); + gtk_table_attach(GTK_TABLE(properties_table), + entry, + 1, 2, + (NPREFS + 1) * n, (NPREFS + 1) * n + 1, + GTK_EXPAND|GTK_FILL, 0, + 1, 1); + for(m = 0; m < NPREFS; ++m) { + label = gtk_label_new(prefs[m].label); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0); + gtk_table_attach(GTK_TABLE(properties_table), + label, + 0, 1, + (NPREFS + 1) * n + 1 + m, (NPREFS + 1) * n + 2 + m, + GTK_FILL/*xoptions*/, 0/*yoptions*/, + 1, 1); + f = &prefdatas[NPREFS * n + m]; + f->track = tracks[n]; + f->row = (NPREFS + 1) * n + 1 + m; + f->p = &prefs[m]; + prefs[m].type->kickoff(f); + } + } + prefs_unfilled = prefs_total; + /* Buttons */ + hbox = gtk_hbox_new(FALSE, 1); + for(n = 0; n < NBUTTONS; ++n) { + button = gtk_button_new_from_stock(buttons[n].stock); + g_signal_connect(G_OBJECT(button), "clicked", + G_CALLBACK(buttons[n].clicked), 0); + gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 1); + } + /* Put it all together */ + vbox = gtk_vbox_new(FALSE, 1); + gtk_box_pack_start(GTK_BOX(vbox), + scroll_widget(properties_table, + "properties"), + TRUE, TRUE, 1); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 1); + gtk_container_add(GTK_CONTAINER(properties_window), vbox); + /* The table only really wants to be vertically scrollable */ + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(GTK_WIDGET(properties_table)->parent->parent), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + /* Pop up a progress bar while we're waiting */ + progress_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(progress_window, "destroy", + G_CALLBACK(gtk_widget_destroyed), &progress_window); + gtk_window_set_default_size(GTK_WINDOW(progress_window), 360, -1); + gtk_window_set_title(GTK_WINDOW(progress_window), + "Fetching Track Properties"); + progress_bar = gtk_progress_bar_new(); + gtk_container_add(GTK_CONTAINER(progress_window), progress_bar); + gtk_widget_show_all(progress_window); +} + +/* Everything is filled in now */ +static void prefdata_alldone(void) { + if(progress_window) + gtk_widget_destroy(progress_window); + /* Default size may be too small */ + gtk_window_set_default_size(GTK_WINDOW(properties_window), 480, 512); + /* TODO: relate default size to required size more closely */ + gtk_widget_show_all(properties_window); +} + +/* Namepart preferences ---------------------------------------------------- */ + +static void kickoff_namepart(struct prefdata *f) { + char *s; + + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + disorder_eclient_get(client, prefdata_completed, f->track, s, + make_callbackdata(f)); +} + +static void completed_namepart(struct prefdata *f) { + if(!f->value) + /* No setting, use the computed default value instead */ + f->value = trackname_part(f->track, "display", f->p->part); + f->widget = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(f->widget), f->value); +} + +static const char *get_edited_namepart(struct prefdata *f) { + return gtk_entry_get_text(GTK_ENTRY(f->widget)); +} + +static void set_namepart(struct prefdata *f, const char *value) { + char *s; + struct callbackdata *cbd = xmalloc(sizeof *cbd); + + cbd->u.f = f; + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + if(strcmp(trackname_part(f->track, "display", f->p->part), value)) + /* Different from default, set it */ + disorder_eclient_set(client, set_namepart_completed, f->track, s, value, + cbd); + else + /* Same as default, just unset */ + disorder_eclient_unset(client, set_namepart_completed, f->track, s, cbd); +} + +/* Called when we've set a namepart */ +static void set_namepart_completed(void *v) { + struct callbackdata *cbd = v; + struct prefdata *f = cbd->u.f; + + namepart_update(f->track, "display", f->p->part); +} + +/* String preferences ------------------------------------------------------ */ + +static void kickoff_string(struct prefdata *f) { + disorder_eclient_get(client, prefdata_completed, f->track, f->p->part, + make_callbackdata(f)); +} + +static void completed_string(struct prefdata *f) { + if(!f->value) + /* No setting, use the default value instead */ + f->value = f->p->default_value; + f->widget = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(f->widget), f->value); +} + +static const char *get_edited_string(struct prefdata *f) { + return gtk_entry_get_text(GTK_ENTRY(f->widget)); +} + +static void set_string(struct prefdata *f, const char *value) { + if(strcmp(f->p->default_value, value)) + /* Different from default, set it */ + disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, + value, 0/*v*/); + else + /* Same as default, just unset */ + disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part, + 0/*v*/); +} + +/* Boolean preferences ----------------------------------------------------- */ + +static void kickoff_boolean(struct prefdata *f) { + disorder_eclient_get(client, prefdata_completed, f->track, f->p->part, + make_callbackdata(f)); +} + +static void completed_boolean(struct prefdata *f) { + f->widget = gtk_check_button_new(); + if(!f->value) + /* Not set, use the default */ + f->value = f->p->default_value; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(f->widget), + strcmp(f->value, "0")); +} + +static const char *get_edited_boolean(struct prefdata *f) { + return (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(f->widget)) + ? "1" : "0"); +} + +static void set_boolean(struct prefdata *f, const char *value) { + char *s; + + byte_xasprintf(&s, "trackname_display_%s", f->p->part); + if(strcmp(value, f->p->default_value)) + disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, value, + 0/*v*/); + else + /* If default value then delete the pref */ + disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part, + 0/*v*/); +} + +/* Querying preferences ---------------------------------------------------- */ + +/* Make a suitable callbackdata */ +static struct callbackdata *make_callbackdata(struct prefdata *f) { + struct callbackdata *cbd = xmalloc(sizeof *cbd); + + cbd->onerror = prefdata_onerror; + cbd->u.f = f; + return cbd; +} + +/* No pref was set */ +static void prefdata_onerror(struct callbackdata *cbd, + int attribute((unused)) code, + const char attribute((unused)) *msg) { + prefdata_completed_common(cbd->u.f, 0); +} + +/* Got the value of a pref */ +static void prefdata_completed(void *v, const char *value) { + struct callbackdata *cbd = v; + + prefdata_completed_common(cbd->u.f, value); +} + +static void prefdata_completed_common(struct prefdata *f, + const char *value) { + f->value = value; + f->p->type->completed(f); + assert(f->value != 0); /* Had better set a default */ + gtk_table_attach(GTK_TABLE(properties_table), f->widget, + 1, 2, + f->row, f->row + 1, + GTK_EXPAND|GTK_FILL/*xoptions*/, 0/*yoptions*/, + 1, 1); + --prefs_unfilled; + if(prefs_total && progress_window) + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress_bar), + 1.0 - (double)prefs_unfilled / prefs_total); + if(!prefs_unfilled) + prefdata_alldone(); +} + +/* Button callbacks -------------------------------------------------------- */ + +static void properties_ok(GtkButton *button, + gpointer userdata) { + properties_apply(button, userdata); + properties_cancel(button, userdata); +} + +static void properties_apply(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + int n; + const char *edited; + struct prefdata *f; + + /* For each possible property we see if we've changed it and if so tell the + * server */ + for(n = 0; n < prefs_total; ++n) { + f = &prefdatas[n]; + edited = f->p->type->get_edited(f); + if(strcmp(edited, f->value)) { + /* The value has changed */ + f->p->type->set(f, edited); + f->value = xstrdup(edited); + } + } +} + +static void properties_cancel(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_widget_destroy(properties_window); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:+COG6p7PaNPZjzknPrKdcw */ diff --git a/disobedience/queue.c b/disobedience/queue.c new file mode 100644 index 0000000..2494505 --- /dev/null +++ b/disobedience/queue.c @@ -0,0 +1,1279 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include "disobedience.h" + +#define HCELLPADDING 4 +#define VCELLPADDING 2 + +/* A queue layout is structured as follows: + * + * vbox + * titlescroll + * titlelayout + * titlecells[col] eventbox (made by wrap_queue_cell) + * titlecells[col]->child label (from columns[]) + * mainscroll + * mainlayout + * cells[row * N + c] eventbox (made by wrap_queue_cell) + * cells[row * N + c]->child label (from column constructors) + * + * titlescroll never has any scrollbars. Instead whenever mainscroll's + * horizontal adjustment is changed, queue_scrolled adjusts titlescroll to + * match, forcing the title and the queue to pan in sync but allowing the queue + * to scroll independently. + * + * Whenever the queue changes everything below mainlayout is thrown away and + * reconstructed from scratch. Name lookups are cached, so this doesn't imply + * lots of disorder protocol traffic. + * + * The last cell on each row is the padding cell, and this extends ridiculously + * far to the right. (Can we do better?) + * + * When drag and drop is active we create extra eventboxes to act as dropzones. + * These only exist while the drag proceeds, as otherwise they steal events + * from more deserving widgets. (It might work to hide them when not in use + * too but this way around the d+d code is a bit more self-contained.) + */ + +/* Queue management -------------------------------------------------------- */ + +struct queuelike; + +static void add_drag_targets(struct queuelike *ql); +static void remove_drag_targets(struct queuelike *ql); +static void redisplay_queue(struct queuelike *ql); +static GtkWidget *column_when(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_who(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_namepart(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static GtkWidget *column_length(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); +static int draggable_row(const struct queue_entry *q); + +static const struct tabtype tabtype_queue; /* forward */ + +static const GtkTargetEntry dragtargets[] = { + { (char *)"disobedience-queue", GTK_TARGET_SAME_APP, 0 } +}; +#define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets) + +/* Definition of a column */ +struct column { + const char *name; /* Column name */ + GtkWidget *(*widget)(const struct queuelike *ql, + const struct queue_entry *q, + const char *data); /* Make a label for this column */ + const char *data; /* Data to pass to widget() */ + gfloat xalign; /* Alignment of the label */ +}; + +/* Need this in the middle of the types for NCOLUMNS */ +static const struct column columns[] = { + { "When", column_when, 0, 1 }, + { "Who", column_who, 0, 0 }, + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } +}; +#define NCOLUMNS (int)(sizeof columns / sizeof *columns) + +/* Data passed to menu item activation handlers */ +struct menuiteminfo { + struct queuelike *ql; /* which queue we're dealing with */ + struct queue_entry *q; /* hovered entry or 0 */ +}; + +struct menuitem { + /* Parameters */ + const char *name; /* name */ + + /* Callbacks */ + void (*activate)(GtkMenuItem *menuitem, + gpointer user_data); + /* Called to activate the menu item. The user data is the queue entry that + * the pointer was over when the menu popped up. */ + + int (*sensitive)(struct queuelike *ql, + struct menuitem *m, + struct queue_entry *q); + /* Called to determine whether the menu item is usable. Returns TRUE if it + * should be sensitive and FALSE otherwise. Q points to the queue entry the + * pointer is over. */ + + /* State */ + gulong handlerid; /* signal handler ID */ + GtkWidget *w; /* menu item widget */ +}; + +struct queuelike { + /* Parameters */ + const char *name; /* queue or recent */ + + /* Callbacks */ + void (*notify)(void); + /* Called when an update completes. */ + + struct queue_entry *(*fixup)(struct queue_entry *q); + /* Fix up the queue after update, or 0. Q is the list passed back from the + * server, the return value is assigned to ql->q. */ + + /* Widgets */ + GtkWidget *mainlayout; /* main layout */ + GtkWidget *mainscroll; /* scroller for main layout */ + GtkWidget *titlelayout; /* title layout */ + GtkWidget *titlecells[NCOLUMNS + 1]; /* title cells */ + GtkWidget **cells; /* all the cells */ + GtkWidget *menu; /* popup menu */ + struct menuitem *menuitems; /* menu items */ + GtkWidget *dragmark; /* drag destination marker */ + GtkWidget **dropzones; /* drag targets */ + + /* State */ + struct queue_entry *q; /* head of queue */ + struct queue_entry *last_click; /* last click */ + int nrows; /* number of rows */ + int mainrowheight; /* height of one row */ + hash *selection; /* currently selected items */ + int swallow_release; /* swallow button release from drag */ +}; + +static struct queuelike ql_queue, ql_recent; /* queue and recently played */ +static struct queue_entry *actual_queue; /* actual queue */ +static struct queue_entry *playing_track; /* currenty playing */ +static time_t last_playing = (time_t)-1; /* when last got playing */ +static int namepart_lookups_outstanding; +static int namepart_completions_deferred; /* # of completions not processed */ +static const struct cache_type cachetype_string = { 3600 }; +static const struct cache_type cachetype_integer = { 3600 }; +static GtkWidget *playing_length_label; + +/* Debugging --------------------------------------------------------------- */ + +#if 0 +static void describe_widget(const char *name, GtkWidget *w, int indent) { + int ww, wh, wx, wy; + + if(name) + fprintf(stderr, "%*s[%s]: '%s'\n", indent, "", + name, gtk_widget_get_name(w)); + gdk_window_get_position(w->window, &wx, &wy); + gdk_drawable_get_size(GDK_DRAWABLE(w->window), &ww, &wh); + fprintf(stderr, "%*s window %p: %dx%d at %dx%d\n", + indent, "", w->window, ww, wh, wx, wy); +} + +static void dump_layout(const struct queuelike *ql) { + GtkWidget *w; + char s[20]; + int row, col; + const struct queue_entry *q; + + describe_widget("mainscroll", ql->mainscroll, 0); + describe_widget("mainlayout", ql->mainlayout, 1); + for(q = ql->q, row = 0; q; q = q->next, ++row) + for(col = 0; col < NCOLUMNS + 1; ++col) + if((w = ql->cells[row * (NCOLUMNS + 1) + col])) { + sprintf(s, "%dx%d", row, col); + describe_widget(s, w, 2); + if(GTK_BIN(w)->child) + describe_widget(0, w, 3); + } +} +#endif + +/* Track detail lookup ----------------------------------------------------- */ + +/* A namepart lookup has completed or failed. */ +static void namepart_completed_or_failed(void) { + D(("namepart_completed_or_failed")); + --namepart_lookups_outstanding; + if(!namepart_lookups_outstanding || namepart_completions_deferred > 24) { + redisplay_queue(&ql_queue); + redisplay_queue(&ql_recent); + namepart_completions_deferred = 0; + } +} + +/* A namepart lookup has completed. */ +static void namepart_completed(void *v, const char *value) { + struct callbackdata *cbd = v; + + D(("namepart_completed")); + cache_put(&cachetype_string, cbd->u.key, value); + ++namepart_completions_deferred; + namepart_completed_or_failed(); +} + +/* A length lookup has completed. */ +static void length_completed(void *v, long l) { + struct callbackdata *cbd = v; + long *value; + + D(("namepart_completed")); + value = xmalloc(sizeof *value); + *value = l; + cache_put(&cachetype_integer, cbd->u.key, value); + ++namepart_completions_deferred; + namepart_completed_or_failed(); +} + +/* A length or namepart lookup has failed. */ +static void namepart_protocol_error( + struct callbackdata attribute((unused)) *cbd, + int attribute((unused)) code, + const char *msg) { + D(("namepart_protocol_error")); + gtk_label_set_text(GTK_LABEL(report_label), msg); + namepart_completed_or_failed(); +} + +/* Arrange to fill in a namepart cache entry */ +static void namepart_fill(const char *track, + const char *context, + const char *part, + const char *key) { + struct callbackdata *cbd; + + ++namepart_lookups_outstanding; + cbd = xmalloc(sizeof *cbd); + cbd->onerror = namepart_protocol_error; + cbd->u.key = key; + disorder_eclient_namepart(client, namepart_completed, + track, context, part, cbd); +} + +/* Look up a namepart. If it is in the cache then just return its value. If + * not then look it up and arrange for the queues to be updated when its value + * is available. */ +static const char *namepart(const char *track, + const char *context, + const char *part) { + char *key; + const char *value; + + D(("namepart %s %s %s", track, context, part)); + byte_xasprintf(&key, "namepart context=%s part=%s track=%s", + context, part, track); + value = cache_get(&cachetype_string, key); + if(!value) { + D(("deferring...")); + /* stick a value in the cache so we don't issue another lookup if we + * revisit */ + cache_put(&cachetype_string, key, value = "?"); + namepart_fill(track, context, part, key); + } + return value; +} + +/* Called from properties.c when we know a name part has changed */ +void namepart_update(const char *track, + const char *context, + const char *part) { + char *key; + + byte_xasprintf(&key, "namepart context=%s part=%s track=%s", + context, part, track); + /* Only refetch if it's actually in the cache */ + if(cache_get(&cachetype_string, key)) + namepart_fill(track, context, part, key); +} + +/* Look up a track length. If it is in the cache then just return its value. + * If not then look it up and arrange for the queues to be updated when its + * value is available. */ +static long getlength(const char *track) { + char *key; + const long *value; + struct callbackdata *cbd; + static const long bogus = -1; + + D(("getlength %s", track)); + byte_xasprintf(&key, "length track=%s", track); + value = cache_get(&cachetype_integer, key); + if(!value) { + D(("deferring..."));; + cache_put(&cachetype_integer, key, value = &bogus); + ++namepart_lookups_outstanding; + cbd = xmalloc(sizeof *cbd); + cbd->onerror = namepart_protocol_error; + cbd->u.key = key; + disorder_eclient_length(client, length_completed, track, cbd); + } + return *value; +} + +/* Column constructors ----------------------------------------------------- */ + +/* Format the 'when' column */ +static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + char when[64]; + struct tm tm; + time_t t; + + D(("column_when")); + switch(q->state) { + case playing_isscratch: + case playing_unplayed: + case playing_random: + t = q->expected; + break; + case playing_failed: + case playing_no_player: + case playing_ok: + case playing_scratched: + case playing_started: + case playing_paused: + case playing_quitting: + t = q->played; + break; + default: + t = 0; + break; + } + if(t) + strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm)); + else + when[0] = 0; + return gtk_label_new(when); +} + +/* Format the 'who' column */ +static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_who")); + return gtk_label_new(q->submitter ? q->submitter : ""); +} + +/* Format one of the track name columns */ +static GtkWidget *column_namepart(const struct queuelike + attribute((unused)) *ql, + const struct queue_entry *q, + const char *data) { + D(("column_namepart")); + return gtk_label_new(namepart(q->track, "display", data)); +} + +/* Compute the length field */ +static const char *text_length(const struct queue_entry *q) { + long l; + time_t now; + char *played = 0, *length = 0; + + /* Work out what to say for the length */ + l = getlength(q->track); + if(l > 0) + byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60); + else + byte_xasprintf(&length, "?:??"); + /* For the currently playing track we want to report how much of the track + * has been played */ + if(q == playing_track) { + /* log_state() arranges that we re-get the playing data whenever the + * pause/resume state changes */ + if(last_state & DISORDER_TRACK_PAUSED) + l = playing_track->sofar; + else { + time(&now); + l = playing_track->sofar + (now - last_playing); + } + byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length); + return played; + } else + return length; +} + +/* Format the length column */ +static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql, + const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_length")); + if(q == playing_track) { + assert(!playing_length_label); + playing_length_label = gtk_label_new(text_length(q)); + /* Zot playing_length_label when it is destroyed */ + g_signal_connect(playing_length_label, "destroy", + G_CALLBACK(gtk_widget_destroyed), &playing_length_label); + return playing_length_label; + } else + return gtk_label_new(text_length(q)); +} + +/* Apply a new queue contents, transferring the selection from the old value */ +static void update_queue(struct queuelike *ql, struct queue_entry *newq) { + struct queue_entry *q; + + D(("update_queue")); + /* Propagate last_click across the change */ + if(ql->last_click) { + for(q = newq; q; q = q->next) { + if(!strcmp(q->id, ql->last_click->id)) + break; + ql->last_click = q; + } + } + /* Tell every queue entry which queue owns it */ + for(q = newq; q; q = q->next) + q->ql = ql; + /* Switch to the new queue */ + ql->q = newq; + /* Clean up any selected items that have fallen off */ + for(q = ql->q; q; q = q->next) + selection_live(ql->selection, q->id); + selection_cleanup(ql->selection); +} + +/* Wrap up a widget for putting into the queue or title */ +static GtkWidget *wrap_queue_cell(GtkWidget *label, + const char *name, + int *wp) { + GtkRequisition req; + GtkWidget *bg; + + D(("wrap_queue_cell")); + /* Padding should be in the label so there are no gaps in the + * background */ + gtk_misc_set_padding(GTK_MISC(label), HCELLPADDING, VCELLPADDING); + /* Event box is just to hold a background color */ + bg = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(bg), label); + if(wp) { + /* Update maximum width */ + gtk_widget_size_request(label, &req); + if(req.width > *wp) *wp = req.width; + } + /* Set widget names */ + gtk_widget_set_name(bg, name); + gtk_widget_set_name(label, name); + return bg; +} + +/* Create the wrapped widget for a cell in the queue display */ +static GtkWidget *get_queue_cell(struct queuelike *ql, + const struct queue_entry *q, + int row, + int col, + const char *name, + int *wp) { + GtkWidget *label; + D(("get_queue_cell %d %d", row, col)); + label = columns[col].widget(ql, q, columns[col].data); + gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); + return wrap_queue_cell(label, name, wp); +} + +/* Add a padding cell to the end of a row */ +static GtkWidget *get_padding_cell(const char *name) { + D(("get_padding_cell")); + return wrap_queue_cell(gtk_label_new(""), name, 0); +} + +/* User button press and menu ---------------------------------------------- */ + +/* Update widget states in order to reflect the selection status */ +static void set_widget_states(struct queuelike *ql) { + struct queue_entry *q; + int row, col; + + for(q = ql->q, row = 0; q; q = q->next, ++row) { + for(col = 0; col < NCOLUMNS + 1; ++col) + gtk_widget_set_state(ql->cells[row * (NCOLUMNS + 1) + col], + selection_selected(ql->selection, q->id) ? + GTK_STATE_SELECTED : GTK_STATE_NORMAL); + } + /* Might need to change sensitivity of 'Properties' in main menu */ + menu_update(-1); +} + +static int queue_before(const struct queue_entry *a, + const struct queue_entry *b) { + while(a && a != b) + a = a->next; + return !!a; +} + +/* A button was pressed and released */ +static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data) { + struct queue_entry *q = user_data, *qq; + struct queuelike *ql = q->ql; + struct menuiteminfo *mii; + int n; + + /* Might be a release left over from a drag */ + if(ql->swallow_release) { + ql->swallow_release = 0; + return FALSE; /* propagate */ + } + + if(event->type == GDK_BUTTON_PRESS + && event->button == 3) { + /* Right button click. + * If the current item is not selected then switch the selection to just + * this item */ + if(q && !selection_selected(ql->selection, q->id)) { + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + ql->last_click = q; + set_widget_states(ql); + } + /* Set the sensitivity of each menu item and (re-)establish the signal + * handlers */ + for(n = 0; ql->menuitems[n].name; ++n) { + if(ql->menuitems[n].handlerid) + g_signal_handler_disconnect(ql->menuitems[n].w, + ql->menuitems[n].handlerid); + gtk_widget_set_sensitive(ql->menuitems[n].w, + ql->menuitems[n].sensitive(ql, + &ql->menuitems[n], + q)); + mii = xmalloc(sizeof *mii); + mii->ql = ql; + mii->q = q; + ql->menuitems[n].handlerid = g_signal_connect + (ql->menuitems[n].w, "activate", + G_CALLBACK(ql->menuitems[n].activate), mii); + } + /* Update the menu according to context */ + gtk_widget_show_all(ql->menu); + gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0, + event->button, event->time); + return TRUE; /* hide the click from other widgets */ + } + if(event->type == GDK_BUTTON_RELEASE + && event->button == 1) { + /* no modifiers: select this, unselect everything else, set last click + * +ctrl: flip selection of this, set last click + * +shift: select from last click to here, don't set last click + * +ctrl+shift: select from last click to here, set last click + */ + switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + case 0: + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + ql->last_click = q; + break; + case GDK_CONTROL_MASK: + selection_flip(ql->selection, q->id); + ql->last_click = q; + break; + case GDK_SHIFT_MASK: + case GDK_SHIFT_MASK|GDK_CONTROL_MASK: + if(ql->last_click) { + if(!(event->state & GDK_CONTROL_MASK)) + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + qq = q; + if(queue_before(ql->last_click, q)) + while(qq != ql->last_click) { + qq = qq->prev; + selection_set(ql->selection, qq->id, 1); + } + else + while(qq != ql->last_click) { + qq = qq->next; + selection_set(ql->selection, qq->id, 1); + } + if(event->state & GDK_CONTROL_MASK) + ql->last_click = q; + } + break; + } + set_widget_states(ql); + gtk_widget_queue_draw(ql->mainlayout); + } + return FALSE; /* propagate */ +} + +/* A button was pressed or released on the mainlayout. For debugging only at + * the moment. */ +static gboolean mainlayout_button(GtkWidget attribute((unused)) *widget, + GdkEventButton attribute((unused)) *event, + gpointer attribute((unused)) user_data) { + return FALSE; /* propagate */ +} + +void queue_select_all(struct queuelike *ql) { + struct queue_entry *qq; + + for(qq = ql->q; qq; qq = qq->next) + selection_set(ql->selection, qq->id, 1); + ql->last_click = 0; + set_widget_states(ql); +} + +void queue_properties(struct queuelike *ql) { + struct vector v; + const struct queue_entry *qq; + + vector_init(&v); + for(qq = ql->q; qq; qq = qq->next) + if(selection_selected(ql->selection, qq->id)) + vector_append(&v, (char *)qq->track); + if(v.nvec) + properties(v.nvec, v.vec); +} + +/* Drag and drop rearrangement --------------------------------------------- */ + +static int draggable_row(const struct queue_entry *q) { + return q->ql == &ql_queue && q != playing_track; +} + +/* Called when a drag begings */ +static void queue_drag_begin(GtkWidget attribute((unused)) *widget, + GdkDragContext attribute((unused)) *dc, + gpointer data) { + struct queue_entry *q = data; + struct queuelike *ql = q->ql; + + /* Make sure the playing track is not selected, since it cannot be dragged */ + if(playing_track) + selection_set(ql->selection, playing_track->id, 0); + /* If the dragged item is not in the selection then change the selection to + * just that */ + if(!selection_selected(ql->selection, q->id)) { + selection_empty(ql->selection); + selection_set(ql->selection, q->id, 1); + set_widget_states(ql); + } + /* Ignore the eventual button release */ + ql->swallow_release = 1; + /* Create dropzones */ + add_drag_targets(ql); +} + +/* Convert an ID back into a queue entry and a screen row number */ +static struct queue_entry *findentry(struct queuelike *ql, + const char *id, + int *rowp) { + int row; + struct queue_entry *q; + + if(id) { + for(q = ql->q, row = 0; q && strcmp(q->id, id); q = q->next, ++row) + ; + } else { + q = 0; + row = playing_track ? 0 : -1; + } + if(rowp) *rowp = row; + return q; +} + +/* Called when data is dropped */ +static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget, + GdkDragContext *drag_context, + gint attribute((unused)) x, + gint attribute((unused)) y, + guint when, + gpointer user_data) { + struct queuelike *ql = &ql_queue; + const char *id = user_data; + struct vector vec; + struct queue_entry *q; + + if(!id || (playing_track && !strcmp(id, playing_track->id))) + id = ""; + vector_init(&vec); + for(q = ql->q; q; q = q->next) + if(q != playing_track && selection_selected(ql->selection, q->id)) + vector_append(&vec, (char *)q->id); + disorder_eclient_moveafter(client, id, vec.nvec, (const char **)vec.vec, + 0/*completed*/, 0/*v*/); + gtk_drag_finish(drag_context, TRUE, TRUE, when); + /* Destroy dropzones */ + remove_drag_targets(ql); + return TRUE; +} + +/* Called when we enter, or move within, a drop zone */ +static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget, + GdkDragContext *drag_context, + gint attribute((unused)) x, + gint attribute((unused)) y, + guint when, + gpointer user_data) { + struct queuelike *ql = &ql_queue; + const char *id = user_data; + int row; + struct queue_entry *q = findentry(ql, id, &row); + + if(!id || q) { + if(!ql->dragmark) { + ql->dragmark = gtk_event_box_new(); + g_signal_connect(ql->dragmark, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->dragmark); + gtk_widget_set_size_request(ql->dragmark, 10240, row ? 4 : 2); + gtk_widget_set_name(ql->dragmark, "queue-drag"); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, + (row + 1) * ql->mainrowheight - !!row); + } else + gtk_layout_move(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, + (row + 1) * ql->mainrowheight - !!row); + gtk_widget_show(ql->dragmark); + gdk_drag_status(drag_context, GDK_ACTION_MOVE, when); + return TRUE; + } else + /* ID has gone AWOL */ + return FALSE; +} + +/* Called when we leave a drop zone */ +static void queue_drag_leave(GtkWidget attribute((unused)) *widget, + GdkDragContext attribute((unused)) *drag_context, + guint attribute((unused)) when, + gpointer attribute((unused)) user_data) { + struct queuelike *ql = &ql_queue; + + if(ql->dragmark) + gtk_widget_hide(ql->dragmark); +} + +/* Add a drag target at position Y. ID is the track to insert the moved tracks + * after, and might be 0 to insert before the start. */ +static void add_drag_target(struct queuelike *ql, int y, int row, + const char *id) { + GtkWidget *eventbox; + + assert(ql->dropzones[row] == 0); + eventbox = gtk_event_box_new(); + /* Make the target zone invisible */ + gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox), FALSE); + /* Make it large enough */ + gtk_widget_set_size_request(eventbox, 10240, + y ? ql->mainrowheight : ql->mainrowheight / 2); + /* Position it */ + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), eventbox, 0, + y ? y - ql->mainrowheight / 2 : 0); + /* Mark it as capable of receiving drops */ + gtk_drag_dest_set(eventbox, + 0, + dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); + g_signal_connect(eventbox, "drag-drop", + G_CALLBACK(queue_drag_drop), (char *)id); + /* Monitor drag motion */ + g_signal_connect(eventbox, "drag-motion", + G_CALLBACK(queue_drag_motion), (char *)id); + g_signal_connect(eventbox, "drag-leave", + G_CALLBACK(queue_drag_leave), (char *)id); + /* The widget needs to be shown to receive drags */ + gtk_widget_show(eventbox); + /* Remember the drag targets */ + ql->dropzones[row] = eventbox; + g_signal_connect(eventbox, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->dropzones[row]); +} + +/* Create dropzones for dragging into */ +static void add_drag_targets(struct queuelike *ql) { + int row, y; + struct queue_entry *q; + + /* Create an array to store the widgets */ + ql->dropzones = xcalloc(ql->nrows, sizeof (GtkWidget *)); + y = 0; + /* Add a drag target before the first row provided it's not the playing + * track */ + if(!playing_track || ql->q != playing_track) + add_drag_target(ql, 0, 0, 0); + /* Put a drag target at the bottom of every row */ + for(q = ql->q, row = 0; q; q = q->next, ++row) { + y += ql->mainrowheight; + add_drag_target(ql, y, row, q->id); + } +} + +/* Remove the dropzones */ +static void remove_drag_targets(struct queuelike *ql) { + int row; + + for(row = 0; row < ql->nrows; ++row) { + if(ql->dropzones[row]) { + gtk_widget_destroy(ql->dropzones[row]); + } + assert(ql->dropzones[row] == 0); + } +} + +/* Layout ------------------------------------------------------------------ */ + +/* Redisplay the queue. */ +static void redisplay_queue(struct queuelike *ql) { + struct queue_entry *q; + int row, col; + GList *c; + const char *name; + GtkRequisition req; + GtkWidget *w; + int maxwidths[NCOLUMNS], x, y, titlerowheight; + int totalwidth = 10240; /* TODO: can we be less blunt */ + + D(("redisplay_queue")); + /* Eliminate all the existing widgets and start from scratch */ + for(c = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout)); + c; + c = c->next) { + /* Destroy both the label and the eventbox */ + if(GTK_BIN(c->data)->child) + gtk_widget_destroy(GTK_BIN(c->data)->child); + gtk_widget_destroy(GTK_WIDGET(c->data)); + } + /* Adjust the row count */ + for(q = ql->q, ql->nrows = 0; q; q = q->next) + ++ql->nrows; + /* We need to create all the widgets before we can position them */ + ql->cells = xcalloc(ql->nrows * (NCOLUMNS + 1), sizeof *ql->cells); + /* Minimum width is given by the column headings */ + for(col = 0; col < NCOLUMNS; ++col) { + /* Reset size so we don't inherit last iteration's maximum size */ + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, -1, -1); + gtk_widget_size_request(GTK_BIN(ql->titlecells[col])->child, &req); + maxwidths[col] = req.width; + } + /* Find the vertical size of the title bar */ + gtk_widget_size_request(GTK_BIN(ql->titlecells[0])->child, &req); + titlerowheight = req.height; + y = 0; + if(ql->nrows) { + /* Construct the widgets */ + for(q = ql->q, row = 0; q; q = q->next, ++row) { + /* Figure out the widget name for this row */ + if(q == playing_track) name = "row-playing"; + else name = row % 2 ? "row-even" : "row-odd"; + /* Make the widget for each column */ + for(col = 0; col <= NCOLUMNS; ++col) { + /* Create and store the widget */ + if(col < NCOLUMNS) + w = get_queue_cell(ql, q, row, col, name, &maxwidths[col]); + else + w = get_padding_cell(name); + ql->cells[row * (NCOLUMNS + 1) + col] = w; + /* Maybe mark it draggable */ + if(draggable_row(q)) { + gtk_drag_source_set(w, GDK_BUTTON1_MASK, + dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); + g_signal_connect(w, "drag-begin", G_CALLBACK(queue_drag_begin), q); + } + /* Catch button presses */ + g_signal_connect(w, "button-release-event", + G_CALLBACK(queuelike_button_released), q); + g_signal_connect(w, "button-press-event", + G_CALLBACK(queuelike_button_released), q); + } + } + /* ...and of each row in the main layout */ + gtk_widget_size_request(GTK_BIN(ql->cells[0])->child, &req); + ql->mainrowheight = req.height; + /* Now we know the maximum width of each column we can set the size of + * everything and position it */ + for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) { + x = 0; + for(col = 0; col < NCOLUMNS; ++col) { + w = ql->cells[row * (NCOLUMNS + 1) + col]; + gtk_widget_set_size_request(GTK_BIN(w)->child, + maxwidths[col], -1); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); + x += maxwidths[col]; + } + w = ql->cells[row * (NCOLUMNS + 1) + col]; + gtk_widget_set_size_request(GTK_BIN(w)->child, + totalwidth - x, -1); + gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); + y += ql->mainrowheight; + } + } + /* Titles */ + x = 0; + for(col = 0; col < NCOLUMNS; ++col) { + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, + maxwidths[col], -1); + gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); + x += maxwidths[col]; + } + gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, + totalwidth - x, -1); + gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); + /* Set the states */ + set_widget_states(ql); + /* Make sure it's all visible */ + gtk_widget_show_all(ql->mainlayout); + gtk_widget_show_all(ql->titlelayout); + /* Layouts might shrink to arrange for the area they shrink out of to be + * redrawn */ + gtk_widget_queue_draw(ql->mainlayout); + gtk_widget_queue_draw(ql->titlelayout); + /* Adjust the size of the layout */ + gtk_layout_set_size(GTK_LAYOUT(ql->mainlayout), x, y); + gtk_layout_set_size(GTK_LAYOUT(ql->titlelayout), x, titlerowheight); + gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight); +} + +/* Called with new queue/recent contents */ +static void queuelike_completed(void *v, struct queue_entry *q) { + struct callbackdata *cbd = v; + struct queuelike *ql = cbd->u.ql; + + D(("queuelike_complete")); + /* Install the new queue */ + update_queue(ql, ql->fixup(q)); + /* Update the display */ + redisplay_queue(ql); + if(ql->notify) + ql->notify(); + /* Update sensitivity of main menu items */ + menu_update(-1); +} + +/* Called with a new currently playing track */ +static void playing_completed(void attribute((unused)) *v, + struct queue_entry *q) { + struct callbackdata cbd; + D(("playing_completed")); + playing_track = q; + /* Record when we got the playing track data so we know how old the 'sofar' + * field is */ + time(&last_playing); + cbd.u.ql = &ql_queue; + queuelike_completed(&cbd, actual_queue); +} + +static void queue_scrolled(GtkAdjustment *adjustment, + gpointer user_data) { + GtkAdjustment *titleadj = user_data; + + D(("queue_scrolled")); + gtk_adjustment_set_value(titleadj, adjustment->value); +} + +/* Create a queuelike thing (queue/recent) */ +static GtkWidget *queuelike(struct queuelike *ql, + struct queue_entry *(*fixup)(struct queue_entry *), + void (*notify)(void), + struct menuitem *menuitems, + const char *name) { + GtkWidget *vbox, *mainscroll, *titlescroll, *label; + GtkAdjustment *mainadj, *titleadj; + int col, n; + + D(("queuelike")); + ql->fixup = fixup; + ql->notify = notify; + ql->menuitems = menuitems; + ql->name = name; + ql->mainrowheight = !0; /* else division by 0 */ + ql->selection = selection_new(); + /* Create the layouts */ + ql->mainlayout = gtk_layout_new(0, 0); + ql->titlelayout = gtk_layout_new(0, 0); + /* Scroll the layouts */ + ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout, name); + titlescroll = scroll_widget(ql->titlelayout, name); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(titlescroll), + GTK_POLICY_NEVER, GTK_POLICY_NEVER); + mainadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(mainscroll)); + titleadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(titlescroll)); + g_signal_connect(mainadj, "changed", G_CALLBACK(queue_scrolled), titleadj); + g_signal_connect(mainadj, "value-changed", G_CALLBACK(queue_scrolled), titleadj); + /* Fill the titles and put them anywhere */ + for(col = 0; col < NCOLUMNS; ++col) { + label = gtk_label_new(columns[col].name); + gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); + ql->titlecells[col] = wrap_queue_cell(label, "row-title", 0); + gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); + } + ql->titlecells[col] = get_padding_cell("row-title"); + gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); + /* Pack the lot together in a vbox */ + vbox = gtk_vbox_new(0, 0); + gtk_box_pack_start(GTK_BOX(vbox), titlescroll, 0, 0, 0); + gtk_box_pack_start(GTK_BOX(vbox), mainscroll, 1, 1, 0); + /* Create the popup menu */ + ql->menu = gtk_menu_new(); + g_signal_connect(ql->menu, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->menu); + for(n = 0; menuitems[n].name; ++n) { + menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name); + gtk_menu_attach(GTK_MENU(ql->menu), menuitems[n].w, 0, 1, n, n + 1); + } + g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_queue); + g_object_set_data(G_OBJECT(vbox), "queue", ql); + /* Catch button presses */ + g_signal_connect(ql->mainlayout, "button-release-event", + G_CALLBACK(mainlayout_button), ql); +#if 0 + g_signal_connect(ql->mainlayout, "button-press-event", + G_CALLBACK(mainlayout_button), ql); +#endif + return vbox; +} + +/* Popup menu items -------------------------------------------------------- */ + +/* Count the number of items selected */ +static int queue_count_selected(const struct queuelike *ql) { + return hash_count(ql->selection); +} + +/* Count the number of items selected */ +static int queue_count_entries(const struct queuelike *ql) { + int nitems = 0; + const struct queue_entry *q; + + for(q = ql->q; q; q = q->next) + ++nitems; + return nitems; +} + +/* Count the number of items selected, excluding the playing track if there is + * one */ +static int count_selected_nonplaying(const struct queuelike *ql) { + int nselected = queue_count_selected(ql); + + if(ql->q == playing_track && selection_selected(ql->selection, ql->q->id)) + --nselected; + return nselected; +} + +static int scratch_sensitive(struct queuelike attribute((unused)) *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* We can scratch if the playing track is selected */ + return playing_track && selection_selected(ql->selection, playing_track->id); +} + +static void scratch_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + if(playing_track) + disorder_eclient_scratch(client, playing_track->id, 0, 0); +} + +static int remove_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry *q) { + /* We can remove if we're hovering over a particular track or any non-playing + * tracks are selected */ + return (q && q != playing_track) || count_selected_nonplaying(ql); +} + +static void remove_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + struct queue_entry *q = mii->q; + struct queuelike *ql = mii->ql; + + if(count_selected_nonplaying(mii->ql)) { + /* Remove selected tracks */ + for(q = ql->q; q; q = q->next) + if(selection_selected(ql->selection, q->id) && q != playing_track) + disorder_eclient_remove(client, q->id, 0, 0); + } else if(q) + /* Remove just the hovered track */ + disorder_eclient_remove(client, q->id, 0, 0); +} + +static int properties_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* "Properties" is sensitive if at least something is selected */ + return hash_count(ql->selection) > 0; +} + +static void properties_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + + queue_properties(mii->ql); +} + +static int selectall_sensitive(struct queuelike *ql, + struct menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* Sensitive if there is anything to select */ + return !!ql->q; +} + +static void selectall_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + queue_select_all(mii->ql); +} + +/* The queue --------------------------------------------------------------- */ + +/* Fix up the queue by sticking the currently playing track on the front */ +static struct queue_entry *fixup_queue(struct queue_entry *q) { + D(("fixup_queue")); + actual_queue = q; + if(playing_track) { + if(actual_queue) + actual_queue->prev = playing_track; + playing_track->next = actual_queue; + return playing_track; + } else + return actual_queue; +} + +/* Called regularly to adjust the so-far played label (redrawing the whole + * queue once a second makes disobedience occupy >10% of the CPU on my Athlon + * which is ureasonable expensive) */ +static gboolean adjust_sofar(gpointer attribute((unused)) data) { + if(playing_length_label && playing_track) + gtk_label_set_text(GTK_LABEL(playing_length_label), + text_length(playing_track)); + return TRUE; +} + +/* Popup menu for the queue. Put the properties first so that finger trouble + * is less dangerous. */ +static struct menuitem queue_menu[] = { + { "Properties", properties_activate, properties_sensitive, 0, 0 }, + { "Select all", selectall_activate, selectall_sensitive, 0, 0 }, + { "Scratch", scratch_activate, scratch_sensitive, 0, 0 }, + { "Remove", remove_activate, remove_sensitive, 0, 0 }, + { 0, 0, 0, 0, 0 } +}; + +GtkWidget *queue_widget(void) { + D(("queue_widget")); + /* Arrange periodic update of the so-far played field */ + g_timeout_add(1000/*ms*/, adjust_sofar, 0); + /* We pass choose_update() as our notify function since the choose screen + * marks tracks that are playing/in the queue. */ + return queuelike(&ql_queue, fixup_queue, choose_update, queue_menu, + "queue"); +} + +void queue_update(void) { + struct callbackdata *cbd; + + D(("queue_update")); + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + cbd->u.ql = &ql_queue; + gtk_label_set_text(GTK_LABEL(report_label), "updating queue"); + disorder_eclient_queue(client, queuelike_completed, cbd); +} + +void playing_update(void) { + D(("playing_update")); + gtk_label_set_text(GTK_LABEL(report_label), "updating playing track"); + disorder_eclient_playing(client, playing_completed, 0); +} + +/* Recently played tracks -------------------------------------------------- */ + +static struct queue_entry *fixup_recent(struct queue_entry *q) { + /* 'recent' is in the wrong order. TODO: globally fix this! */ + struct queue_entry *qr = 0, *qn; + + D(("fixup_recent")); + while(q) { + qn = q->next; + /* Swap next/prev pointers */ + q->next = q->prev; + q->prev = qn; + /* Remember last node for new head */ + qr = q; + /* Next node */ + q = qn; + } + return qr; +} + +static struct menuitem recent_menu[] = { + { "Properties", properties_activate, properties_sensitive,0, 0 }, + { "Select all", selectall_activate, selectall_sensitive, 0, 0 }, + { 0, 0, 0, 0, 0 } +}; + +GtkWidget *recent_widget(void) { + D(("recent_widget")); + return queuelike(&ql_recent, fixup_recent, 0, recent_menu, "recent"); +} + +void recent_update(void) { + struct callbackdata *cbd; + + D(("recent_update")); + cbd = xmalloc(sizeof *cbd); + cbd->onerror = 0; + cbd->u.ql = &ql_recent; + gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list"); + disorder_eclient_recent(client, queuelike_completed, cbd); +} + +/* Main menu plumbing ------------------------------------------------------ */ + +static int queue_properties_sensitive(GtkWidget *w) { + return !!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue")); +} + +static int queue_selectall_sensitive(GtkWidget *w) { + return !!queue_count_entries(g_object_get_data(G_OBJECT(w), "queue")); +} + +static void queue_properties_activate(GtkWidget *w) { + queue_properties(g_object_get_data(G_OBJECT(w), "queue")); +} + +static void queue_selectall_activate(GtkWidget *w) { + queue_select_all(g_object_get_data(G_OBJECT(w), "queue")); +} + +static const struct tabtype tabtype_queue = { + queue_properties_sensitive, + queue_selectall_sensitive, + queue_properties_activate, + queue_selectall_activate, +}; + +/* Other entry points ------------------------------------------------------ */ + +int queued(const char *track) { + struct queue_entry *q; + + D(("queued %s", track)); + for(q = ql_queue.q; q; q = q->next) + if(!strcmp(q->track, track)) + return 1; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:kxVCqYNfvMJkYlkf2Pn8pg */ diff --git a/doc/Makefile.am b/doc/Makefile.am new file mode 100644 index 0000000..743ace3 --- /dev/null +++ b/doc/Makefile.am @@ -0,0 +1,47 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +SEDFILES=disorder.1 disorderd.8 disorder_config.5 \ + disorder-dump.8 disorder_protocol.5 disorder-deadlock.8 \ + disorder-rescan.8 disobedience.1 disorderfm.1 + +include ${top_srcdir}/scripts/sedfiles.make + +man_MANS=disorderd.8 disorder.1 disorder.3 disorder_config.5 disorder-dump.8 \ + disorder_protocol.5 tkdisorder.1 disorder-deadlock.8 \ + disorder-rescan.8 disobedience.1 disorderfm.1 + +HTMLMAN=$(foreach man,$(man_MANS),$(man).html) + +$(HTMLMAN) : %.html : % $(top_srcdir)/scripts/htmlman + rm -f $@.new + $(top_srcdir)/scripts/htmlman $< >$@.new + chmod 444 $@.new + mv -f $@.new $@ + +pkgdata_DATA=$(HTMLMAN) + +EXTRA_DIST=disorderd.8.in disorder.1.in disorder_config.5.in \ + disorder.3 disorder-dump.8.in disorder_protocol.5.in \ + tkdisorder.1 disorder-deadlock.8.in disorder-rescan.8.in \ + disobedience.1.in disorderfm.1.in + +CLEANFILES=$(SEDFILES) $(HTMLMAN) +# arch-tag:9e90ca91629f0571b87f5a5f11227b7f diff --git a/doc/checklist.txt b/doc/checklist.txt new file mode 100644 index 0000000..f62e7be --- /dev/null +++ b/doc/checklist.txt @@ -0,0 +1,80 @@ +* Server + +After an hour or so of play use lsof to check that only a reasonable +number of FDs are used by the server; the speaker; the deadlock +checker. + +* Playing + +Check that artist and album work. + +Scratch button should work. + +Queue some tracks, check they can be removed. + +Album link should show track as playing. + +Amount of track played should be correct (also 'disorder playing'). + +* Recent + +Most recent should be at the top. + +Check that artist, album and prefs links work. + +* Choose + +Queue some tracks. They should be marked as queued. + +Pick an album. Try 'play all'. Check order. Remove all. + +Navigate around. Go into albums, back out with the navigation links. + +Go up outside the collection. Should work, produce directories you +can go back into. + +* Search + +Try a large search, e.g. 'love'. + +Look for 'Various'. It should be in the right order and say +'Various', i.e. an alias artist name should not have leaked into it. + +* Manage + +Check pause and play controls. Pause should _not crash_ for tracks +that cannot be paused. + +Set the volume up and down. + +Set the volume to exact values (different for L and R), check that the +proper speaker is affected. + +Add some tracks, rearrange them, remove them again. + +* Help + +Are all the man page links there? + +Are recent UI changes documented? + +* About + +Does the search league look plausible? + +Are there are any good candidates for additional stopwords? + +Is the copyright date right? Also check credits.html. + +* Preferences + +Modify prefs for a track from 'recent'. + +Modify prefs for a single track from 'choose'. + +Modify prefs for a whole album from 'choose'. + +Local Variables: +mode:outline +End: +# arch-tag:wuvzKmIxK6XShozQ9Bsuww diff --git a/doc/disobedience.1.in b/doc/disobedience.1.in new file mode 100644 index 0000000..75fc867 --- /dev/null +++ b/doc/disobedience.1.in @@ -0,0 +1,223 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disobedience 1 +.SH NAME +disobedience \- GUI client for DisOrder jukebox +.SH SYNOPSIS +.B disobedience +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disobedience +is a graphical client for DisOrder. It is a work in progress and many features +are not implemented yet. However everything in this man page is either +implemented or marked as missing. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SS "GTK+ Options" +Additional options are supported by the GTK+ library. Refer to GTK+ +documentation for further information. Under X11 they include: +.TP +.B --display \fIDISPLAY\fR +The X display to use. +.TP +.B --screen \fISCREEN\fR +The screen number to use. +.\" If know enough to use it you know enough to find it +.\" .TP +.\" .B --sync +.\" Make all X requests synchronously. +.SH "WINDOWS AND ICONS" +.SS "File Menu" +This only has one option, "Quit", which terminates the program. +.SS "Edit Menu" +This has the following options: +.TP +.B "Select All" +Select all tracks in whichever of the Queue or Recent tabs are showing. +.TP +.B Properties +Edit the details of the selected tracks. +.SS "Help Menu" +This has only one option, "About DisOrder", which pops up a box giving the +name, author and version number of the software. +.SS "Controls" +.TP +.B "Pause button" +The pause button can be used to pause and resume tracks. +.TP +.B "Scratch button" +The scratch button, a red cross, can be used to interrupt the currently playing +track. +.TP +.B "Random play button" +The random play button can be used to enable and disable random play. It does +not take effect until the currently playing track finishes. +.TP +.B "Play button" +The play button controls whether tracks will be played at all. As above it +does not take effect until the currently playing track finishes. +.TP +.B "Volume slider" +The volume slider indicates the current volume level and can be used to adjust +it. 0 is silent and 10 is maximum volume. +.TP +.B "Balance slider" +The balance slider indicates the current balance and can be used to adjust it. +-1 means only the left speaker, 0 means both speakers at equal volume and +1 +means the only the right speaker. +.SS "Queue Tab" +This displays the currently playing track and the queue. The currently playing +track is at the top and has a green background. Queued tracks appear below it +and have alternating red and white backgrounds. +.PP +The left button can be use to select and deselect tracks. On its own it just +selects the pointed track and deselects everything else. With CTRL it flips +the state of the pointed track without affecting anything else. With SHIFT it +selects every track from the last click to the current position and deselects +everything else. With both CTRL and SHIFT it selects everything from the last +click to the current position without deselecting anything. +.PP +The right button pops up a menu. This has the following options: +.TP +.B Properties +Edit the details of the selected tracks. See +.B "Properties Window" +below. +.TP +.B "Select All" +Select all tracks. +.TP +.B Scratch +Interrupt the currently playing track. (Note that this appears even if you +right click over a queued track rather than the currently playing track.) +.TP +.B Remove +Remove the selected tracks from the queue. +.SS "Recent Tab" +This displays recently played tracks, the most recent at the top. +.PP +The left button functions as above. The right button pops up a menu with the +following options: +.TP +.B Properties +Edit the details of the selected tracks. See +.B "Properties Window" +below. +.TP +.B "Select All" +Select all tracks. +.SS "Choose Tab" +This displays all the tracks known to the server in a tree structure. +.PP +Directories are represented with an arrow to their left. This can be clicked +to reveal or hide the contents of the directory. The top level "directories" +break up tracks by their first letter. +.PP +Playable files are represented by their name. If they are playing or in the +queue then a notes icon appears next to them. +.PP +Left clicking on a file will select it. As with the queue tab you can use +SHIFT and CTRL to select multiple files. +.PP +The text box at the top is a search form. If you enter search terms here then +the display will be limited to tracks containing all those words. You can also +limit the results to tracks with particular tags, by including \fBtag:\fITAG\fR +for each tag. +.PP +To start a new search just edit the contents of the search box. The cancel +button to its right clears the current search. +.PP +Right clicking will pop up a menu with the following options: +.TP +.B Play +Play selected tracks. +.TP +.B Properties +Edit properties of selected tracks. +.PP +A middle click on a file will add it to the queue. +.SS "Properties Window" +This window contains details of one or more tracks and allows them to be +edited. +.PP +The Artist, Album and Title fields determine how the tracks appear in +the queue and recently played tabs. +.PP +The Tags field determine which tags apply to the track. Tags are separated by +commas and can contain any printing characters except comma. +.PP +The Random checkbox determines whether the track will be picked at random. +Random play is enabled for every track by default, but it can be turned off +here. +.PP +Press "OK" to confirm all changes and close the window, "Apply" to confirm +changes but keep the window open and "Cancel" to close the window and discard +all changes. +.SH "KEYBOARD SHORTCUTS" +.TP +.B CTRL+A +Select all tracks (queue/recent) +.TP +.B CTRL+Q +Quit. +.SH "GTK+ RESOURCES" +You can override these resources in order to customize the appearance of +Disobedience. TODO example that actually works. +.SS "Widget Names" +.TP +.B disobedience.*.choose +This is the panel containing the track choice tree. +.TP +.B disobedience.*.queue +This is the panel displaying the queue. +.TP +.B disobedience.*.choose +This is the panel listing recently played tracks. +.TP +.B disobedience.*.row-playing +This is the row listing the currently playing track. +.TP +.B disobedience.*.row-odd +This an odd-numbered row in the queue or recently played track list. +.TP +.B disobedience.*.row-even +This an even-numbered row in the queue or recently played track list. +.SH "SEE ALSO" +.BR disorder_config (5) +.PP +.B http://www.gtk.org/api/2.6/gtk/gtk-x11.html +.br +- Using GTK+ on the X Window System +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:PmkAKyNwF7gDXfzSQ9GnYg diff --git a/doc/disorder-deadlock.8.in b/doc/disorder-deadlock.8.in new file mode 100644 index 0000000..a876bb0 --- /dev/null +++ b/doc/disorder-deadlock.8.in @@ -0,0 +1,47 @@ +.\" +.\" Copyright (C) 2005 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-deadlock 8 +.SH NAME +disorder-deadlock \- DisOrder deadlock manager +.SH SYNOPSIS +.B disorder-deadlock +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disorder-deadlock +is DisOrder's deadlock manager. It is automatically started by the +server and does not need to be invoked manually. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:miB8m/aYAr2KdqJ/DLNqig diff --git a/doc/disorder-dump.8.in b/doc/disorder-dump.8.in new file mode 100644 index 0000000..601f0cc --- /dev/null +++ b/doc/disorder-dump.8.in @@ -0,0 +1,116 @@ +.\" +.\" Copyright (C) 2004, 2005 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-dump 8 +.SH NAME +disorder-dump \- DisOrder dump/undump tool +.SH SYNOPSIS +.B disorder-dump +.RI [ OPTIONS ] +.BR --dump | --undump +.I PATH +.br +.B disorder-dump +.RI [ OPTIONS ] +.BR --recompute-aliases +.SH DESCRIPTION +.B disorder-dump +is used to dump and restore preferences data. +.SH OPTIONS +.TP +.B --dump +Write preferences data to \fIPATH\fR. This can safely be used whether +or not the server is running. +.TP +.B --undump +Read preferences data from \fIPATH\fR, replacing (unrecoverably) the +current settings. This should normally only be done while the server +is not running. +.IP +If the server is running then it may hang while the undump completes. +.TP +.B --recover +Perform database recovery at startup. The server should not be +running if this option is used. +.TP +.B --recompute-aliases +Recompute aliases without dumping or undumping the databases. Under +normal circumstances this is never necessary. +.TP +.B --remove-pathless +Remove tracks with no associated path when undumping or when +recomputing aliases. In normal use such tracks are all aliases. +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR /etc/disorder/config . +.TP +.B --debug\fR +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH NOTES +This program might be used for a number of purposes: +.TP 2 +.B . +Taking a backup of the non-regeneratable parts of DisOrder's databases. +.TP +.B . +Indoctrinating one DisOrder server with the preference values of +another. +.TP +.B . +Upgrading DisOrder across data format changes in the underlying +database library. +.PP +The output file is versioned, so versions produced from a future +version of DisOrder may be rejected by \fB--undump\fR. It has an end +marker so truncated inputs will also be rejected. +.PP +The input or output file must be a regular file, as it may be rewound +and re-read or re-written multiple times. +.PP +The dump or undump operation is carried out inside a single +transaction, so it should seem atomic from the point of view of +anything else accessing the databases. +.PP +The server performs normal database recovery on startup. However if +the database needs normal recovery before an undump can succeed and +you don't want to start the server for some reason then the +.B --recover +operation is available for this purpose. No other process should be +accessing the database at the time. +.PP +DisOrder does not currently support catastrophic recovery. +.PP +This program requires write access to DisOrder's databases. Ideally +therefore it should be run as the same user as the server or as root. +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorderd\fR(8) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:acbd58712d46b2a4559b49a099126acf diff --git a/doc/disorder-rescan.8.in b/doc/disorder-rescan.8.in new file mode 100644 index 0000000..e66894e --- /dev/null +++ b/doc/disorder-rescan.8.in @@ -0,0 +1,48 @@ +.\" +.\" Copyright (C) 2005 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder-rescan 8 +.SH NAME +disorder-rescan \- DisOrder rescanner +.SH SYNOPSIS +.B disorder-rescan +.RI [ OPTIONS ] +.RI [ PATH ...] +.SH DESCRIPTION +.B disorder-rescan +is DisOrder's rescan rescanner. It is invoked by DisOrder when +necessary and does not need to be invoked manually. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:w32jqhcFaBEGCakBFZI0GQ diff --git a/doc/disorder.1.in b/doc/disorder.1.in new file mode 100644 index 0000000..369f2ca --- /dev/null +++ b/doc/disorder.1.in @@ -0,0 +1,346 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder 1 +.SH NAME +disorder \- DisOrder jukebox client +.SH SYNOPSIS +.B disorder +.RI [ OPTIONS ] +.RB [ -- ] +.RI [ COMMANDS ...] +.br +.B disorder +.B --length +.RI [ OPTIONS ] +.RB [ -- ] +.IR PATH ... +.SH DESCRIPTION +Without the \fB--length\fR option, +.B disorder +is used to query the \fBdisorderd\fR(8) daemon from the command line. +It may be used to request tracks, scratch tracks, query the current +state, etc, and by an administrator to shutdown or reconfigure the +daemon. +.PP +If no commands are specified then \fBdisorder\fR connects to the +daemon and then immediately disconnects. This can be used to test +whether the daemon is running. Otherwise, it executes the commands +specified. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --length\fR, \fB-L +Calculate the length in seconds of the files specified using the tracklength +plugin. +.TP +.B --version\fR, \fB-V +Display version number. +.TP +.B --help-commands\fR, \fB-H +List all known commands. +.SH COMMANDS +.TP +.B dirs \fIDIRECTORY\fR [\fB~\fIREGEXP\fR] +List all the directories in \fIDIRECTORY\fR. +.IP +An optional regexp may be specified, marked with an initial \fB~\fR. Only +directories with a basename matching the regexp will be returned. +.TP +.B disable +Disables playing after the current track finishes. +.TP +.B enable +(Re-)enable playing. +.TP +.B files \fIDIRECTORY\fR [\fB~\fIREGEXP\fR] +List all the files in \fIDIRECTORY\fR. +.IP +An optional regexp may be specified, marked with an initial \fB~\fR. Only +files with a basename matching the regexp will be returned. +.TP +.B get \fITRACK\fR \fIKEY\fR +Display the preference \fIKEY\fR for \fITRACK\fR. +.TP +.B get-global \fIKEY\fR +Get a global preference. +.TP +.B get-volume +Displays the current volume settings. +.TP +.B length \fITRACK\fR +Reports the length of \fITRACK\fR in seconds. +.TP +.B log +Writes event log messages to standard output, until the server is terminated. +See \fBdisorder_protocol\fR (5) for details of the output syntax. +.TP +.B move \fITRACK\fR \fIDELTA\fR +Move +.I TRACK +by +.I DELTA +within the queue. Positive values move towards the head of the queue, negative +values towards the tail. +.IP +Note that if you specify a negative value then the +.B -- +option separate (before all commands) becomes mandatory, as otherwise the +negative value is misinterpreted an an option. +.TP +.B part \fITRACK\fR \fICONTEXT\fI \fIPART\fR +Get a track name part. +.IP +\fICONTEXT\fR should be either \fBsort\fR or \fBdisplay\fR. \fBpart\fR is the +part of the name desired, typically \fBartist\fR, \fBalbum\fR or \fBtitle\fR. +.TP +.B pause +Pause the current track. (Note that not all players support pausing.) +.TP +.B play \fITRACKS\fR... +Add \fITRACKS\fR to the end of the queue. +.TP +.B playing +Report the currently playing track. +.TP +.B prefs \fITRACK\fR +Display all the preferences for \fITRACK\fR. +.TP +.B queue +List the current queue. The first entry in the list is the next track to play. +.TP +.B random-disable +Disable random play. +.TP +.B random-enable +Enable random play. +.TP +.B recent +List recently played tracks. The first entry is the oldest track, the last +entry is the most recently played one. +.TP +.B remove \fITRACK\fR +Remove a track from the queue. +.TP +.B resolve \fITRACK\fR +Resolve aliases for \fITRACK\fR and print out the real track name. +.TP +.B resume +Resume the current track after a pause. +.TP +.B scratch +Scratch the currently playing track. +.TP +.B scratch-id \fIID\fR +Scratch the currently playing track, provided it has the given ID. +.TP +.B search \fITERMS\fR +Search for tracks containing all of the listed terms. The terms are +separated by spaces and form a single argument, so must be quoted, +for example: +.IP +.B "disorder search 'bowie china'" +.IP +You can limit the search to tracks with a particular tag, too, using the +\fBtag:\fR modifier. For example: +.IP +.B "disorder search 'love tag:depressing' +.TP +.B set \fITRACK\fR \fIKEY\fR \fIVALUE\fR +Set the preference \fIKEY\fR for \fITRACK\fR to \fIVALUE\fR. +.TP +.B set-global \fIKEY\fR \fIVALUE\fR +Set a global preference. +.TP +.B set-volume \fBLEFT\fR \fBRIGHT\fR +Sets the volume. +.TP +.B stats +List server statistics. +.TP +.B tags +List known tags. +.TP +.B unset \fITRACK\fR \fIKEY\fR +Unset the preference \fIKEY\fR for \fITRACK\fR. +.TP +.B unset-global \fIKEY\fR +Unset the global preference \fIKEY\fR. +.TP +.B version +Report the daemon's version number. +.PP +For +.B move +and +.BR remove , +tracks may be specified by name or by ID. If you use the name and a track +appears twice in the queue it is undefined which is affected. +.SS "Privileged Commands" +These commands are only available to privileged users. +.TP +.B become \fIUSER\fR +Become another user. +.TP +.B reconfigure +Make the daemon reload its configuration file. +.TP +.B rescan +Rescan the filesystem for new tracks. There is an automatic daily rescan but +if you've just added some tracks and want them to show up immediately, use this +command. +.TP +.B shutdown +Shut down the daemon. +.SH PREFERENCES +Currently the following preferences are supported. Some are expected +to be set by users, others updated automatically by plugins. +.TP +.B pick_at_random +If this preference is present and set to "0" then the track will not +be picked for random play. Otherwise it may be. +.TP +.B played +A decimal integer giving the number times the track was played. This +includes tracks that are scratched or were picked at random. +.TP +.B played_time +The last time the track was played, as a \fBtime_t\fR converted to a +decimal integer. +.TP +.B scratched +The number of times the track has been scratched. +.TP +.B requested +A decimal integer giving the number of times the track was requested. +(Tracks that are removed before being played are not counted.) +.TP +.B tags +Tags that apply to this track, separated by commas. Tags can contain any +printing character except comma. Leading and trailing spaces are not +significant but internal spaces are. +.IP +Using the +.B required-tags +and +.B prohibited-tags +global preferences, it is possible to limit the tracks that will be selected at +random. +.TP +.B trackname_\fICONTEXT\fB_\fIPART\fR +These preferences can be used to override the filename parsing rules +to find a track name part. For backwards compatibility, +\fBtrackname_\fIPART\fR will be used if the full version +is not present. +.TP +.B unscratched +The number of times the track has been played to completion without +being scratched. +.SH "Superuser Commands" +These commands will (generally) only work for root, who must be a privileged +user. +.TP +.B authorize \fIUSER\fR +Chooses a password for \fIUSER\fR and adds it to \fIconfig.private\fR. Also +creates an appropriate \fIconfig.USER\fR, be owned by the user. +.IP +If at least one \fBauthorize\fR command succeeds then the server is +automatically told to re-read its configuration. +.SH NOTES +.B disorder +is locale-aware. If you do not set the locale correctly then it may +not handle non-ASCII data properly. +.PP +The client determines which user to attempt to authenticate as by +examining the current UID. +.PP +This program is not intended to run in a setuid environment. +.PP +The regexp syntax used by the \fBfiles\fR and \fBdirs\fR commands use the +syntax described in \fBpcrepattern\fR(3). Matching is case-independent. It is +strongly recommended that you quote regexps, since they often contain +characters treated specially by the shell. For example: +.PP +.B "disorder dirs /Music ~'^(?!the [^t])t'" +.SH TROUBLESHOOTING +If you cannot play a track, or it does not appear in the database even after a +rescan, check the following things: +.TP +.B . +Are there any error messages in the system log? The server logs to +\fBLOG_DAEMON\fR, which typically ends up in +.I /var/log/daemon.log +or +.IR /var/log/messages , +though this depends on local configuration. +.TP +.B . +Is the track in a known format? Have a look at +.I pkgconfdir/config +for the formats recognized by the local installation. The filename matching is +case-sensitive. +.TP +.B . +Do permissions on the track allow the server to read it? +.TP +.B . +Do the permissions on the containing directories allow the server to read and +execute them? +.PP +The user the server runs as is determined by the \fBuser\fR directive in the +configuration file. The README recommends using \fBjukebox\fR for this purpose +but it could be different locally. +.SH ENVIRONMENT +.TP +.B LOGNAME +The default username. +.TP +.B HOME +The user's home directory. +.TP +.B LC_ALL\fR, \fBLANG\fR, etc +Current locale. See \fBlocale\fR(7). +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.TP +.I ~/.disorder/passwd +Per-user password file +.TP +.I pkgstatedir/socket +Communication socket for \fBdisorder\fR(1). +.SH "SEE ALSO" +\fBdisorderd\fR(8), \fBdisorder_config\fR(5), \fBsyslog\fR(3), \fBtime\fR(2), +\fBpcrepattern\fR(3), \fBdisobedience\fR(1) +.PP +"\fBpydoc disorder\fR" for the Python API documentation. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:f4ecee92157cdac8ee0f71c3fc14044d diff --git a/doc/disorder.3 b/doc/disorder.3 new file mode 100644 index 0000000..62508f5 --- /dev/null +++ b/doc/disorder.3 @@ -0,0 +1,430 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder 3 +.SH NAME +disorder \- plugin interface to DisOrder jukebox +.SH SYNOPSIS +.B "#include <disorder.h>" +.SH DESCRIPTION +This header file defines the plugin interface to DisOrder. +.PP +The first half of this man page describes the functions DisOrder +provides to plugins; the second half describes the functions that +plugins must provide. +.SH "MEMORY ALLOCATION" +DisOrder uses a garbage collector internally. Therefore it is recommended that +plugins use the provided memory allocation interface, rather than calling +\fBmalloc\fR(3) etc directly. +.PP +.nf +\fBvoid *disorder_malloc(size_t); +void *disorder_realloc(void *, size_t); +.fi +.IP +These functions behave much like \fBmalloc\fR(3) and \fBrealloc\fR(3) +except that they never fail; they always zero out the memory +allocated; and you do not need to free the result. +.IP +They may still return a null pointer if asked for a 0-sized +allocation. +.PP +.nf +\fBvoid *disorder_malloc_noptr(size_t); +void *disorder_realloc_noptr(void *, size_t); +.fi +.IP +These functions are like \fBmalloc\fR(3) and \fBrealloc\fR(3) +except that they never fail and you must not put any pointer +values in the allocated memory. +.IP +They may still return a null pointer if asked for a 0-sized +allocation. They do not guarantee to zero out the memory allocated. +.PP +.nf +\fBchar *disorder_strdup(const char *); +char *disorder_strndup(const char *, size_t); +.fi +.IP +These functions are like \fBstrdup\fR(3) and \fBstrndup\fR(3) except +that they never fail and you do not need to free the result. +.PP +.nf +\fBint disorder_asprintf(char **rp, const char *fmt, ...); +int disorder_snprintf(char buffer[], size_t bufsize, + const char *fmt, ...); +.fi +.IP +These function are like \fBsnprintf\fR(3) and \fBasprintf\fR(3). +.B disorder_asprintf +never fails on memory allocation and +you do not need to free the results. +.IP +Floating point conversions and wide character support are not +currently implemented. +.PP +"Never fail" in the above means that the process is terminated on error. +.SH LOGGING +Standard error doesn't reliably go anywhere in current versions of DisOrder, +and whether syslog is to be used varies depending on how the program is +invoked. Therefore plugins should use these functions to log any errors or +informational messages. +.PP +.nf +\fBvoid disorder_error(int errno_value, const char *fmt, ...); +.fi +.IP +Log an error message. If \fBerrno_value\fR is not 0 then the relevant +string is included in the error message. +.PP +.nf +\fBvoid disorder_fatal(int errno_value, const char *fmt, ...); +.fi +.IP +Log an error message and then terminate the process. If +\fBerrno_value\fR is not 0 then the relevant string is included in the +error message. +.IP +.B disorder_fatal +is the right way to terminate the process if a fatal error arises. +You shouldn't usually try to use \fBexit\fR(3) or \fB_exit\fR(2). +.PP +.nf +\fBvoid disorder_info(const char *fmt, ...); +.fi +.IP +Log a message. +.IP +.SH "TRACK DATABASE" +The functions in this section provide a way of accessing the track database. +In server plugins these access the database directly; in client plugins the +requests are transmitted to the server over a socket. +.PP +All strings in this section are encoded using UTF-8. +.PP +.nf +\fBint disorder_track_exists(const char *track); +.fi +.IP +This function returns non-0 if \fBtrack\fR exists and 0 if it does +not. +.PP +.nf +\fBconst char *disorder_track_get_data(const char *track, + const char *key); +.fi +.IP +This function looks up the value of \fBkey\fR for \fBtrack\fR and +returns a pointer to a copy of it. Do not bother to free the pointer. +If the track or key are not found a null pointer is returned. +.PP +.nf +\fBint disorder_track_set_data(const char *track, + const char *key, + const char *value); +.fi +.IP +This function sets the value of \fBkey\fR for \fBtrack\fR to +\fBvalue\fR. On success, 0 is returned; on error, -1 is returned. +.IP +If \fBvalue\fR is a null pointer then the preference is deleted. +.IP +Values starting with an underscore are stored in the tracks database, +and are lost if the track is deleted; they should only ever have +values that can be regenerated on demand. Other values are stored in +the prefs database and never get automatically deleted. +.PP +.nf +\fBconst char *disorder_track_random(void) +.fi +.IP +Returns a pointer to a copy of the name of a randomly chosen track. +Each non-alias track has an equal probability of being chosen. +Aliases are never returned. +Only available in server plugins. +.SH "PLUGIN FUNCTIONS" +This section describes the functions that you must implement to write various +plugins. All of the plugins have at least one standard implementation +available in the DisOrder source. +.PP +Some functions are listed as only available in server plugins. +Currently this means that they are not even defined outside the +server. +.PP +All strings in this section are encoded using UTF-8. +.SS tracklength.so +This is a server plugin. +.PP +.nf +\fBlong disorder_tracklength(const char *track, + const char *path); +.fi +.IP +Called to calculate the length of a track. \fBtrack\fR is the track +name (UTF-8) and \fBpath\fR is the path name if there was one, or a +null pointer otherwise. \fBpath\fR will be the same byte string return from +the scanner plugin, and so presumably encoded according to the +filesystem encoding. +.IP +If the return value is positive it should be the track length in +seconds (round up if it is not an integral number of seconds long). +.IP +If the return value is zero then the track length is unknown. +.IP +If the return value is negative then an error occurred determining the +track length. +.PP +Tracklength plugins are invoked from a subprocess of the server, so +they can block without disturbing the server's operation. +.SS notify.so +This is a server plugin. +.PP +.nf +\fBvoid disorder_notify_play(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR is about to be played. \fBsubmitter\fR identifies the +submitter or is a null pointer if the track was picked for random play. +.PP +.nf +\fBvoid disorder_notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +.fi +.IP +Called when \fBtrack\fR is scratched by \fBscratcher\fR. \fBsubmitter\fR +identifies the submitter or is a null pointer if the track was picked for +random play. \fBseconds\fR is the number of seconds since the track started +playing. +.PP +.nf +\fBvoid disorder_notify_not_scratched(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR completes without being scratched (an error might have +occurred though). \fBsubmitter\fR identifies the submitter or is a null +pointer if the track was picked for random play. +.PP +.nf +\fBvoid disorder_notify_queue(const char *track, + const char *submitter); +.fi +.IP +Called when \fBtrack\fR is added to the queue by \fBsubmitter\fR +(which is never a null pointer). Not called for scratches. +.PP +.nf +\fBvoid disorder_notify_queue_remove(const char *track, + const char *remover); +.fi +.IP +Called when \fBtrack\fR is removed from queue by \fBremover\fR (which +is never a null pointer). +.PP +.nf +\fBvoid disorder_notify_queue_move(const char *track, + const char *remover); +.fi +.IP +Called when \fBtrack\fR is moved in the queue by \fBmover\fR +(which is never a null pointer). +.PP +.nf +\fBvoid disorder_notify_pause(const char *track, + const char *who); +.fi +.IP +Called when \fBtrack\fR is paused by \fBwho\fR +(which might be a null pointer). +.PP +.nf +\fBvoid disorder_notify_resume(const char *track, + const char *who); +.fi +.IP +Called when \fBtrack\fR is resumed by \fBwho\fR +(which might be a null pointer). +.SS "Scanner Plugins" +Scanner plugins are server plugins and may have any name; they are +chosen via the configuration file. +.PP +.nf +\fBvoid disorder_scan(const char *root); +.fi +.IP +Write a list of files below \fBroot\fR to standard output. Each +filename should be in the encoding defined for this root in the +configuration file and should be terminated by character 0. +.IP +It is up to the plugin implementor whether they prefer to use stdio or +write to file descriptor 1 directly. +.IP +All the filenames had better start with \fBroot\fR as this is used to +match them back up to the right collection to call +\fBdisorder_check\fR on. +.PP +.nf +\fBint disorder_check(const char *root, const char *path); +.fi +.IP +Check whether file \fBpath\fR under \fBroot\fR still exists. Should +return 1 if it exists, 0 if it does not and -1 on error. This is run +in the main server process. +.PP +Both scan and recheck are executed inside a subprocess, so it will not +break the server if they block for an extended period (though of +course, they should not gratuitously take longer than necessary to do +their jobs). +.SS "Player plugins" +Player plugins are server plugins and may have any name; they are +chosen via the configuration file. +.PP +.nf +extern const unsigned long disorder_player_type; +.fi +.IP +This defines the player type and capabilities. It should consist of a +single type value ORed with any number of capability values. The +following are known type values: +.RS +.TP +.B DISORDER_PLAYER_STANDALONE +A standalone player that writes directly to some suitable audio +device. +.TP +.B DISORDER_PLAYER_RAW +A player that writes raw samples to \fB$DISORDER_RAW_FD\fR, for +instance by using the \fBdisorder\fR libao driver. +.RE +.IP +Known capabilities are: +.RS +.TP +.B DISORDER_PLAYER_PREFORK +Supports the prefork and cleanup calls. +.TP +.B DISORDER_PLAYER_PAUSES +Supports the pause and resume calls. +.RE +.PP +.nf +\fBvoid *disorder_play_prefork(const char *track); +.fi +.IP +Called before a track is played, if \fB_PREFORK\fR is set. +\fBtrack\fR is the name of the track in UTF-8. This function must +never block, as it runs inside the main loop of the server. +.IP +The return value will be passed to the functions below as \fBdata\fR. +On error, a null pointer should be returned. +.PP +.nf +\fBvoid disorder_play_cleanup(void *data); +.fi +.IP +Called after a track has been completed, if \fB_PREFORK\fR is set, for +instance to release the memory used by \fBdata\fR. This function must +never block, as it runs inside the main loop of the server. +.PP +.nf +\fBvoid disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void *data); +.fi +.IP +Play a track. +.IP +\fBpath\fR is the path name as originally encoded in the filesystem. +This is the value you should ultimately pass to \fBopen\fR(2). +.IP +\fBtrack\fR is the path name converted to UTF-8. This value (possibly +converted to some other encoding) should be used in any logs, etc. +.IP +If there is no meaningful path, or if the track is a scratch (where no +filename encoding information is available), \fBpath\fR will be equal +to \fBtrack\fR. +.IP +The parameters are any additional arguments +supplied to the \fBplayer\fR configuration file command. +.IP +This function is always called inside a fork, and it should not return +until playing has finished. +.IP +DisOrder sends the subprocess a signal if the track is to be scratched +(and when \fBdisorderd\fR is shut down). By default this signal is +\fBSIGKILL\fR but it can be reconfigured. +.PP +.nf +\fBint disorder_play_pause(long *playedp, + void *data); +.fi +.IP +Pauses the current track, for players that support pausing. This +function must never block, as it runs inside the main loop of the +server. +.IP +On success, should return 0 and set \fB*playedp\fR to the number of +seconds played so far of this track, or to -1 if this cannot be +determined. +.IP +On error, should return -1. +.PP +.nf +\fBvoid disorder_play_resume(void *data); +.fi +.IP +Resume playing the current track after a pause. This function must +never block, as it runs inside the main loop of the server. +.SH NOTES +There is no special DisOrder library to link against; the symbols are +exported by the executables themselves. +(You should NOT try to link against \fB-ldisorder\fR.) +Plugins must be separately +linked against any other libraries they require, even if the DisOrder +executables are already linked against them. +.PP +The easiest approach is probably to develop the plugin inside the +DisOrder tree; then you can just use DisOrder's build system. This +might also make it easier to submit patches if you write something of +general utility. +.PP +Failing that you can use Libtool, if you make sure to pass the +\fB-module\fR option. For current versions of DisOrder you only need +the shared object itself, not the \fB.la\fR file. +.PP +If you know the right runes for your toolchain you could also build +the modules more directly. +.PP +It is possible, up to a point, to implement several plugin interfaces +from within a single shared object. If you ever use any of the +functions that are listed as only being available in server plugins, +though, then you can only use the resulting shared object as a server +plugin. +.SH "SEE ALSO" +.BR disorderd (8), +.BR disorder (1), +.BR disorder_config (5) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:ff45ed6fb0bc4fff61a4179138e26f01 diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in new file mode 100644 index 0000000..d9bd260 --- /dev/null +++ b/doc/disorder_config.5.in @@ -0,0 +1,1022 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder_config 5 +.SH NAME +pkgconfdir/config - DisOrder jukebox configuration +.SH DESCRIPTION +The purpose of DisOrder is to organize and play digital audio files, under the +control of multiple users. \fIpkgconfdir/config\fR is the primary +configuration file but this man page currently documents all of its various +configuration files. +.SS Tracks +DisOrder can be configured with multiple collections of tracks, indexing them +by their filename, and picking players on the basis of filename patterns (for +instance, "*.mp3"). +.PP +Although the model is of filenames, it is not inherent that there are +corresponding real files - merely that they can be interpreted by the chosen +player. See \fBdisorder\fR(3) for more details about this. +.PP +Each track can have a set of preferences associated with it. These are simple +key-value pairs; they can be used for anything you like, but a number of keys +have specific meanings. See \fBdisorder\fR(1) for more details about these. +.SS "Track Names" +Track names are derived from filenames under the control of regular +expressions, rather than attempting to interpret format-specific embedded name +information. They can be overridden by setting preferences. +.PP +Names for display are distinguished from names for sorting, so with the right +underlying filenames an album can be displayed in its original order even if +the displayed track titles are not lexically sorted. +.SS "Server State" +A collection of global preferences define various bits of server state: whether +random play is enabled, what tags to check for when picking at random, etc. +.SS "Users And Access Control" +DisOrder distinguishes between multiple users. This is for access control and +reporting, not to provide different views of the world: i.e. preferences and so +on are global. +.PP +It's possible to restrict a small number of operations to a specific subset of +users. However, it is assumed that every user is supposed to be able to do +most operations - since the users are all sharing the same audio environment +they are expected to cooperate with each other. +.PP +Access control is entirely used-based. If you configure DisOrder to listen for +TCP/IP connections then it will accept a connection from anywhere provided the +right password is available. Passwords are never transmitted over TCP/IP +connections in clear, but everything else is. The expected model is that +host-based access control is imposed at the network layer. +.SS "Web Interface" +The web interface is controlled by a collection of template files, one for each +kind of page, and a collection of option files. These are split up and +separate from the main configuration file to make it more convenient to +override specific bits. +.PP +The web interface connects to the DisOrder server like any other user, though +it is given a special privilege to "become" any other user. (Thus, any process +with the same UID as the web interface is very powerful as far as DisOrder +goes.) +.PP +Access control to the web interface is (currently) separate from DisOrder's own +access control (HTTP authentication is required) but uses the same user +namespace. +.SH "CONFIGURATION FILE" +.SS "General Syntax" +Lines are split into fields separated by whitespace (space, tab, line +feed, carriage return, form feed). Comments are started by the number +sign ("#"). +.PP +Fields may be unquoted (in which case they may not contain spaces and +may not start with a quotation mark or apostrophe) or quoted by either +quotation marks or apostrophes. Inside quoted fields every character +stands for itself, except that a backslash can only appear as part of +one of the following escape sequences: +.TP +.B \e\e +Backslash +.TP +.B \e" +Quotation mark +.\" " +.TP +.B \e' +Apostrophe +.TP +.B \en +Line feed +.PP +No other escape sequences are allowed. +.PP +Within any line the first field is a configuration command and any +further fields are parameters. Lines with no fields are ignored. +.PP +After editing the config file use \fBdisorder reconfigure\fR to make +it re-read it. If there is anything wrong with it the daemon will +record a log message and ignore the new config file. (You should fix +it before next terminating and restarting the daemon, as it cannot +start up without a valid config file.) +.SS "Global Configuration" +.TP +.B home \fIDIRECTORY\fR +The home directory for state files. Defaults to +.IR pkgstatedir . +.TP +.B plugin \fIPATH\fR +Adds a directory to the plugin path. (This is also used by the web +interface.) +.IP +Plugins are opened the first time they are required and never after, +so after changing a plugin you must restart the server before it is +guaranteed to take effect. +.SS "Server Configuration" +.TP +.B alias \fIPATTERN\fR +Defines the pattern use construct virtual filenames from \fBtrackname_\fR +preferences. +.IP +Most characters stand for themselves, the exception being \fB{\fR which is used +to insert a track name part in the form \fB{\fIname\fB}\fR or +\fB{/\fIname\fB}\fR. +.IP +The difference is that the first form just inserts the name part while the +second prefixes it with a \fB/\fR if it is nonempty. +.IP +The pattern should not attempt to include the collection root, which is +automatically included, but should include the proper extension. +.IP +The default is \fB{/artist}{/album}{/title}{ext}\fR. +.TP +.B channel \fICHANNEL\fR +The mixer channel that the volume control should use. Valid names depend on +your operating system and hardware, but some standard ones that might be useful +are: +.RS +.TP 8 +.B pcm +Output level for the audio device. This is probably what you want. +.TP +.B speaker +Output level for the PC speaker, if that is connected to the sound card. +.TP +.B pcm2 +Output level for alternative codec device. +.TP +.B vol +Master output level. The OSS documentation recommends against using this, as +it affects all output devices. +.RE +.IP +You can also specify channels by number, if you know the right value. +.TP +.B collection \fIMODULE\fR \fIENCODING\fR \fIROOT\fR +Define a collection of tracks. +.IP +\fIMODULE\fR defines which plugin module should be used for this +collection. Use the supplied \fBfs\fR module for tracks that exists +as ordinary files in the filesystem. +.IP +\fIENCODING\fR defines the encoding of filenames in this collection. +For \fBfs\fR this would be the encoding you use for filenames. +Examples might be \fBiso-8859-1\fR or \fButf-8\fR. +.IP +\fIROOT\fR is the root in the filesystem of the filenames and is +passed to the plugin module. +.TP +.B device \fINAME\fR +ALSA device to play raw-format audio. Default is \fBdefault\fR, i.e. to use +the whatever the ALSA configured default is. +.TP +.B gap \fISECONDS\fR +Specifies the number of seconds to leave between tracks. The default +is 2. +.TP +.B history \fIINTEGER\fR +Specifies the number of recently played tracks to remember (including +failed tracks and scratches). +.TP +.B listen \fR[\fIHOST\fR] \fISERVICE\fR +Listen for connections on the address specified by \fIHOST\fR and port +specified by \fISERVICE\fR. If \fIHOST\fR is omitted then listens on all +local addresses. +.IP +Normally the server only listens on a UNIX domain socket. +.TP +.B lock yes\fR|\fBno +Determines whether the server locks against concurrent operation. Default is +\fByes\fR. +.TP +.B mixer \fIPATH\fR +The path to the mixer device, if you want access to the volume control, +e.g. \fB/dev/mixer\fR. +.TP +.B namepart \fIPART\fR \fIREGEXP\fR \fISUBST\fR [\fICONTEXT\fR [\fIREFLAGS\fR]] +Determines how to extract trackname part \fIPART\fR from a +track name (with the collection root part removed). +Used in \fB@recent@\fR, \fB@playing@\fR and \fB@search@\fR. +.IP +Track names can be different in different contexts. For instance the sort +string might include an initial track number, but this would be stripped for +the display string. \fICONTEXT\fR should be a glob pattern matching the +contexts in which this directive will be used. +.IP +Valid contexts are \fBsort\fR and \fBdisplay\fR. +.IP +All the \fBnamepart\fR directives are considered in order. The +first directive for the right part, that matches the desired context, +and with a \fIREGEXP\fR that +matches the track is used, and the value chosen is constructed from +\fISUBST\fR according to the substitution rules below. +.IP +Note that searches use the raw track name and \fBtrackname_\fR preferences but +not (currently) the results of \fBnamepart\fR, so generating words via this option +that aren't in the original track name will lead to confusing results. +.IP +If you supply no \fBnamepart\fR directives at all then a default set will be +supplied automatically. But if you supply even one then you must supply all of +them. See the example config file for the defaults. +.TP +.B nice_rescan \fIPRIORITY\fR +Set the recan subprocess priority. The default is 10. +.IP +(Note that higher values mean the process gets less CPU time; UNIX priority +values are the backwards.) +.TP +.B nice_server \fIPRIORITY\fR +Set the server priority. This is applied to the server at startup time (and +not when you reload configuration). The server does not use much CPU itself +but this value is inherited by programs it executes. If you have limited CPU +then it might help to set this to a small negative value. The default is 0. +.TP +.B nice_speaker \fIPRIORITY\fR +Set the speaker process priority. This is applied to the speaker process at +startup time (and not when you reload the configuration). The speaker process +is not massively CPU intensive by today's standards but depends on reasonably +timely scheduling. If you have limited CPU then it might help to set this to a +small negative value. The default is 0. +.TP +.B player \fIPATTERN\fR \fIMODULE\fR [\fIOPTIONS.. [\fB--\fR]] \fIARGS\fR... +Specifies the player for files matching the glob \fIPATTERN\fR. \fIMODULE\fR +specifies which plugin module to use. +.IP +The following options are supported: +.RS +.TP +.B --wait-for-device\fR[\fB=\fIDEVICE\fR] +Waits (for up to a couple of seconds) for the default, or specified, libao +device to become openable. +.TP +.B -- +Defines the end of the list of options. Needed if the first argument to the +plugin starts with a "-". +.RE +.IP +The following are the standard modules: +.RS +.TP +.B exec \fICOMMAND\fR \fIARGS\fR... +The command is executed via \fBexecvp\fR(3), not via the shell. +The \fBPATH\fR environment variable is searched for the executable if it is not +an absolute path. +The command is expected to know how to open its own sound device. +.TP +.B execraw \fICOMMAND\fR \fIARGS\fR... +Identical to the \fBexec\fR except that the player is expected to use the +DisOrder raw player protocol (see notes below). +.TP +.B shell \fR[\fISHELL\fR] \fICOMMAND\fR +The command is executed using the shell. If \fISHELL\fR is specified then that +is used, otherwise \fBsh\fR will be used. In either case the \fBPATH\fR +environment variable is searched for the shell executable if it is not an +absolute path. The track name is stored in the environment variable +\fBTRACK\fR. +.IP +Be careful of the interaction between the configuration file quoting rules and +the shell quoting rules. +.RE +.IP +If multiple player commands match a track then the first match is used. +.TP +.B prefsync \fISECONDS\fR +The interval at which the preferences log file will be synchronised. Defaults +to 3600, i.e. one hour. +.TP +.B signal \fINAME\fR +Defines the signal to be sent to track player process groups when tracks are +scratched. The default is \fBSIGKILL\fR. +.IP +Signals are specified by their full C name, i.e. \fBSIGINT\fR and not \fBINT\fR +or \fBInterrupted\fR or whatever. +.TP +.B restrict \fR[\fBscratch\fR] [\fBremove\fR] [\fBmove\fR] +Determine which operations are restricted to the submitter of a +track. By default, no operations are restricted, i.e. anyone can +scratch or remove anything. +.IP +If \fBrestrict scratch\fR or \fBrestrict remove\fR are set then only the user +that submitted a track can scratch or remove it, respectively. +.IP +If \fBrestrict move\fR is set then only trusted users can move tracks around in +the queue. +.IP +If \fBrestrict\fR is used more than once then only the final use has any +effect. +.TP +.B scratch \fIPATH\fR +Specifies a scratch. When a track is scratched, a scratch track is +played at random. +Scratches are played using the same logic as other tracks. +.IP +At least for the time being, path names of scratches must be encoded using +UTF-8 (which means that ASCII will do). +.TP +.B stopword \fIWORD\fR ... +Specifies one or more stopwords that should not take part in searches +over track names. +.SS "Client Configuration" +.TP +.B connect \fR[\fIHOST\fR] \fISERVICE\fR +Connect to the address specified by \fIHOST\fR and port specified by +\fISERVICE\fR. If \fIHOST\fR is omitted then connects to the local host. +Normally the UNIX domain socket is used instead. +.SS "Web Interface Configuration" +.TP +.B refresh \fISECONDS\fR +Specifies the maximum refresh period in seconds. Default 15. +.TP +.B templates \fIPATH\fR ... +Specifies the directory containing templates used by the web +interface. If a template appears in more than one template directory +then the one in the earliest directory specified is chosen. +.IP +See below for further details. +.TP +.B transform \fITYPE\fR \fIREGEXP\fR \fISUBST\fR [\fICONTEXT\fR [\fIREFLAGS\fR]] +Determines how names are sorted and displayed in track choice displays. +.IP +\fITYPE\fR is the type of transformation; usually \fBtrack\fR or +\fBdir\fR but you can define your own. +.IP +\fICONTEXT\fR is a glob pattern matching the context. Standard contexts are +\fBsort\fR (which determines how directory names are sorted) and \fBdisplay\fR +(which determines how they are displayed). Again, you can define your +own. +.IP +All the \fBtransform\fR directives are considered in order. If +the \fITYPE\fR, \fIREGEXP\fR and the \fICONTEXT\fR match +then a new track name is constructed from +\fISUBST\fR according to the substitution rules below. If several +match then each is executed in order. +.IP +If you supply no \fBtransform\fR directives at all then a default set will be +supplied automatically. But if you supply even one then you must supply all of +them. See the example config file for the defaults. +.TP +.B url \fIURL\fR +Specifies the URL of the web interface. This URL will be used in +generated web pages. +.IP +This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not +\fB/cgi-bin/jukebox\fR. +.SS "Authentication Configuration" +.TP +.B allow \fIUSERNAME\fR \fIPASSWORD\fR +Specify a username/password pair. +.TP +.B password \fIPASSWORD\fR +Specify password. +.TP +.B trust \fIUSERNAME\fR +Allow \fIUSERNAME\fR to perform privileged operations such as shutting +down or reconfiguring the daemon, or becoming another user. +.TP +.B user \fIUSER\fR +Specifies the user to run as. Only makes sense if invoked as root (or +the target user). +.TP +.B username \fIUSERNAME\fR +Specify username. The default is taken from the environment variable +\fBLOGNAME\fR. +.PP +Configuration files are read in the following order: +.TP +.I pkgconfdir/config +.TP +.I pkgconfdir/config.private +Should be readable only by the jukebox group, and contain \fBallow\fR +commands for authorised users. +.TP +.I pkgconfdir/config.\fRUSER +Per-user system-controlled client configuration. Optional but if it +exists must be readable only by the relevant user. Would normally +contain a \fBpassword\fR directive. +.TP +.I ~\fRUSER\fI/.disorder/passwd +Per-user client configuration. Optional but if it exists must be +readable only by the relevant user. Would normally contain a +\fBpassword\fR directive. +.SH "GLOBAL PREFERENCES" +These are the values set with \fBset-global\fR. +.TP +.B required-tags +If this is set an nonempty then randomly played tracks will always have at +least one of the listed tags. +.IP +Tags can contain any printing character except comma. Leading and trailing +spaces are not significant but internal spaces are. Tags in a list are +separated by commas. +.TP +.B prohibited-tags +If this is set an nonempty then randomly played tracks will never have any of +the listed tags. +.TP +.B playing +If unset or \fByes\fR then play is enabled. Otherwise it is disabled. Use +\fBdisable\fR rather than setting it directly. +.TP +.B random-play +If unset or \fByes\fR then random play is enabled. Otherwise it is disabled. +Use \fBdisable\fR rather than setting it directly. +.SH "LIBAO DRIVER" +.SS "Raw Protocol Players" +Raw protocol players are expected to use the \fBdisorder\fR libao driver. +Programs that use libao generally have command line options to select the +driver and pass options to it. +.SS "Driver Options" +The known driver options are: +.TP +.B fd +The file descriptor to write to. If this is not specified then the driver +looks like the environment variable \fBDISORDER_RAW_FD\fR. If that is not set +then the default is 1 (i.e. standard output). +.TP +.B fragile +If this is set to a nonzero value then the driver will call \fB_exit\fR(2) if a +write to the output file descriptor fails. This is a workaround for buggy +players such as \fBogg123\fR that ignore write errors. +.SH "WEB TEMPLATES" +When \fBdisorder.cgi\fR wants to generate a page for an action it searches the +directories specified with \fBtemplates\fR for a matching file. It is +suggested that you leave the distributed templates unchanged and put +any customisations in an earlier entry in the template path. +.PP +The supplied templates are: +.TP +.B about.html +Display information about DisOrder. +.TP +.B choose.html +Navigates through the track database to choose a track to play. The +\fBdir\fR argument gives the directory to look in; if it is missing +then the root directory is used. +.TP +.B choosealpha.html +Provides a front end to \fBchoose.html\fR which allows subsets of the top level +directories to be selected by initial letter. +.TP +.B playing.html +The "front page", which usually shows the currently playing tracks and +the queue. +Gets an HTTP \fBRefresh\fR header. +.IP +If the \fBmgmt\fR CGI argument is set to \fBtrue\fR then we include extra +buttons for moving tracks up and down in the queue. There is some logic in +\fBdisorder.cgi\fR to ensure that \fBmgmt=true\fR is preserved across refreshes +and redirects back into itself, but URLs embedded in web pages must include it +explicitly. +.TP +.B prefs.html +Views preferences. If the \fBfile\fR, \fBname\fR and \fBvalue\fR arguments are +all set then that preference is modified; if \fBfile\fR and \fBname\fR are set +but not \fBvalue\fR then the preference is deleted. +.TP +.B recent.html +Lists recently played tracks. +.TP +.B search.html +Presents search results. +.TP +.B volume.html +Primitive volume control. +.PP +Additionally, other standard files are included by these: +.TP +.B credits.html +Included at the end of the main content \fB<DIV>\fR element. +.TP +.B sidebar.html +Included at the start of the \fB<BODY>\fR element. +.TP +.B stdhead.html +Included in the \fB<HEAD>\fR element. +.TP +.B stylesheet.html +Contains the default DisOrder stylesheet. You can override this by editing the +CSS or by replacing it all with a \fB<LINK>\fR to an external stylesheet. +.PP +Templates are ASCII files containing HTML documents, with an expansion +syntax to enable data supplied by the implementation to be inserted. +.PP +If you want to use characters outside the ASCII range, use either the +appropriate HTML entity, e.g. \fBé\fR, or an SGML numeric +character reference, e.g. \fBý\fR. Use \fB@\fR to insert a +literal \fB@\fR without falling foul of the expansion syntax. +.SS "Expansion Syntax" +Expansions are surrounded by at ("@") symbols take the form of a keyword +followed by zero or more arguments. Arguments may either be quoted by curly +brackets ("{" and "}") or separated by colons (":"). Both kinds may be mixed +in a single expansion, though doing so seems likely to cause confusion. +The descriptions below contain suggested forms for each +expansion. +.PP +Leading and trailing whitespace in unquoted arguments is ignored, as is +whitespace (including newlines) following a close bracket ("}"). +.PP +Arguments are recursively expanded before being interpreted, except for +\fITEMPLATE\fR arguments. These are expanded (possibly more than once) to +produce the final expansion. +(More than once means the same argument being expanded more than once +for different tracks or whatever, not the result of the first +expansion itself being re-expanded.) +.PP +Strings constructed by expansions (i.e. not literally copied from the template +text) are SGML-quoted: any character which does not stand for itself in #PCDATA +or a quoted attribute value is replaced by the appropriate numeric character +reference. +.PP +The exception to this is that such strings are \fInot\fR quoted when they are +generated in the expansion of a parameter. +.PP +In the descriptions below, the current track means the one set by +\fB@playing@\fR, \fB@recent@\fR or \fB@queue@\fR, not the one that is playing. +If none of these expansions are in force then there is no current track. +\fIBOOL\fR should always be either \fBtrue\fR or \fBfalse\fR. +.SS "Expansions" +The following expansion keywords are defined: +.TP +.B @#{\fICOMMENT\fB}@ +Ignored. +.TP +.B @action@ +The current action. This reports +.B manage +if the action is really +.B playing +but +.B mgmt=true +was set. +.TP +.B @and{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@ +If there are no arguments, or all the arguments are \fBtrue\fB, then expands to +\fBtrue\fR, otherwise to \fBfalse\fR. +.TP +.B @arg:\fINAME\fB@ +Expands to the value of CGI script argument \fINAME\fR. +.TP +.B @basename@ +The basename of the current directory component, in \fB@navigate@\fR. +.TP +.B @basename{\fIPATH\fB}@ +The base name part of \fIPATH\fR. +.TP +.B @choose{\fIWHAT\fB}{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly for each file or directory under +\fB@arg:directory@\fR. +\fIWHAT\fR should be either \fBfile\fR or \fBdirectory\fR. +Use \fB@file@\fR to get the display name or filename of the file or +directory. +Usually used in \fBchoose.html\fR. +.TP +.B @dirname@ +The directory of the current directory component, in \fB@navigate@\fR. +.TP +.B @dirname{\fIPATH\fB}@ +The directory part of \fIPATH\fR. +.TP +.B @enabled@ +Expands to \fBtrue\fR if play is currently enabled, otherwise to \fBfalse\fR. +.TP +.B @eq{\fIA\fB}{\fIB\fB} +Expands to \fBtrue\fR if \fIA\fR and \fIB\fR are identical, otherwise to +\fBfalse\fR. +.TP +.B @file@ +Expands to the filename of the current file or directory, inside the template +argument to \fBchoose\fR. +.TP +.B @files{\fITEMPLATE\fB} +Expands \fITEMPLATE\fB once for each file indicated by the \fBdirectory\fR CGI +arg if it is present, or otherwise for the list of files counted by \fBfiles\fR +with names \fB0_file\fR, \fB1_file\fR etc. +.TP +.B @fullname@ +The full path of the current directory component, in \fB@navigate@\fR. +.TP +.B @id@ +The ID of the current track. +.TP +.B @if{\fIBOOL\fB}{\fITRUEPART\fB}{\fIFALSEPART\fB}@ +If \fIBOOL\fR expands to \fBtrue\fR then expands to \fITRUEPART\fR, otherwise +to \fIFALSEPART\fR (which may be omitted). +.TP +.B @include:\fIPATH\fR@ +Include the named file as if it were a template file. If \fIPATH\fR +starts with a \fB/\fR then it is used as-is; otherwise, ".html" is +appended and the template path is searched. +.TP +.B @index@ +Expands to the index of the current file in \fB@queue@\fR, \fB@recent@\fR or +\fB@files@\fR. +.TP +.B @isdirectories@ +Expands to \fBtrue\fR if there are any directories in \fB@arg:directory@\fR, +otherwise to \fBfalse\fR. +.TP +.B @isfiles@ +Expands to \fBtrue\fR if there are any files in \fB@arg:directory@\fR, +otherwise to \fBfalse\fR. +.TP +.B @isfirst@ +Expands to \fBtrue\fR if this is the first repetition of a \fITEMPLATE\fR +argument in a loop (\fB@queue\fR or similar), otherwise to \fBfalse\fR. +.TP +.B @islast@ +Expands to \fBtrue\fR if this is the last repetition of a \fITEMPLATE\fR in a +loop, otherwise to \fBfalse\fR. +.TP +.B @isplaying@ +Expands to \fBtrue\fR if a track is playing, otherwise to \fBfalse\fR. +.TP +.B @isqueue@ +Expands to \fBtrue\fR if there are any tracks in the queue, otherwise to +\fBfalse\fR. +.TP +.B @isrecent@ +Expands to \fBtrue\fR if the recently played list has any tracks in it, +otherwise to \fBfalse\fR. +.TP +.B @label:\fINAME\fR\fB@ +Expands to the value of label \fINAME\fR. See the shipped \fIoptions.labels\fR +file for full documentation of the labels used by the standard templates. +.TP +.B @length@ +Expands to the length of the current track. +.TP +.B @navigate{\fIDIRECTORY\fB}{\fITEMPLATE\fB} +Expands \fITEMPLATE\fR for each component of \fIDIRECTORY\fR in turn. +Use \fB@dirname\fR and \fB@basename@\fR to get the components of the path to +each component. +Usually used in \fBchoose.html\fR. +.TP +.B @ne{\fIA\fB}{\fIB\fB} +Expands to \fBtrue\fR if \fIA\fR and \fIB\fR differ, otherwise to \fBfalse\fR. +.TP +.B @nfiles@ +Expands to the number of files from \fB@files\fR (above). +.TP +.B @nonce@ +Expands to a string including the time and process ID, intended to be +unique across invocations. +.TP +.B @not{\fIBOOL\fB}@ +Expands to \fBfalse\fR if \fIBOOL\fR is \fBtrue\fR, otherwise to +\fBfalse\fR. +.TP +.B @or{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@ +If at least one argument is \fBtrue\fB, then expands to \fBtrue\fR, otherwise +to \fBfalse\fR. +.TP +.B @parity@ +Expands to \fBeven\fR or \fBodd\fR depending on whether the current track is at +an even or odd position in \fB@queue@\fR, \fB@recent@\fR or \fB@files@\fR. +.TP +.B @part{\fICONTEXT\fB}{\fIPART\fB}@ +Expands to track name part \fIPART\fR using context \fICONTEXT\fR for the +current track. The context may be omitted (and normally would be) and defaults +to \fBdisplay\fR. +.TP +.B @part{\fICONTEXT\fB}{\fIPART\fB}{\fITRACK\fB}@ +Expands to track name part \fIPART\fR using context \fICONTEXT\fR for +\fITRACK\fR. In this usage the context may not be omitted. +.TP +.B @paused@ +Expands to \fBtrue\fR if the current track is paused, else \fBfalse\fR. +.TP +.B @playing{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR using the playing track as the current track. +.TP +.B @pref{\fITRACK\fB}{\fIKEY\fB}@ +Expand to the track preference, or the empty string if it is not set. +.TP +.B @prefname@ +Expands to the name of the current preference, in the template +argument of \fB@prefs@\fR. +.TP +.B @prefs{\fIFILE\fB}{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly, for each preference of track +\fIFILE\fR. +Use \fB@prefname@\fR and \fB@prefvalue@\fR to get the name and value. +.TP +.B @prefvalue@ +Expands to the value of the current preference, in the template +argument of \fB@prefs@\fR. +.TP +.B @queue{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly using the each track on the queue in turn as +the current track. The track at the head of the queue comes first. +.TP +.B @random-enabled@ +Expands to \fBtrue\fR if random play is currently enabled, otherwise to +\fBfalse\fR. +.TP +.B @recent{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR repeatedly using the each recently played track in turn +as the current track. The most recently played track comes first. +.TP +.B @resolve{\fITRACK\fB}@ +Resolve aliases for \fITRACK\fR and expands to the result. +.TP +.B @search{\fIPART\fB}\fR[\fB{\fICONTEXT\fB}\fR]\fB{\fITEMPLATE\fB}@ +Expands \fITEMPLATE\fR once for each group of search results that have +a common value of track part \fIPART\fR. +The groups are sorted by the value of the part. +.IP +.B @part@ +and +.B @file@ +within the template will apply to one of the tracks in the group. +.IP +If \fICONTEXT\fR is specified it should be either \fBsort\fR or \fBdisplay\fR, +and determines the context for \fIPART\fR. The default is \fBsort\fR. Usually +you want \fBdisplay\fR for everything except the title and \fBsort\fR for the +title. If you use \fBsort\fR for artist and album then you are likely to get +strange effects. +.TP +.B @server-version@ +Expands to the server's version string. +.TP +.B @shell{\fICOMMAND\fB}@ +Expands to the output of \fICOMMAND\fR executed via the shell. \fBsh\fR is +searched for using \fBPATH\fR. If the command fails then this is logged but +otherwise ignored. +.TP +.B @state@ +In \fB@queue@\fR and \fB@recent@\fR, expands to the state of the current +track. Otherwise the empty string. Known states are: +.RS +.TP 12 +.B failed +The player terminated with nonzero status, but not because the track was +scratched. +.TP +.B isscratch +A scratch, in the queue. +.TP +.B no_player +No player could be found. +.TP +.B ok +Played successfully. +.TP +.B random +A randomly chosen track, in the queue. +.TP +.B scratched +This track was scratched. +.TP +.B unplayed +An explicitly queued track, in the queue. +.RE +.IP +Some additional states only apply to playing tracks, so will never be seen in +the queue or recently-played list: +.RS +.TP 12 +.B paused +The track has been paused. +.TP +.B quitting +Interrupted because the server is shutting down. +.TP +.B started +This track is currently playing. +.RE +.TP +.B @stats@ +Expands to the server statistics. +.TP +.B @thisurl@ +Expands to the URL of the current page. Typically used in +.B back +arguments. If there is a +.B nonce +argument then it is changed to a fresh value. +.TP +.B @track@ +The current track. +.TP +.B @trackstate{\fIPATH\fB}@ +Expands to the current track state: \fBplaying\fR if the track is actually +playing now, \fBqueued\fR if it is queued or the empty string otherwise. +.TP +.B @transform{\fIPATH\fB}{\fITYPE\fB}{\fICONTEXT\fB}@ +Transform a path according to \fBtransform\fR (see above). +\fIPATH\fR should be a raw filename (of a track or directory). +\fITYPE\fR should be the transform type (e.g. \fItrack\fR or \fIdir\fR). +\fICONTEXT\fR should be the context, and can be omitted (the default +is \fBdisplay\fR). +.TP +.B @url@ +Expands to the canonical URL as defined in \fIpkgconfdir/config\fR. +.TP +.B @urlquote{\fISTRING\fB}@ +URL-quote \fISTRING\fR. +.TP +.B @version@ +Expands to \fBdisorder.cgi\fR's version string. +.TP +.B @volume:\fISPEAKER\fB@ +The volume on the left or right speaker. \fISPEAKER\fR must be \fBleft\fB or +\fBright\fR. +.TP +.B @when@ +When the current track was played (or when it is expected to be played, if it +has not been played yet) +.TP +.B @who@ +Who submitted the current track. +.SH "WEB OPTIONS" +This is a file called \fIoptions\fR, searched for in the same manner +as templates. It includes numerous options for the control of the web +interface. The general syntax is the same as the main configuration +file, except that it should be encoded using UTF-8 (though this might +change to the current locale's character encoding; stick to ASCII to +be safe). +.PP +The shipped \fIoptions\fR file includes four standard options files. +In order, they are: +.TP +.I options.labels +The default labels file. You wouldn't normally edit this directly - instead +supply your own commands in \fIoptions.user\fR. Have a look at the shipped +version of the file for documentation of labels used by the standard templates. +.TP +.I options.user +A user options file. Here you should put any overrides for the default +labels and any extra labels required by your modified templates. +.PP +Valid directives are: +.TP +.B columns \fINAME\fR \fIHEADING\fR... +Defines the columns used in \fB@playing@\fR and \fB@recent@\fB. \fINAME\fR +must be either \fBplaying\fR, \fBrecent\fR or \fBsearch\fR. +\fIHEADING\fR... is a list of +heading names. If a column is defined more than once then the last definitions +is used. +.IP +The heading names \fBbutton\fR, \fBlength\fR, \fBwhen\fR and \fBwho\fR +are built in. +.TP +.B include \fIPATH\fR +Includes another file. If \fIPATH\fR starts with a \fB/\fR then it is +taken as is, otherwise it is searched for in the template path. +.TP +.B label \fINAME\fR \fIVALUE\fR +Define a label. If a label is defined more than once then the last definition +is used. +.SS Labels +Some labels are defined inside \fBdisorder.cgi\fR and others by the +default templates. You can define your own labels and use them inside +a template. +.PP +When an undefined label is expanded, if it has a dot in its name then +the part after the final dot is used as its value. Otherwise the +whole name is used as the value. +.PP +Labels are no longer documented here, see the shipped \fIoptions.labels\fR file +instead. +.SH "REGEXP SUBSTITUTION RULES" +Regexps are PCRE regexps, as defined in \fBpcrepattern\fR(3). The +only option used is \fBPCRE_UTF8\fR. Remember that the configuration +file syntax means you have to escape backslashes and quotes inside +quoted strings. +.PP +In a \fISUBST\fR string the following sequences are interpreted +specially: +.TP +.B $1 \fR... \fB$9 +These expand to the first to ninth bracketed subexpression. +.TP +.B $& +This expands to the matched part of the subject string. +.TP +.B $$ +This expands to a single \fB$\fR symbol. +.PP +All other pairs starting with \fB$\fR are undefined (and might be used +for something else in the future, so don't rely on the current +behaviour.) +.PP +If \fBi\fR is present in \fIREFLAGS\fR then the match is case-independent. If +\fBg\fR is present then all matches are replaced, otherwise only the first +match is replaced. +.SH "ACTIONS" +What the web interface actually does is terminated by the \fBaction\fR CGI +argument. The values listed below are supported. +.PP +Except as specified, all actions redirect back to the \fBplaying.html\fR +template unless the \fBback\fR argument is present, in which case the URL it +gives is used instead. +.PP +Redirection to \fBplaying.html\fR preserves \fBmgmt=true\fR if it is present. +.TP 8 +.B "move" +Move track \fBid\fR by offset \fBdelta\fR. +.TP +.B "play" +Play track \fBfile\fR, or if that is missing then play all the tracks in +\fBdirectory\fR. +.TP +.B "playing" +Don't change any state, but instead compute a suitable refresh time and include +that in an HTTP header. Expands the \fBplaying.html\fR template rather than +redirecting. +.IP +This is the default if \fBaction\fR is missing. +.TP +.B "random-disable" +Disables random play. +.TP +.B "random-enable" +Enables random play. +.TP +.B "disable" +Disables play completely. +.TP +.B "enable" +Enables play. +.TP +.B "pause" +Pauses the current track. +.TP +.B "remove" +Remove track \fBid\fR. +.TP +.B "resume" +Resumes play after a pause. +.TP +.B "scratch" +Scratch the playing track. If \fBid\fR is present it must match the playing +track. +.TP +.B "volume" +Change the volume by \fBdelta\fR, or if that is missing then set it to the +values of \fBleft\fR and \fBright\fR. Expands to the \fBvolume.html\fR template +rather than redirecting. +.TP +.B "prefs" +Adjust preferences from the \fBprefs.html\fR template (which it then expands +rather than redirecting). +.IP +If +.B parts +is set then the cooked interface is assumed. The value of +.B parts +is used to determine which trackname preferences are set. By default the +.B display +context is adjusted but this can be overridden with the +.B context +argument. Also the +.B random +argument is checked; if it is set then random play is enabled for that track, +otherwise it is disabled. +.IP +Otherwise if the +.B name +and +.B value +arguments are set then they are used to set a single preference. +.IP +Otherwise if just the +.B name +argument is set then that preference is deleted. +.IP +It is recommended that links to the \fBprefs\fR action use \fB@resolve@\fR to +enure that the real track name is always used. Otherwise if the preferences +page is used to adjust a trackname_ preference, the alias may change, leading +to the URL going stale. +.TP +.B "error" +This action is generated automatically when an error occurs connecting to the +server. The \fBerror\fR label is set to an indication of what the error is. +.SH "TRACK NAME PARTS" +The traditional track name parts are \fBartist\fR, \fBalbum\fR and \fBtitle\fR, +with the obvious intended meaning. These are controlled by configuration and +by \fBtrackname_\fR preferences. +.PP +In addition there are two built-in parts, \fBpath\fR which is the whole path +name and \fBext\fR which is the filename extension, including the initial dot +(or the empty string if there is not extension). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorderd\fR(8), \fBdisorder-dump\fR(8), +\fBpcrepattern\fR(3) +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:43b51c6f7ce647119d5409797c55908e diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in new file mode 100644 index 0000000..0344ac8 --- /dev/null +++ b/doc/disorder_protocol.5.in @@ -0,0 +1,444 @@ +.\" +.\" Copyright (C) 2004, 2005, 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorder_protocol 5 +.SH NAME +disorder_protocol \- DisOrder communication protocol +.SH DESCRIPTION +The DisOrder client and server communicate via the protocol described +in this man page. +.PP +The protocol is liable to change without notice. You are recommended to check +the implementation before believing this document. +.SH "GENERAL SYNTAX" +Everything is encoded using UTF-8. +.PP +Commands and responses consist of a line followed (depending on the +command or response) by a message. +.PP +The line syntax is the same as described in \fBdisorder_config\fR(5) except +that comments are prohibited. +.PP +Bodies borrow their syntax from RFC821; they consist of zero or more ordinary +lines, with any initial full stop doubled up, and are terminated by a line +consisting of a full stop and a line feed. +.SH COMMANDS +Commands always have a command name as the first field of the line; responses +always have a 3-digit response code as the first field. See below for more +details about this field. +.PP +All commands require the connection to have been already authenticated unless +stated otherwise. +.PP +Neither commands nor responses have a body unless stated otherwise. +.TP +.B allfiles \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the files and directories in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching files and directories are returned. +.TP +.B become \fIUSER\fR +Instructs the server to treat the connection as if \fIUSER\fR had +authenticated it. Only trusted users may issue this command. +.TP +.B dirs \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the directories in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching directories are returned. +.TP +.B disable \fR[\fBnow\fR] +Disables further playing. If the optional \fBnow\fR argument is present then +the current track is stopped. +.TP +.B enable +Re-enables further playing, and is the opposite of \fBdisable\fR. +.TP +.B enabled +Reports whether playing is enabled. The second field of the response line will +be \fByes\fR or \fBno\fR. +.TP +.B exists \fITRACK\fR +Reports whether the named track exists. The second field of the response line +will be \fByes\fR or \fBno\fR. +.TP +.B files \fIDIRECTORY\fR [\fIREGEXP\fR] +Lists all the files in \fIDIRECTORY\fR in a response body. +If \fIREGEXP\fR is present only matching files are returned. +.TP +.B get \fITRACK\fR \fIPREF\fR +Gets a preference value. On success the second field of the response line will +have the value. +.TP +.B get-global \fIKEY\fR +Get a global preference. +.TP +.B length \fITRACK\fR +Gets the length of the track in seconds. On success the second field of the +response line will have the value. +.TP +.B log +Sends event log messages in a response body. The command will only terminate (and +close the response body with a final dot) when a further command is readable on +the control connection. +.IP +See \fBEVENT LOG\fR below for more details. +.TP +.B move \fITRACK\fR \fIDELTA\fR +Move a track in the queue. The track may be identified by ID (preferred) or +name (which might cause confusion if it's there twice). \fIDELTA\fR should be +an negative or positive integer and indicates how many steps towards the head +of the queue the track should be moved. +.TP +.B moveafter \fITARGET\fR \fIID\fR ... +Move all the tracks in the \fIID\fR list after ID \fITARGET\fR. If +\fITARGET\fR is the empty string then the listed tracks are put at the head of +the queue. If \fITARGET\fR is listed in the ID list then the tracks are moved +to just after the first non-listed track before it, or to the head if there is +no such track. +.TP +.B part \fITRACK\fR \fICONTEXT\fI \fIPART\fR +Get a track name part. Returns an empty string if a name part cannot be +constructed. +.IP +.I CONTEXT +is one of +.B sort +or +.B display +and +.I PART +is usually one of +.BR artist , +.B album +or +.BR title . +.TP +.B pause +Pause the current track. +.TP +.B play \fITRACK\fR +Add a track to the queue. +.TP +.B playing +Reports what track is playing. +.IP +If the response is \fB252\fR then the rest of the response line consists of +track information (see below). +.IP +If the response is \fB259\fR then nothing is playing. +.TP +.B prefs \fBTRACK\fR +Sends back the preferences for \fITRACK\fR in a response body. +Each line of the response has the usual line syntax, the first field being the +name of the pref and the second the value. +.TP +.B queue +Sends back the current queue in a response body, one track to a line, the track +at the head of the queue (i.e. next to be be played) first. See below for the +track information syntax. +.TP +.B random-disable +Disable random play (but don't stop the current track). +.TP +.B random-enable +Enable random play. +.TP +.B random-enabled +Reports whether random play is enabled. The second field of the response line +will be \fByes\fR or \fBno\fR. +.TP +.B recent +Sends back the current recently-played list in a response body, one track to a +line, the track most recently played last. See below for the track +information syntax. +.TP +.B reconfigure +Request that DisOrder reconfigure itself. Only trusted users may issue this +command. +.TP +.B remove \fIID\fR +Remove the track identified by \fIID\fR. If \fBrestrict remove\fR is enabled +in the server's configuration then only the user that submitted the track may +remove it. +.TP +.B rescan +Rescan all roots for new or obsolete tracks. +.TP +.B resolve \fITRACK\fR +Resolve a track name, i.e. if this is an alias then return the real track name. +.TP +.B resume +Resume the current track after a \fBpause\fR command. +.TP +.B scratch \fR[\fIID\fR] +Remove the track identified by \fIID\fR, or the currently playing track if no +\fIID\fR is specified. If \fBrestrict scratch\fR is enabled in the server's +configuration then only the user that submitted the track may scratch it. +.TP +.B search \fITERMS\fR +Search for tracks matching the search terms. The results are put in a response +body, one to a line. +.IP +The search string is split in the usual way, with quoting supported, into a +list of terms. Only tracks matching all terms are included in the results. +.IP +Any terms of the form \fBtag:\fITAG\fR limits the search to tracks with that +tag. +.IP +All other terms are interpreted as individual words which must be present in +the track name. +.IP +Spaces in terms don't currently make sense, but may one day be interpreted to +allow searching for phrases. +.TP +.B \fBset\fR \fITRACK\fR \fIPREF\fR \fIVALUE\fR +Set a preference. +.TP +.B set-global \fIKEY\fR \fIVALUE\fR +Set a global preference. +.TP +.B stats +Send server statistics in plain text in a response body. +.TP +.B \fBtags\fR +Send the list of currently known tags in a response body. +.TP +.B \fBunset\fR \fITRACK\fR \fIPREF\fR +Unset a preference. +.TP +.B \fBunset-global\fR \fIKEY\fR +Unset a global preference. +.TP +.B user \fIUSER\fR \fIRESPONSE\fR +Authenticate as \fIUSER\fR. +.IP +When a connection is made the server sends a \fB221\fR response before any +command is received. As its first field this contains a challenge string +encoded in hex. +.IP +The \fIRESPONSE\fR consists of the SHA-1 hash of the user's password +concatenated with the challenge, encoded in hex. +.TP +.B version +Send back a response with the server version as the second field. +.TP +.B volume \fR[\fILEFT\fR [\fIRIGHT\fR]] +Get or set the volume. +.IP +With zero parameters just gets the volume and reports the left and right sides +as the 2nd and 3rd fields of the response. +.IP +With one parameter sets both sides to the same value. With two parameters sets +each side independently. +.SH RESPONSES +Responses are three-digit codes. The first digit distinguishes errors from +succesful responses: +.TP +.B 2 +Operation succeeded. +.TP +.B 5 +Operation failed. +.PP +The second digit breaks down the origin of the response: +.TP +.B 0 +Generic responses not specific to the handling of the command. Mostly this is +parse errors. +.TP +.B 3 +Authentication responses. +.TP +.B 5 +Responses specific to the handling of the command. +.PP +The third digit provides extra information about the response: +.TP +.B 0 +Text part is just commentary. +.TP +.B 1 +Text part is a constant result e.g. \fBversion\fR. +.TP +.B 2 +Text part is a potentially variable result. +.TP +.B 3 +Text part is just commentary; a dot-stuffed body follows. +.TP +.B 4 +Text part is just commentary; an indefinite dot-stuffed body follows. (Used +for \fBlog\fR.) +.TP +.B 4 +Text part is just commentary; an indefinite dot-stuffed body follows. (Used +for \fBlog\fR.) +.TP +.B 9 +The text part is just commentary (but would normally be a response for this +command) e.g. \fBplaying\fR. +.SH AUTHENTICATION +The server starts by issuing a challenge line, with response code 231. This +contains a random challenge encoded in hex. +.PP +The client should send back a \fBuser\fR command with username and a +hex-encoded response. The response is the SHA-1 hash of the user's password +and the challenge. +.SH "TRACK INFORMATION" +Track information is encoded in a line (i.e. using the usual line syntax) as +pairs of fields. The first is a name, the second a value. The names have the +following meanings: +.TP 12 +.B expected +The time the track is expected to be played at. +.TP +.B id +A string uniquely identifying this queue entry. +.TP +.B played +The time the track was played at. +.TP +.B scratched +The user that scratched the track. +.TP +.B state +The current track state. Valid states are: +.RS +.TP 12 +.B failed +The player failed (exited with nonzero status but wasn't scratched). +.TP +.B isscratch +The track is actually a scratch. +.TP +.B no_player +No player could be found for the track. +.TP +.B ok +The track was played without any problems. +.TP +.B scratched +The track was scratched. +.TP +.B started +The track is currently playing. +.TP +.B unplayed +In the queue, hasn't been played yet. +.TP +.B quitting +The track was terminated because the server is shutting down. +.RE +.TP +.B submitter +The user that submitted the track. +.TP +.B track +The filename of the track. +.TP +.B when +The time the track was added to the queue. +.TP +.B wstat +The wait status of the player in decimal. +.SH NOTES +Times are decimal integers using the server's \fBtime_t\fR. +.PP +For file listings, the regexp applies to the basename of the returned file, not +the whole filename, and letter case is ignored. \fBpcrepattern\fR(3) describes +the regexp syntax. +.PP +Filenames are in UTF-8 even if the collection they come from uses some other +encoding - if you want to access the real file (in such cases as the filenames +actually correspond to a real file) you'll have to convert to whatever the +right encoding is. +.SH "EVENT LOG" +The event log consists of lines starting with a hexadecimal timestamp and a +keyword followed by (optionally) parameters. The parameters are quoted in the +usual DisOrder way. Currently the following keywords are used: +.TP +.B completed \fITRACK\fR +Completed playing \fITRACK\fR +.TP +.B failed \fITRACK\fR \fIERROR\fR +Completed playing \fITRACK\fR with an error status +.TP +.B moved \fIUSER\fR +User \fIUSER\fR moved some track(s). Further details aren't included any +more. +.TP +.B playing \fITRACK\fR [\fIUSER\fR] +Started playing \fITRACK\fR. +.TP +.B queue \fIQUEUE-ENTRY\fR... +Added \fITRACK\fR to the queue. +.TP +.B recent_added \fIQUEUE-ENTRY\fR... +Added \fIID\fR to the recently played list. +.TP +.B recent_removed \fIID\fR +Removed \fIID\fR from the recently played list. +.TP +.B removed \fIID\fR [\fIUSER\fR] +Queue entry \fIID\fR was removed. This is used both for explicit removal (when +\fIUSER\fR is present) and when playing a track (when it is absent). +.TP +.B scratched \fITRACK\fR \fIUSER\fR +\fITRACK\fR was scratched by \fIUSER\fR. +.TP +.B state \fIKEYWORD\fR +Some state change occurred. The current set of keywords is: +.RS +.TP +.B disable_play +Playing was disabled. +.TP +.B disable_random +Random play was disabled. +.TP +.B enable_play +Playing was enabled. +.TP +.B enable_random +Random play was enabled. +.TP +.B pause +The current track was paused. +.TP +.B resume +The current track was resumed. +.RE +.TP +.B volume \fILEFT\fR \fIRIGHT\fR +The volume changed. +.PP +.IR QUEUE-ENTRY ... +is as defined in +.B "TRACK INFORMATION" +above. +.SH "SEE ALSO" +\fBdisorder\fR(1), +\fBtime\fR(2), +\fBdisorder\fR(3), +\fBpcrepattern\fR(3) +\fBdisorder_config\fR(5), +\fBdisorderd\fR(8), +\fButf8\fR(7) +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:7b6e9931e426d2b810422b20aef38601 diff --git a/doc/disorderd.8.in b/doc/disorderd.8.in new file mode 100644 index 0000000..4e68055 --- /dev/null +++ b/doc/disorderd.8.in @@ -0,0 +1,164 @@ +.\" +.\" Copyright (C) 2004, 2005 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorderd 8 +.SH NAME +disorderd \- DisOrder jukebox daemon +.SH SYNOPSIS +.B disorderd +.RI [ OPTIONS ] +.SH DESCRIPTION +.B disorderd +is a daemon which plays audio files and services requests from users +concerning what is to be played. +.SH OPTIONS +.TP +.B --config \fIPATH\fR, \fB-c \fIPATH +Set the configuration file. The default is +.IR pkgconfdir/config . +See +.BR disorder_config (5) +for further information. +.TP +.B --pidfile \fIPATH\fR, \fB-P \fIPATH +Write a pidfile. +.TP +.B --foreground, \fB-f +Run in the foreground. (By default, +.B disorderd +detaches from its terminal and runs in the background.) +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH NOTES +.SS "Environmental Dependencies" +It is important that +.B disorder-deadlock +and +.B disorder-rescan +are available on the PATH. The example "init" script attempts to +ensure this by appending sbindir to the path, but if you have +installed programs in unusual locations then this might not work. +.SS "How To Configure Authentication" +The administrator should create \fIpkgconfdir/config.private\fR, make sure it +is not world-readable, and populate it with \fBallow\fR commands +listing usernames and passwords. Use +e.g. \fBpwgen\fR(1) to generate random passwords. Passwords should +then be distributed to users. +.PP +Each user should create the file \fI~/.disorder/passwd\fR +and make sure it is not world-readable. Having done so +they should add a \fBpassword\fR command to it giving their password (and +optionally a \fBusername\fR command if their DisOrder username is not the +same as their login name). +.SS Locales +.B disorderd +is locale-aware. If you do not set the locale correctly then it may +not handle non-ASCII data properly. +.PP +Filenames and the configuration file are assumed to be encoded using the +current locale. The +communication with the client happens in UTF-8 (since the client and the server +don't know what locale each other might be using - in the future they might not +even be on the same host.) +.SS Backups +DisOrder uses Berkeley DB but currently discards log files that are no longer +in use. This means that DB's catastrophic recovery cannot be used (normal +recovery can be used, and indeed the server does this automatically on +startup). +.PP +It is suggested that instead you just back up the output of +.BR disorder-dump (8), +which saves only the parts of the database that cannot be regenerated +automatically, and thus has relatively modest storage requirements. +.SH SIGNALS +.TP 8 +.B SIGHUP +Re-read the configuration file. +.TP +.B SIGTERM +Terminate the daemon gracefully. +.TP +.B SIGINT +Terminate the daemon gracefully. +.PP +It may be more convenient to perform these operations from the client +\fBdisorder\fR(1). +.SH FILES +.TP +.I pkgconfdir/config +Global configuration file. See \fBdisorder_config\fR(5). +.TP +.I pkgconfdir/config.private +Private configuration (usernames and passwords). +.TP +.I ~/.disorder/passwd +Per-user password file. +.TP +.I pkgstatedir/queue +Saved copy of queue. Do not edit while the daemon is running. +.TP +.I pkgstatedir/recent +Saved copy of recently played track list. +Do not edit while the daemon is running. +.TP +.I pkgstatedir/prefs.db +Preferences database. +.TP +.I pkgstatedir/search.db +Search database. +.TP +.I pkgstatedir/tracks.db +Tracks database. +.TP +.I pkgstatedir/DB_CONFIG +Berkeley DB configuration file. This may be used to override database +settings without recompiling DisOrder. See the Berkeley DB +documention for further details. +.TP +.I pkgstatedir/log.* +Database log files. +.TP +.I pkgstatedir/socket +Communication socket for \fBdisorder\fR(1). +.TP +.I pkgstatedir/lock +Lockfile. This prevents multiple instances of DisOrder running +simultaneously. +.TP +.I sbindir/disorder-deadlock +Deadlock manager. +.TP +.I sbindir/disorder-rescan +Rescanner. +.SH ENVIRONMENT +.TP +.B LC_ALL\fR, \fBLANG\fR, etc +Current locale. See \fBlocale\fR(7). +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorder-dump\fR(8) +.\" Local Variables: +.\" mode:nroff +.\" End: +.\" arch-tag:90dfddb7692b5c7f621c81bd7852ebde diff --git a/doc/disorderfm.1.in b/doc/disorderfm.1.in new file mode 100644 index 0000000..81b48f3 --- /dev/null +++ b/doc/disorderfm.1.in @@ -0,0 +1,100 @@ +.\" +.\" Copyright (C) 2006 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH disorderfm 1 +.SH NAME +disorderfm \- DisOrder file management utility +.SH SYNOPSIS +.B disorderfm +.RI [ OPTIONS ] +.I SOURCE +.I DESTINATION +.SH DESCRIPTION +.B disorderfm +recursively links or copies files from +.I SOURCE +to +.IR DESTINATION , +transforming filenames along the way. +.SH OPTIONS +.SS "Filename Format" +.TP +.B --from\fI ENCODING\fR, \fB-f\fI ENCODING +Specifies the filename encoding used below +.IR SOURCE . +.TP +.B --to\fI ENCODING\fR, \fB-t\fI ENCODING +Specifies the filename encoding used below +.IR DESTINATION . +.PP +If neither of \fB--from\fR or \fB--to\fR are specified then no encoding +translation is performed. If only one is specified then the other is set to +the current locale. +.TP +.B --windows-friendly\fR, \fB-w +Specifies that filenames below +.I DESTINATION +must be Windows-friendly. This is achieved by replacing special characters +with '_', prefixing device names with '_' and stripping trailing dots and +spaces. +.SS "File Selection" +.TP +.B --include\fI PATTERN\fR, \fB-i\fI PATTERN +Include filenames matching the glob pattern \fIPATTERN\fR. +.TP +.B --exclude\fI PATTERN\fR, \fB-e\fI PATTERN +Exclude filenames matching the glob pattern \fIPATTERN\fR. +.PP +These options may be used more than once. They will be checked in order and +the first that matches any given filename will determine whether that file is +included or excluded. +.PP +If none of the options match and \fB--include\fR was used at all then the file +is excluded. If none of the options match and \fB--include\fR was never used +then the file is included. +.SS "File Copying" +.TP +.B --link\fR, \fB-l +Files are hard-linked to their destination location. This is the default +action. +.TP +.B --symlink\fR, \fB-s +Symlinks are made in the destination location pointing back into the source +directory. +.TP +.B --copy\fR, \fB-c +Files are copied into their destination location. +.TP +.B --no-action\fR, \fB-n +The destination location is not modified in any way. Instead a report is +written to standard output saying what would be done. +.SS "Other" +.TP +.B --debug\fR, \fB-d +Enable debugging. +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:HqblI8GXJamh2oeBBgb1Ug diff --git a/doc/tkdisorder.1 b/doc/tkdisorder.1 new file mode 100644 index 0000000..d5fb365 --- /dev/null +++ b/doc/tkdisorder.1 @@ -0,0 +1,60 @@ +.\" +.\" Copyright (C) 2005 Richard Kettlewell +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, but +.\" WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +.\" General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +.\" USA +.\" +.TH tkdisorder 1 +.SH NAME +tkdisorder \- DisOrder jukebox client +.SH SYNOPSIS +.B tkdisorder +.RI [ OPTIONS ] +.SH DESCRIPTION +.B tkdisorder +is a simple graphical client for DisOrder. It is not really finished. +.PP +The main window is divided into two. The top half contains the name +of the current track and a progress bar indicating how far through +playing it is. It also contains three buttons: +.TP +.B Quit +Terminates tkdisorder. +.TP +.B Scratch +Terminates the current track. +.TP +.B Recent +Pops up a window listing recently played tracks, most recent at the +top. +.PP +The bottom half of the window lists the current queue, with the next +track to be played at the top. +.SH OPTIONS +.TP +.B --help\fR, \fB-h +Display a usage message. +.TP +.B --version\fR, \fB-V +Display version number. +.SH "SEE ALSO" +\fBdisorder\fR(1), \fBdisorder_config\fR(5) +.PP +"\fBpydoc disorder\fR" for the Python API documentation. +.\" Local Variables: +.\" mode:nroff +.\" fill-column:79 +.\" End: +.\" arch-tag:yguAsuJM8a5kzQ+UqEnSKg diff --git a/driver/Makefile.am b/driver/Makefile.am new file mode 100644 index 0000000..a43470d --- /dev/null +++ b/driver/Makefile.am @@ -0,0 +1,29 @@ +# +# This file is part of DisOrder +# Copyright (C) 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + + +aolib_LTLIBRARIES=libdisorder.la +aolibdir=${libdir}/ao/plugins-2 +AM_CPPFLAGS=-I${top_srcdir}/lib + +libdisorder_la_SOURCES=disorder.c +libdisorder_la_LDFLAGS=-module + +# arch-tag:tYZU3CmDyRkdyrcHgJMxcA diff --git a/driver/disorder.c b/driver/disorder.c new file mode 100644 index 0000000..8385ae8 --- /dev/null +++ b/driver/disorder.c @@ -0,0 +1,170 @@ + +/* + * This file is part of DisOrder. + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <poll.h> +#include <ao/ao.h> +#include <ao/plugin.h> + +/* extra declarations to help out lazy <ao/plugin.h> */ +int ao_plugin_test(void); +ao_info *ao_plugin_driver_info(void); +char *ao_plugin_file_extension(void); + +/* private data structure for this driver */ +struct internal { + int fd; /* output file descriptor */ + int exit_on_error; /* exit on write error */ +}; + +/* like write() but never returns EINTR/EAGAIN or short */ +static int do_write(int fd, const void *ptr, size_t n) { + size_t written = 0; + int ret; + struct pollfd ufd; + + memset(&ufd, 0, sizeof ufd); + ufd.fd = fd; + ufd.events = POLLOUT; + while(written < n) { + ret = write(fd, (const char *)ptr + written, n - written); + if(ret < 0) { + switch(errno) { + case EINTR: break; + case EAGAIN: + /* Someone sneakily gave us a nonblocking file descriptor, wait until + * we can write again */ + ret = poll(&ufd, 1, -1); + if(ret < 0 && errno != EINTR) return -1; + break; + default: + return -1; + } + } else + written += ret; + } + return written; +} + +/* return 1 if this driver can be opened */ +int ao_plugin_test(void) { + return 1; +} + +/* return info about this driver */ +ao_info *ao_plugin_driver_info(void) { + static const char *options[] = { "fd" }; + static const ao_info info = { + AO_TYPE_LIVE, /* type */ + (char *)"DisOrder format driver", /* name */ + (char *)"disorder", /* short_name */ + (char *)"http://www.greenend.org.uk/rjk/disorder/", /* comment */ + (char *)"Richard Kettlewell", /* author */ + AO_FMT_NATIVE, /* preferred_byte_format */ + 0, /* priority */ + (char **)options, /* options */ + 1, /* option_count */ + }; + return (ao_info *)&info; +} + +/* initialize the private data structure */ +int ao_plugin_device_init(ao_device *device) { + struct internal *i = malloc(sizeof (struct internal)); + const char *e; + + if(!i) return 0; + memset(i, 0, sizeof *i); + if((e = getenv("DISORDER_RAW_FD"))) + i->fd = atoi(e); + else + i->fd = 1; + device->internal = i; + return 1; +} + +/* set an option */ +int ao_plugin_set_option(ao_device *device, + const char *key, + const char *value) { + struct internal *i = device->internal; + + if(!strcmp(key, "fd")) + i->fd = atoi(value); + else if(!strcmp(key, "fragile")) + i->exit_on_error = atoi(value); + /* unknown options are required to be ignored */ + return 1; +} + +/* open the device */ +int ao_plugin_open(ao_device *device, ao_sample_format *format) { + struct internal *i = device->internal; + + /* we would like native-order samples */ + device->driver_byte_format = AO_FMT_NATIVE; + if(do_write(i->fd, format, sizeof *format) < 0) { + if(i->exit_on_error) exit(-1); + return 0; + } + return 1; +} + +/* play some samples */ +int ao_plugin_play(ao_device *device, const char *output_samples, + uint_32 num_bytes) { + struct internal *i = device->internal; + + if(do_write(i->fd, output_samples, num_bytes) < 0) { + if(i->exit_on_error) _exit(-1); + return 0; + } + return 1; +} + +/* close the device */ +int ao_plugin_close(ao_device attribute((unused)) *device) { + return 1; +} + +/* delete private data structures */ +void ao_plugin_device_clear(ao_device *device) { + free(device->internal); + device->internal = 0; +} + +/* report preferred filename extension */ +char *ao_plugin_file_extension(void) { + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:ru/Jqo0hseWMoSt/ba4Xlw */ diff --git a/examples/Makefile.am b/examples/Makefile.am new file mode 100644 index 0000000..74984f4 --- /dev/null +++ b/examples/Makefile.am @@ -0,0 +1,31 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +noinst_SCRIPTS=disorder.init +noinst_DATA=config.sample + +SEDFILES=disorder.init config.sample + +include ${top_srcdir}/scripts/sedfiles.make + +EXTRA_DIST=disorder.init.in config.sample.in disorder-log + +CLEANFILES=$(SEDFILES) +# arch-tag:c335ba7bb6f5a6077a563a1b79951e31 diff --git a/examples/config.sample.in b/examples/config.sample.in new file mode 100644 index 0000000..eb10fda --- /dev/null +++ b/examples/config.sample.in @@ -0,0 +1,59 @@ +# player programs +player *.mp3 execraw mpg321 -q -o disorder +player *.ogg execraw ogg123 -q -d disorder -o fragile:1 +player *.wav shell --wait-for-device play + +# use the fs module to list files under /export/mp3. The encoding +# is ISO-8859-1. +collection fs ISO-8859-1 /export/mp3 + +# don't leave a gap between tracks +gap 0 + +# scratch tracks +scratch pkgdatadir/slap.ogg +scratch pkgdatadir/scratch.mp3 + +# trust the web user and root +trust www-data root + +# run as user jukebox +user jukebox + +# volume control +mixer /dev/mixer +channel pcm + +# URL of the web interface +url http://jukebox.anjou.terraraq.org.uk/ + +# stopwords (i.e. ignored words) for the track search facility +stopword 01 02 03 04 05 06 07 08 09 10 +stopword 1 2 3 4 5 6 7 8 9 +stopword 11 12 13 14 15 16 17 18 19 20 +stopword 21 22 23 24 25 26 27 28 29 30 +stopword the a an and to too in on of we i am as im for is + +# namepart and transform are now filled in by default if you do not supply +# them. However if you supply any namepart directives then you will not +# get any defaults at all, so you must supply the full set. Similarly, +# if you supply any transform directives then you must supply the full set. + +# Parsing of track names for the currently playing track, the recently +# played list and the queue. +#namepart title "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#namepart title "/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort +#namepart album "/([^/]+)/[^/]+$" "$1" * +#namepart artist "/([^/]+)/[^/]+/[^/]+$" "$1" * +# used in alias construction +#namepart ext "(\\.[a-zA-Z0-9]+)$" "$1" * + +# Transformations of directory and filenames for the track choice screen +#transform track "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$" "$2" display +#transform track "^.*/([^/]+)\\.[a-zA-Z0-9]+$" "$1" sort + +#transform dir "^.*/([^/]+)$" "$1" * +#transform dir "^(the) ([^/]*)" "$2, $1" sort i +#transform dir "[[:punct:]]" "" sort g + +# arch-tag:57c841bad362239972e38de20d15e6c0 diff --git a/examples/disorder-log b/examples/disorder-log new file mode 100755 index 0000000..5a966a8 --- /dev/null +++ b/examples/disorder-log @@ -0,0 +1,70 @@ +#! /usr/bin/env python +# +# This file is part of DisOrder +# Copyright (C) 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +# Example use of disorder.monitor class + +import disorder + +class mymonitor(disorder.monitor): + def completed(self, track): + print "completed %s" % track + return True + + def failed(self, track, error): + print "failed %s (%s)" % (track, error) + return True + + def moved(self, id, offset, user): + print "%s moved by %s (%s)" % (id, offset, user) + return True + + def playing(self, track, user): + print "%s playing" % track + return True + + def queue(self, q): + print "queued %s" % str(q) + return True + + def recent_added(self, q): + print "recent_added %s" % str(q) + return True + + def recent_removed(self, id): + print "recent_removed %s" % id + return True + + def removed(self, id, user): + print "removed %s" % id + return True + + def scratched(self, track, user): + print "%s scratched %s" % (track, user) + return True + + def invalid(self, line): + print "invalid line: %s" % line + return True + +m = mymonitor() +m.run() + +# arch-tag:198M6M5uzq+f8AP1q+P1NA diff --git a/examples/disorder.init.in b/examples/disorder.init.in new file mode 100644 index 0000000..5348597 --- /dev/null +++ b/examples/disorder.init.in @@ -0,0 +1,68 @@ +#! /bin/sh +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +DAEMON=sbindir/disorderd +CLIENT=bindir/disorder + +PATH="$PATH:sbindir" + +start() { + if ${CLIENT} >/dev/null 2>&1; then + : already running + else + printf "Starting disorderd... " + ${DAEMON} + echo done + fi +} + +stop() { + if ${CLIENT} >/dev/null 2>&1; then + printf "Stopping disorderd... " + ${CLIENT} shutdown + echo done + else + : not running + fi +} + +reload() { + printf "Reconfiguring disorderd... " + ${CLIENT} reconfigure + echo done +} + +restart() { + stop + sleep 2 + start +} + +case "$1" in +start | stop | reload | restart ) "$1" ;; +force-reload ) reload ;; +* ) + echo "usage: $0 start|stop|restart|reload" 1>&2 + exit 1 +esac +# arch-tag:b75a857d0e9f723d2ae30687758ee1eb diff --git a/images/Makefile.am b/images/Makefile.am new file mode 100644 index 0000000..1af87d2 --- /dev/null +++ b/images/Makefile.am @@ -0,0 +1,30 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +static_DATA=cross.png down.png downdown.png edit.png nocross.png \ +nodown.png nodowndown.png noup.png noupup.png tick.png up.png upup.png \ +notes.png play.png pause.png random.png randomcross.png notescross.png + +staticdir=${pkgdatadir}/static + +EXTRA_DIST=$(static_DATA) + +CLEANFILES=$(SEDFILES) +# arch-tag:izAg/jA9nncHHWR5+ZuD+Q diff --git a/images/cross.png b/images/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7a461f70ee49bf49f3276d2ea4ad152cc038c9 GIT binary patch literal 226 zcmV<803H8{P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0001~Nkl<ZILoDy z$qE875JVq-ss|5(%Z$$MGFQRB^(aiuWg;P@6U9JZQuV6oPGC8LQP7Y%f|FnfFGqov zYBI3%U1G4SIs>~JRz|ajpz`1nc&6sMN75yz1f`+%VaUxe=HVYhy4*8e6L1^)V@rG` z^541chc8Ab|AFHng1up^1-+paIoSV5R>%>-S}<9#7t#fuNw9I^))UT&CAXe$7m7s+ c4njV@8BjzBC-NeySO5S307*qoM6N<$f=97h>;M1& literal 0 HcmV?d00001 diff --git a/images/down.png b/images/down.png new file mode 100644 index 0000000000000000000000000000000000000000..cb51f2d022869c6261545410f0a4d57c8bf3305c GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`HJ&bxAr`0KUb@J6D1fK+;d-Sl z*%qC~B?iY|G08sKSJEQ(##^L%W#9+jU(WN2{5%dNdUrSdo~^a8vgniY@rL~p1?!qF zF7o{1E32|2sb`9)j;GLc@w*an1qUZcyp}q&dEsH+;-opwms9Is8or&e{6(#6?WK38 g=2y&_vThHP!3@(6+|L;_f$m`NboFyt=akR{0BcN1qyPW_ literal 0 HcmV?d00001 diff --git a/images/downdown.png b/images/downdown.png new file mode 100644 index 0000000000000000000000000000000000000000..73ddade93e097380dbfc90888705d842c43a9b15 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`b)GJcAr`0aPCUrlpdi5FZhxt5 zYD9Bu<TTz07I`m*=J}U79!W0l?3{duz3k*WMor#?#tNe_6KPZapZ^+H6^OmBh+BDq zadOTLrp=4ym+$i2!?5MRikf)4#T7z41`KhARTm}JhVk!cx_R<$V-35br$F7VW}(WZ gNl&z|d1XCeU9TH?#k1fnFVH0np00i_>zopr0By-a-~a#s literal 0 HcmV?d00001 diff --git a/images/edit.png b/images/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..5a348f72f5fa52b77e7782764c3f1965a5a79cf0 GIT binary patch literal 559 zcmV+~0?_@5P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru)dCX~Bm%O62MYiI0lY~> zK~y-)osvIjQ&AMgfA^*}!T5Z&kvEmrid4a(4HUGrgA|2|p_6veq2Q23kW?3SckS$= zi{Ri^L<>qqp<`Q#gxEnHItaD|O#IU%@15h27k$xSf**X-cfKE&dkz9Lod-M^01Cia zx{<VcAP+?k6et7J9RWXnm&XM@31}X4r~&y7ockvr(|}9~R0Oo?kD$ZdKpew~3{-X_ z<tXG(C&&xK)?S0of!5kUHbIAatr{qwK6-HC&%O~TuYfaaAfxaQ=!pzeMpAsZnr3^~ z=Rk5#u_tCn)4647<`y_V4V5L}32+7Q!?5Zl*}8Nb>5Y+c;#g~riRxU-3&K@S0Z&(q zab{J?9_c50VT5mkZaC4h^>@~olj+>D{|gW%lfbj*gRUd<V|aauwtx2W?PtCFxNy1j zwf3k508n+~lbgATrTYGOf4lN(afciCH>d~lCT7ieAR5@{K>SN})prwzf!n}NG<#jx z=H`P9>VW`^1)>-KdO=j{i!(`8y$&p_y)UJ%-CLvPOLXKFaHiv_uP@Fxo}0J>lxQ4P x(_)Pord!3VNdoiXw#y;pmf`=j56A+iega%Awotv{$)W%N002ovPDHLkV1mmV^|Sy0 literal 0 HcmV?d00001 diff --git a/images/nocross.png b/images/nocross.png new file mode 100644 index 0000000000000000000000000000000000000000..18aa9a8c34ba3b34023f3f91d58eb5ff0ed884af GIT binary patch literal 341 zcmV-b0jmCqP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0003TNkl<ZILnQa z%T5A85Jk^0ALX`0q6_~;f(R%Wxh}wWqAP!=d_{MDh&wSxV-PnzrUN|#ch^<--0D<! zPLL#-*_yjQQ6WifGpoD%13+HDzz}Gd*;iSJq&9E~)Xi+_?)O;)$LdKsFM(qn)=F-A zCaDv_y9pS8EEtA>68$bP1+EgkfuwO3O*sONSxo~;<NqZBxci-%&4BeKa9`3%ameFN z2>GoDIgoT37g9@{R80SwFXVGi)da390yfQT;_lB20V}|Tw&IZDC++|@33Cr{U?UOo z?(Q#H1>BW%5eD>uQNq)z0UyAu3=RQfpg-rC0iU_Mzna+_upJs5NekcB-5<^D8)$@5 nQ_^kx0xd~<%Qh%!Klc3rgieI77(wiF00000NkvXXu0mjfM-Ymh literal 0 HcmV?d00001 diff --git a/images/nodown.png b/images/nodown.png new file mode 100644 index 0000000000000000000000000000000000000000..98f1367ac58d47f247498453af7d3ee284db2bb5 GIT binary patch literal 254 zcmV<a00IArP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0002RNkl<ZILqzQ zAx;Bf5C!0GfusaY6OdJ$pb0_k74RfDTu*?QjHoBUp-FAeu!b{$ssvGigalFZw^``6 zA*mxg_5YKXnfGR1rG<#-VTM!Pc*P(yKbM6)JL12=pJ-5(@GQ~83r;rh-*ARikPdK; zdmNWqf6Kr=p38fVQB@*5z#}exsr(67nK|2srfJ^mx}M+&!!5h{!X2(Na}L!{MnqiV zp=C)+@=az=zb&zAOoyeF<h3hoG8qwv7?<yDW`1<>77`CG5VCj2NB{r;07*qoM6N<$ Ef)mzi82|tP literal 0 HcmV?d00001 diff --git a/images/nodowndown.png b/images/nodowndown.png new file mode 100644 index 0000000000000000000000000000000000000000..19862972fbfe387cca6777e7eb24d7be56d85160 GIT binary patch literal 246 zcmV<S015wzP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0002JNkl<ZILqDA zy$J$A6a?VUAFu%t3~j>Hzyi$d!pP9zB2yE~Ft7#_3$TI96vP5NP%wE1tDKi;AP#J> zv-4)(yxm5YWfF9dqpun#3{aFr(}(yygQ6zCI4i#1!3YN&W8G`?_e6KVsEHjW$@8^p z3|gVdqh?n|$T7wWw;F?6XvR_VG=nSVnBr1Ve2KGpXhAz>utqny-kTU*Y;aBtXMZq* ws*kkA5c`DN2e*9{x8Jl{Mq0*xk=~l(0a98vdJvz!xBvhE07*qoM6N<$g1Po(w*UYD literal 0 HcmV?d00001 diff --git a/images/notes.png b/images/notes.png new file mode 100644 index 0000000000000000000000000000000000000000..07e3d04bad0cb94920fe8c8a6b6187da9595f4b7 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``#oJ8Lo80ey=<7}6ez%YVR79B zC2j%UTivclf;zUAUQ*HCu%*Cy+AO7P55XyK6nh1Z=w|%PJ*M#KuSQf=Vuj7S+VV6x zW7g@4j=a7m7j&L<%>PiO;U=qj?T53y{%wsT^DCVB`!!hSEBT(&_{}Z9{KO*dLh~Js zjztUiwK1-WWIH-zP2Qbo_x}5aj;g1BGT#6F(yA*lBP{aIKmNE6HH+SuoU4q>&&@G5 w%HfOBOzx4_+1OG0)1BS;kDE`ja`8sy*=IHMOdoe!0=>fE>FVdQ&MBb@0PXi-djJ3c literal 0 HcmV?d00001 diff --git a/images/notescross.png b/images/notescross.png new file mode 100644 index 0000000000000000000000000000000000000000..ed107e49aee157657332140425ddbc517ffd4485 GIT binary patch literal 444 zcmV;t0Ym<YP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0004lNkl<ZILnpO zJ7`ov7{&45y)0;>!9oZsE?OuGN*aj<ixfV?Y8pYXNhvmhonT>WWhtf+EGz`=f{1Z3 zvN3`Wf?^<o#eyQKTv-K;h8>HU$lX`a4=$WL%>T@pnQt<w0;5>TnAN{Z4><LKtruya zTq?k$NlPX}ID;~_qdda4mt^Z`R;W)gR4T%$js!x+0bId7+`P`fS#}NbEhqfsY7?bC zQIDQy(`3}uKpn@??AlYtOo+Oe!Wg^XVLv-D*a=oqLoV)l22Z+Tpu*lB39Gdv^3{=y zr@c&i!C73q-QXWh9v9&*gH+3F7_>SE+6)2*abgXFV}V0$x9}gZB{sa_@&rq_iw1$= z7ANN^e`E`MVe>Q^ERMx%+c(GQ7Ns3xjOWM#!`~Ud#kodoTQH4hQC<ivTw`jPjm1O{ z(;57lV^1HL*w(dIyy!SDzmaFm*SVXs0*~6^AF1Kor}CK_1AJP~*IC}a=F;DplT3_9 m{(buHq!+1U<->uI)&D=<-){c7N9Rib0000<MNUMnLSTY|0LLl- literal 0 HcmV?d00001 diff --git a/images/noup.png b/images/noup.png new file mode 100644 index 0000000000000000000000000000000000000000..8e6398b82cd44bf9dd282240ddf6b9bd3f809711 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`8$DedLo80ey|_QqDN%&=!~I}K zb`BwLYjKa{$2{L674;@OD(cpmu;zh|;toeGZ%1Q?77jD{PZPciUh?uQ{Xh4x<sCOx zLBm7dne&Z5?|!{*<%H!C$#HJ)E6$vG`!#oc^PVHiT*F@Z-Ym7wh>S}N@ch<%y==*{ zq@U(7N~VGv4@kc|Rn$}0T5)LKmG}y&2h9Ab7Dv?IX*02(DR}e8$%SX*(IrY74x1SF iU)BF3`k%>Mv4G8wPq&V@=8z@O@eH1>elF{r5}E+|30`Ob literal 0 HcmV?d00001 diff --git a/images/noupup.png b/images/noupup.png new file mode 100644 index 0000000000000000000000000000000000000000..29efac2b361ec12ecaea5d83731866bb370d604a GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`n><|{Lo7}|y|k8>DUs*cM|pWp zo`yqrnKGIZ*=%MpZs-U;5zWwhNPx{DA(E*xk%5QtviF0cau&w6Rr}t4{&U$ZU)eWU z^x{#YPps=w7nQYI9+X;K!EbnUrAE%Q8R`~xGhOR?;#3!O^GJX3I<WUe>cUz^l^e-% zo5S{fvf1LvCy^}PYMf`<5S=mUL(grCgn%6fSEclFr``B>koTQwgV?Y4c9+}(SxS<Q jtg>i-RxSKT?gPVK6Lr?3>c-JP*E4v!`njxgN@xNAx)NGp literal 0 HcmV?d00001 diff --git a/images/pause.png b/images/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f009402252e0ac08fc5a234c71e6f7c731927e GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`ik>cxAr_~TQyd!p^Q>qP?ClSo m*V*V~kab`M<6<@*F=htc3?_xfm}L<_#SEUVelF{r5}E*;&KExb literal 0 HcmV?d00001 diff --git a/images/play.png b/images/play.png new file mode 100644 index 0000000000000000000000000000000000000000..bf16543ef4c2d6f71d52bcfeaf702bbf46602a4d GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Rh}-6Ar_~XURLB|aujg6xV@ld zs|p7T`)px_um?M2)gu}YHay{LzUk>;5V=cR^X7i5xmo*44=F6{lHAR+pz@F!^NJmb z^BGO%U(XA+-uHL{qe=XRvkY4EEV^PUFEX!x!L~Gf+r0;($zM8BbB{W&+|Qu;DPrLj d|G#s8vDIu1Y@3|Kdl2Xf22WQ%mvv4FO#pugLy-Ug literal 0 HcmV?d00001 diff --git a/images/random.png b/images/random.png new file mode 100644 index 0000000000000000000000000000000000000000..ed2f03859da50d6dad138eb37b4b57504a6ce2ed GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`vpiiKLo7~jy=2IB$U&g>q5evh z-s>D@X4Ax)(l>0}c6oP&h;7@Ah-xP3?gaweg1JFD9kXIjHn%q3{l5QZV#RfiY?*?C zvg$bo*PPDlKX1ILe?ZQg=Yn%%D1)U}fFMKOL)Uvk2K=vLbAE}GPOfvCnYGTK$8h=% zvu9G1Z(Q4c@PU}-<n>Gr_N)SW0pDH*$}zrni#&a0|NaHW2e>njYghf<U+@;_N(N6? KKbLh*2~7Zjfl>$n literal 0 HcmV?d00001 diff --git a/images/randomcross.png b/images/randomcross.png new file mode 100644 index 0000000000000000000000000000000000000000..e3d515b6a007f849c1c1f6518f05831510b277ed GIT binary patch literal 440 zcmV;p0Z0CcP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0004hNkl<ZILmd? zKWh|09LDj_tcwx^|9~&Rkix=JL_IW|iJqRNSctv@!6NS<Z$LXMD+@vG)zeEbCQ;Nw ztWt^=Vr3_2qPZK3oe7)u?tz8h&itP5Z)fKTk|J$v2R3n2^*)0s{J=ZhiHuMNR@Kds z#mM4Wq6trn;R;Vu?<^E)ay!GW^8FTfaXd}mkJ~MDC_XTkTIV9Og@zWme~+J$!>Z%_ z`ocyh?Qe3h-d_e*VpX>rtcz)V)MFtp9^zXnmji1hBuK>hCS<_$fG(8$zQWUNhMgk> zUgK5WJz%=WjS=M@wyyDDhR4~w3($mo=Ew`Kk0=YayF5C^!@BuDK++cUxiDleoaFfl zzN#`{A|P@amsQC*n%7%wq_Pt^GZs)c!;pQsK9IgD=~5k1B=f1dIjfIlW3>Yb$=n^& z)DOH#1I}T7Oi!=GpOp2`Ft68cC!|o#@BU@}7J*NAR@-=)CpWHm4NLXufsZ+fgGh-G icx!?J>p9+m_wWy!0Cz$5wf0^B0000<MNUMnLSTYH_{p6B literal 0 HcmV?d00001 diff --git a/images/tick.png b/images/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..c9378be8b165d50159b6e94a4a58085579ac7b32 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J3U<-Lo80uy==&J$U&g>;r$tx z)D)7Q3%+@i%iJ`ze8VTPrf`8RU)YkeGqa_GysNYYE6p?y8F22t`EuING|4iyUosnA zd0uoDw+Q&V2z47DS+A#X#FlM&$D+p86-it&ykbdWUEzH9S{|)@8QoE=A*U3_Cf;$y z+fJdVxZLPRw?z5hwF29JJ>7cw`=!Ie>KjYmeao$_I%y{NHdtNg$WrG0f{QP(eoT@x q&gDL`ys^A?o33+6NA`(R{}_48_Ht$^PL=?Ag2B_(&t;ucLK6VM1Yb4) literal 0 HcmV?d00001 diff --git a/images/up.png b/images/up.png new file mode 100644 index 0000000000000000000000000000000000000000..52cbf045fe165dc501b6feb8a89a113cb1cb1e61 GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Rh}-6Ar`0KPB_TfU?9N4E_mj& z+?pd&I%hWJD7d~j%;ln3XC~C_c69y@gZX#f6-rx7XN*d$Si$vzLEUQa-5};3#~a33 zWtE?51iqa~pZaL>`WvCE7`eYBd9qADkUn)Oheo5~#)Jv!r=J`Uxmws+U-T(|X3z9r d;x$PJx#SmH{rMfI{1oU422WQ%mvv4FO#m9&MIQhF literal 0 HcmV?d00001 diff --git a/images/upup.png b/images/upup.png new file mode 100644 index 0000000000000000000000000000000000000000..50611bb22f14b20e841a812d951d520356f7a2d1 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`<(@8%Ar_~fPCCuopdjEfKW+)H z_C_}D%{o#GI4lF2RH`C{6>3b}^!+~R8En3(=n%;j!E)5Mw}Gw9@$^mAvklyb)cxMg z($1gx;bhpVDeLYitukR`i(rlC{B<B*<@ay%7e=Aqj@fq_+44{2lVFJc82xe2_JiM< Z`82~LugsNw-wAXAgQu&X%Q~loCIFcWJ`w-` literal 0 HcmV?d00001 diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..91809f7 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,93 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +lib_LTLIBRARIES=libdisorder.la +include_HEADERS=disorder.h +noinst_PROGRAMS=test + +# This library is there to share code between the DisOrder applications, +# not to provide an interface to anyone else. As such there are no +# guarantees regarding its ABI. +libdisorder_la_SOURCES=charset.c charset.h \ + addr.c addr.h \ + authhash.c authhash.h \ + basen.c basen.h \ + cache.c cache.h \ + client.c client.h \ + client-common.c client-common.h \ + configuration.c configuration.h \ + defs.c defs.h \ + eclient.c eclient.h \ + event.c event.h \ + eventlog.c eventlog.h \ + filepart.c filepart.h \ + hash.c hash.h \ + hex.c hex.h \ + inputline.c inputline.h \ + kvp.c kvp.h \ + log.c log.h log-impl.h \ + logfd.c logfd.h \ + mem.c mem.h mem-impl.h \ + mime.h mime.c \ + mixer.c mixer.h \ + plugin.c plugin.h \ + printf.c printf.h \ + asprintf.c fprintf.c snprintf.c \ + queue.c queue.h \ + regsub.c regsub.h \ + selection.c selection.h \ + signame.c signame.h \ + sink.c sink.h \ + speaker.c speaker.h \ + split.c split.h \ + syscalls.c syscalls.h \ + types.h \ + table.c table.h \ + trackname.c trackname.h \ + user.h user.c \ + utf8.h utf8.c \ + vacopy.h \ + vector.c vector.h \ + words.c words.h casefold.h unicodegc.h \ + wstat.c wstat.h \ + disorder.h +libdisorder_la_LIBADD=$(LIBGCRYPT) $(LIBGC) $(LIBICONV) $(LIBNSL) \ + $(LIBSOCKET) $(LIBDL) $(LIBPCRE) +libdisorder_la_LDFLAGS=-release ${VERSION} + +definitions.h: Makefile + rm -f $@.new + echo "#define PKGLIBDIR \"${pkglibdir}\"" > $@.new + echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new + echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new + echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new + mv $@.new $@ +defs.o: definitions.h +defs.lo: definitions.h + +test_SOURCES=test.c +test_LDADD=libdisorder.la $(LIBPCRE) +test_DEPENDENCIES=libdisorder.la + +check: test + ./test + +CLEANFILES=definitions.h +# arch-tag:a8730cb4f8b4517b6e37b1f717956d7c diff --git a/lib/addr.c b/lib/addr.c new file mode 100644 index 0000000..07224c6 --- /dev/null +++ b/lib/addr.c @@ -0,0 +1,101 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netdb.h> + +#include "log.h" +#include "printf.h" +#include "configuration.h" +#include "addr.h" + +struct addrinfo *get_address(const struct stringlist *a, + const struct addrinfo *pref, + char **namep) { + struct addrinfo *res; + char *name; + int rc; + + if(a->n == 1) { + byte_xasprintf(&name, "host * service %s", a->s[0]); + if((rc = getaddrinfo(0, a->s[0], pref, &res))) { + error(0, "getaddrinfo %s: %s", a->s[0], gai_strerror(rc)); + return 0; + } + } else { + byte_xasprintf(&name, "host %s service %s", a->s[0], a->s[1]); + if((rc = getaddrinfo(a->s[0], a->s[1], pref, &res))) { + error(0, "getaddrinfo %s %s: %s", a->s[0], a->s[1], gai_strerror(rc)); + return 0; + } + } + if(!res || res->ai_socktype != SOCK_STREAM) { + error(0, "getaddrinfo didn't give us a stream socket"); + if(res) + freeaddrinfo(res); + return 0; + } + if(namep) + *namep = name; + return res; +} + +int addrinfocmp(const struct addrinfo *a, + const struct addrinfo *b) { + const struct sockaddr_in *ina, *inb; + const struct sockaddr_in6 *in6a, *in6b; + + if(a->ai_family != b->ai_family) return a->ai_family - b->ai_family; + if(a->ai_socktype != b->ai_socktype) return a->ai_socktype - b->ai_socktype; + if(a->ai_protocol != b->ai_protocol) return a->ai_protocol - b->ai_protocol; + switch(a->ai_protocol) { + case PF_INET: + ina = (const struct sockaddr_in *)a->ai_addr; + inb = (const struct sockaddr_in *)b->ai_addr; + if(ina->sin_port != inb->sin_port) return ina->sin_port - inb->sin_port; + return ina->sin_addr.s_addr - inb->sin_addr.s_addr; + break; + case PF_INET6: + in6a = (const struct sockaddr_in6 *)a->ai_addr; + in6b = (const struct sockaddr_in6 *)b->ai_addr; + if(in6a->sin6_port != in6b->sin6_port) + return in6a->sin6_port - in6b->sin6_port; + return memcmp(&in6a->sin6_addr, &in6b->sin6_addr, + sizeof (struct in6_addr)); + default: + error(0, "unsupported protocol family %d", a->ai_protocol); + return memcmp(a->ai_addr, b->ai_addr, a->ai_addrlen); /* kludge */ + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e143b5b1b677e108d957da2f6d09bccd */ diff --git a/lib/addr.h b/lib/addr.h new file mode 100644 index 0000000..c20a4c8 --- /dev/null +++ b/lib/addr.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef ADDR_H +#define ADDR_H + +struct addrinfo *get_address(const struct stringlist *a, + const struct addrinfo *pref, + char **namep); + +int addrinfocmp(const struct addrinfo *a, + const struct addrinfo *b); + +#endif /* ADDR_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3502f44d5fa2fd1825e8169df59fa864 */ diff --git a/lib/asprintf.c b/lib/asprintf.c new file mode 100644 index 0000000..3abe0db --- /dev/null +++ b/lib/asprintf.c @@ -0,0 +1,90 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> +#include <errno.h> + +#include "printf.h" +#include "sink.h" +#include "mem.h" +#include "vector.h" +#include "log.h" + +int byte_vasprintf(char **ptrp, + const char *fmt, + va_list ap) { + struct dynstr d; + int n; + + dynstr_init(&d); + if((n = byte_vsinkprintf(sink_dynstr(&d), fmt, ap)) >= 0) { + dynstr_terminate(&d); + *ptrp = d.vec; + } + return n; +} + +int byte_asprintf(char **ptrp, + const char *fmt, + ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vasprintf(ptrp, fmt, ap); + va_end(ap); + return n; +} + +int byte_xasprintf(char **ptrp, + const char *fmt, + ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_xvasprintf(ptrp, fmt, ap); + va_end(ap); + return n; +} + +int byte_xvasprintf(char **ptrp, + const char *fmt, + va_list ap) { + int n; + + if((n = byte_vasprintf(ptrp, fmt, ap)) < 0) + fatal(errno, "error calling byte_vasprintf"); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:39488c5fd6a6d176e613e7e747e55628 */ diff --git a/lib/authhash.c b/lib/authhash.c new file mode 100644 index 0000000..360d7ef --- /dev/null +++ b/lib/authhash.c @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <gcrypt.h> + +#include "hex.h" +#include "log.h" +#include "authhash.h" + +#ifndef AUTHHASH +# define AUTHHASH GCRY_MD_SHA1 +#endif + +const char *authhash(const void *challenge, size_t nchallenge, + const char *password) { + gcrypt_hash_handle h; + const char *res; + +#if HAVE_GCRY_ERROR_T + { + gcry_error_t e; + + if((e = gcry_md_open(&h, AUTHHASH, 0))) { + error(0, "gcry_md_open: %s", gcry_strerror(e)); + return 0; + } + } +#else + h = gcry_md_open(AUTHHASH, 0); +#endif + gcry_md_write(h, password, strlen(password)); + gcry_md_write(h, challenge, nchallenge); + res = hex(gcry_md_read(h, AUTHHASH), gcry_md_get_algo_dlen(AUTHHASH)); + gcry_md_close(h); + return res; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:fbc5c3876475fbeca3f7813dff16352d */ diff --git a/lib/authhash.h b/lib/authhash.h new file mode 100644 index 0000000..4ef57ae --- /dev/null +++ b/lib/authhash.h @@ -0,0 +1,35 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef AUTHHASH_H +#define AUTHHASH_H + +const char *authhash(const void *challenge, size_t nchallenge, + const char *user); + +#endif /* AUTHHASH_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:225ea2aea8c434a7a92fae4018e54c60 */ diff --git a/lib/basen.c b/lib/basen.c new file mode 100644 index 0000000..157919a --- /dev/null +++ b/lib/basen.c @@ -0,0 +1,81 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "basen.h" + +/* test whether v is 0 */ +static int zero(const unsigned long *v, int nwords) { + int n; + + for(n = 0; n < nwords && !v[n]; ++n) + ; + return n == nwords; +} + +/* divide v by m returning the remainder */ +static unsigned divide(unsigned long *v, int nwords, unsigned long m) { + unsigned long r = 0, a, b; + int n; + + /* we do the divide 16 bits at a time */ + for(n = 0; n < nwords; ++n) { + a = v[n] >> 16; + b = v[n] & 0xFFFF; + a += r << 16; + r = a % m; + a /= m; + b += r << 16; + r = b % m; + b /= m; + v[n] = (a << 16) + b; + } + return r; +} + +int basen(unsigned long *v, + int nwords, + char buffer[], + size_t bufsize, + unsigned base) { + size_t i = bufsize; + static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + do { + if(i <= 1) return -1; /* overflow */ + buffer[--i] = chars[divide(v, nwords, base)]; + } while(!zero(v, nwords)); + memmove(buffer, buffer + i, bufsize - i); + buffer[bufsize - i] = 0; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:iGDXjhkM2cdyv0RSlftgGQ */ diff --git a/lib/basen.h b/lib/basen.h new file mode 100644 index 0000000..aedd5cf --- /dev/null +++ b/lib/basen.h @@ -0,0 +1,42 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef BASEN_H +#define BASEN_H + +int basen(unsigned long *v, + int nwords, + char buffer[], + size_t bufsize, + unsigned base); +/* convert the big-endian value at @v@ composed of @nwords@ 32-bit words + * into a base-@base@ string and store in @buffer@. + * Returns 0 on success or -1 on overflow. + */ + +#endif /* BASEN_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:mJ+XdWbrDM6Ft/J6YXXb4A */ diff --git a/lib/cache.c b/lib/cache.c new file mode 100644 index 0000000..df8fa68 --- /dev/null +++ b/lib/cache.c @@ -0,0 +1,108 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <time.h> + +#include "hash.h" +#include "mem.h" +#include "log.h" +#include "cache.h" + +static hash *h; + +struct cache_entry { + const struct cache_type *type; + const void *value; + time_t birth; +}; + +static int expired(const struct cache_entry *c, time_t now) { + return now - c->birth > c->type->lifetime; +} + +void cache_put(const struct cache_type *type, + const char *key, const void *value) { + struct cache_entry *c; + + if(!h) + h = hash_new(sizeof (struct cache_entry)); + c = xmalloc(sizeof *c); + c->type = type; + c->value = value; + time(&c->birth); + hash_add(h, key, c, HASH_INSERT_OR_REPLACE); +} + +const void *cache_get(const struct cache_type *type, const char *key) { + const struct cache_entry *c; + + if(h + && (c = hash_find(h, key)) + && c->type == type + && !expired(c, time(0))) + return c->value; + else + return 0; +} + +static int expiry_callback(const char *key, void *value, void *u) { + const struct cache_entry *c = value; + const time_t *now = u; + + if(expired(c, *now)) + hash_remove(h, key); + return 0; +} + +void cache_expire(void) { + time_t now; + + if(h) { + time(&now); + hash_foreach(h, expiry_callback, &now); + } +} + +static int clean_callback(const char *key, void *value, void *u) { + const struct cache_entry *c = value; + const struct cache_type *type = u; + + if(!type || c->type == type) + hash_remove(h, key); + return 0; +} + +void cache_clean(const struct cache_type *type) { + if(h) + hash_foreach(h, clean_callback, (void *)type); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:uoFZfd12rkQj/ppG5g3BtQ */ diff --git a/lib/cache.h b/lib/cache.h new file mode 100644 index 0000000..f1aeccd --- /dev/null +++ b/lib/cache.h @@ -0,0 +1,54 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CACHE_H +#define CACHE_H + +/* Defines a cache mapping keys to typed data items */ + +struct cache_type { + int lifetime; /* Lifetime of a cache entry */ +}; + +void cache_put(const struct cache_type *type, + const char *key, const void *value); +/* Inserts KEY into the cache with value VALUE. If KEY is already + * present it is overwritten. */ + +const void *cache_get(const struct cache_type *type, const char *key); +/* Get a value from the cache. */ + +void cache_expire(void); +/* Expire values from the cache */ + +void cache_clean(const struct cache_type *type); +/* Clean all elements of a particular type, or all elements if TYPE=0 */ + +#endif /* CACHE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:8TQYM58jrfK4bi9YaWTutQ */ diff --git a/lib/casefold.h b/lib/casefold.h new file mode 100644 index 0000000..5b00c2b --- /dev/null +++ b/lib/casefold.h @@ -0,0 +1,915 @@ +struct cm { + uint32_t ch; + const char *tr; +} cm0[] = { + { 192, "\xC3\xA0" }, + { 256, "\xC4\x81" }, + { 512, "\xC8\x81" }, + { 1024, "\xD1\x90" }, + { 1152, "\xD2\x81" }, + { 1280, "\xD4\x81" }, + { 1344, "\xD5\xB0" }, + { 7680, "\xE1\xB8\x81" }, + { 7744, "\xE1\xB9\x81" }, + { 7808, "\xE1\xBA\x81" }, + { 7872, "\xE1\xBB\x81" }, + { 8064, "\xE1\xBC\x80\xCE\xB9" }, + { 9408, "\xE2\x93\x9A" }, + { 64256, "ff" }, + { 66560, "\xF0\x90\x90\xA8" }, +}, cm1[] = { + { 65, "a" }, + { 193, "\xC3\xA1" }, + { 321, "\xC5\x82" }, + { 385, "\xC9\x93" }, + { 1025, "\xD1\x91" }, + { 1217, "\xD3\x82" }, + { 1345, "\xD5\xB1" }, + { 8065, "\xE1\xBC\x81\xCE\xB9" }, + { 9409, "\xE2\x93\x9B" }, + { 64257, "fi" }, + { 66561, "\xF0\x90\x90\xA9" }, +}, cm2[] = { + { 66, "b" }, + { 194, "\xC3\xA2" }, + { 258, "\xC4\x83" }, + { 386, "\xC6\x83" }, + { 514, "\xC8\x83" }, + { 962, "\xCF\x83" }, + { 1026, "\xD1\x92" }, + { 1282, "\xD4\x83" }, + { 1346, "\xD5\xB2" }, + { 7682, "\xE1\xB8\x83" }, + { 7746, "\xE1\xB9\x83" }, + { 7810, "\xE1\xBA\x83" }, + { 7874, "\xE1\xBB\x83" }, + { 8066, "\xE1\xBC\x82\xCE\xB9" }, + { 8130, "\xE1\xBD\xB4\xCE\xB9" }, + { 9410, "\xE2\x93\x9C" }, + { 64258, "fl" }, + { 66562, "\xF0\x90\x90\xAA" }, +}, cm3[] = { + { 67, "c" }, + { 195, "\xC3\xA3" }, + { 323, "\xC5\x84" }, + { 1027, "\xD1\x93" }, + { 1219, "\xD3\x84" }, + { 1347, "\xD5\xB3" }, + { 8067, "\xE1\xBC\x83\xCE\xB9" }, + { 8131, "\xCE\xB7\xCE\xB9" }, + { 9411, "\xE2\x93\x9D" }, + { 64259, "ffi" }, + { 66563, "\xF0\x90\x90\xAB" }, +}, cm4[] = { + { 68, "d" }, + { 196, "\xC3\xA4" }, + { 260, "\xC4\x85" }, + { 388, "\xC6\x85" }, + { 452, "\xC7\x86" }, + { 516, "\xC8\x85" }, + { 1028, "\xD1\x94" }, + { 1284, "\xD4\x85" }, + { 1348, "\xD5\xB4" }, + { 7684, "\xE1\xB8\x85" }, + { 7748, "\xE1\xB9\x85" }, + { 7812, "\xE1\xBA\x85" }, + { 7876, "\xE1\xBB\x85" }, + { 8068, "\xE1\xBC\x84\xCE\xB9" }, + { 8132, "\xCE\xAE\xCE\xB9" }, + { 9412, "\xE2\x93\x9E" }, + { 64260, "ffl" }, + { 66564, "\xF0\x90\x90\xAC" }, +}, cm5[] = { + { 69, "e" }, + { 197, "\xC3\xA5" }, + { 325, "\xC5\x86" }, + { 453, "\xC7\x86" }, + { 837, "\xCE\xB9" }, + { 1029, "\xD1\x95" }, + { 1221, "\xD3\x86" }, + { 1349, "\xD5\xB5" }, + { 8069, "\xE1\xBC\x85\xCE\xB9" }, + { 9413, "\xE2\x93\x9F" }, + { 64261, "st" }, + { 66565, "\xF0\x90\x90\xAD" }, +}, cm6[] = { + { 70, "f" }, + { 198, "\xC3\xA6" }, + { 262, "\xC4\x87" }, + { 390, "\xC9\x94" }, + { 518, "\xC8\x87" }, + { 902, "\xCE\xAC" }, + { 1030, "\xD1\x96" }, + { 1286, "\xD4\x87" }, + { 1350, "\xD5\xB6" }, + { 7686, "\xE1\xB8\x87" }, + { 7750, "\xE1\xB9\x87" }, + { 7814, "\xE1\xBA\x87" }, + { 7878, "\xE1\xBB\x87" }, + { 8070, "\xE1\xBC\x86\xCE\xB9" }, + { 8134, "\xCE\xB7\xCD\x82" }, + { 9414, "\xE2\x93\xA0" }, + { 64262, "st" }, + { 66566, "\xF0\x90\x90\xAE" }, +}, cm7[] = { + { 71, "g" }, + { 199, "\xC3\xA7" }, + { 327, "\xC5\x88" }, + { 391, "\xC6\x88" }, + { 455, "\xC7\x89" }, + { 1031, "\xD1\x97" }, + { 1223, "\xD3\x88" }, + { 1351, "\xD5\xB7" }, + { 1415, "\xD5\xA5\xD6\x82" }, + { 8071, "\xE1\xBC\x87\xCE\xB9" }, + { 8135, "\xCE\xB7\xCD\x82\xCE\xB9" }, + { 9415, "\xE2\x93\xA1" }, + { 66567, "\xF0\x90\x90\xAF" }, +}, cm8[] = { + { 72, "h" }, + { 200, "\xC3\xA8" }, + { 264, "\xC4\x89" }, + { 456, "\xC7\x89" }, + { 520, "\xC8\x89" }, + { 904, "\xCE\xAD" }, + { 1032, "\xD1\x98" }, + { 1288, "\xD4\x89" }, + { 1352, "\xD5\xB8" }, + { 7688, "\xE1\xB8\x89" }, + { 7752, "\xE1\xB9\x89" }, + { 7816, "\xE1\xBA\x89" }, + { 7880, "\xE1\xBB\x89" }, + { 7944, "\xE1\xBC\x80" }, + { 8008, "\xE1\xBD\x80" }, + { 8072, "\xE1\xBC\x80\xCE\xB9" }, + { 8136, "\xE1\xBD\xB2" }, + { 9416, "\xE2\x93\xA2" }, + { 66568, "\xF0\x90\x90\xB0" }, +}, cm9[] = { + { 73, "i" }, + { 201, "\xC3\xA9" }, + { 329, "\xCA\xBCn" }, + { 393, "\xC9\x96" }, + { 905, "\xCE\xAE" }, + { 1033, "\xD1\x99" }, + { 1225, "\xD3\x8A" }, + { 1353, "\xD5\xB9" }, + { 7945, "\xE1\xBC\x81" }, + { 8009, "\xE1\xBD\x81" }, + { 8073, "\xE1\xBC\x81\xCE\xB9" }, + { 8137, "\xE1\xBD\xB3" }, + { 9417, "\xE2\x93\xA3" }, + { 66569, "\xF0\x90\x90\xB1" }, +}, cm10[] = { + { 74, "j" }, + { 202, "\xC3\xAA" }, + { 266, "\xC4\x8B" }, + { 330, "\xC5\x8B" }, + { 394, "\xC9\x97" }, + { 458, "\xC7\x8C" }, + { 522, "\xC8\x8B" }, + { 906, "\xCE\xAF" }, + { 1034, "\xD1\x9A" }, + { 1162, "\xD2\x8B" }, + { 1290, "\xD4\x8B" }, + { 1354, "\xD5\xBA" }, + { 7690, "\xE1\xB8\x8B" }, + { 7754, "\xE1\xB9\x8B" }, + { 7818, "\xE1\xBA\x8B" }, + { 7882, "\xE1\xBB\x8B" }, + { 7946, "\xE1\xBC\x82" }, + { 8010, "\xE1\xBD\x82" }, + { 8074, "\xE1\xBC\x82\xCE\xB9" }, + { 8138, "\xE1\xBD\xB4" }, + { 9418, "\xE2\x93\xA4" }, + { 66570, "\xF0\x90\x90\xB2" }, +}, cm11[] = { + { 75, "k" }, + { 203, "\xC3\xAB" }, + { 395, "\xC6\x8C" }, + { 459, "\xC7\x8C" }, + { 1035, "\xD1\x9B" }, + { 1227, "\xD3\x8C" }, + { 1355, "\xD5\xBB" }, + { 7947, "\xE1\xBC\x83" }, + { 8011, "\xE1\xBD\x83" }, + { 8075, "\xE1\xBC\x83\xCE\xB9" }, + { 8139, "\xE1\xBD\xB5" }, + { 9419, "\xE2\x93\xA5" }, + { 66571, "\xF0\x90\x90\xB3" }, +}, cm12[] = { + { 76, "l" }, + { 204, "\xC3\xAC" }, + { 268, "\xC4\x8D" }, + { 332, "\xC5\x8D" }, + { 524, "\xC8\x8D" }, + { 908, "\xCF\x8C" }, + { 1036, "\xD1\x9C" }, + { 1164, "\xD2\x8D" }, + { 1292, "\xD4\x8D" }, + { 1356, "\xD5\xBC" }, + { 7692, "\xE1\xB8\x8D" }, + { 7756, "\xE1\xB9\x8D" }, + { 7820, "\xE1\xBA\x8D" }, + { 7884, "\xE1\xBB\x8D" }, + { 7948, "\xE1\xBC\x84" }, + { 8012, "\xE1\xBD\x84" }, + { 8076, "\xE1\xBC\x84\xCE\xB9" }, + { 8140, "\xCE\xB7\xCE\xB9" }, + { 9420, "\xE2\x93\xA6" }, + { 66572, "\xF0\x90\x90\xB4" }, +}, cm13[] = { + { 77, "m" }, + { 205, "\xC3\xAD" }, + { 461, "\xC7\x8E" }, + { 1037, "\xD1\x9D" }, + { 1229, "\xD3\x8E" }, + { 1357, "\xD5\xBD" }, + { 7949, "\xE1\xBC\x85" }, + { 8013, "\xE1\xBD\x85" }, + { 8077, "\xE1\xBC\x85\xCE\xB9" }, + { 9421, "\xE2\x93\xA7" }, + { 66573, "\xF0\x90\x90\xB5" }, +}, cm14[] = { + { 78, "n" }, + { 206, "\xC3\xAE" }, + { 270, "\xC4\x8F" }, + { 334, "\xC5\x8F" }, + { 398, "\xC7\x9D" }, + { 526, "\xC8\x8F" }, + { 910, "\xCF\x8D" }, + { 1038, "\xD1\x9E" }, + { 1166, "\xD2\x8F" }, + { 1294, "\xD4\x8F" }, + { 1358, "\xD5\xBE" }, + { 7694, "\xE1\xB8\x8F" }, + { 7758, "\xE1\xB9\x8F" }, + { 7822, "\xE1\xBA\x8F" }, + { 7886, "\xE1\xBB\x8F" }, + { 7950, "\xE1\xBC\x86" }, + { 8078, "\xE1\xBC\x86\xCE\xB9" }, + { 9422, "\xE2\x93\xA8" }, + { 66574, "\xF0\x90\x90\xB6" }, +}, cm15[] = { + { 79, "o" }, + { 207, "\xC3\xAF" }, + { 399, "\xC9\x99" }, + { 463, "\xC7\x90" }, + { 911, "\xCF\x8E" }, + { 1039, "\xD1\x9F" }, + { 1359, "\xD5\xBF" }, + { 7951, "\xE1\xBC\x87" }, + { 8079, "\xE1\xBC\x87\xCE\xB9" }, + { 9423, "\xE2\x93\xA9" }, + { 66575, "\xF0\x90\x90\xB7" }, +}, cm16[] = { + { 80, "p" }, + { 208, "\xC3\xB0" }, + { 272, "\xC4\x91" }, + { 336, "\xC5\x91" }, + { 400, "\xC9\x9B" }, + { 528, "\xC8\x91" }, + { 912, "\xCE\xB9\xCC\x88\xCC\x81" }, + { 976, "\xCE\xB2" }, + { 1040, "\xD0\xB0" }, + { 1168, "\xD2\x91" }, + { 1232, "\xD3\x91" }, + { 1360, "\xD6\x80" }, + { 7696, "\xE1\xB8\x91" }, + { 7760, "\xE1\xB9\x91" }, + { 7824, "\xE1\xBA\x91" }, + { 7888, "\xE1\xBB\x91" }, + { 8016, "\xCF\x85\xCC\x93" }, + { 8080, "\xE1\xBC\xA0\xCE\xB9" }, + { 66576, "\xF0\x90\x90\xB8" }, +}, cm17[] = { + { 81, "q" }, + { 209, "\xC3\xB1" }, + { 401, "\xC6\x92" }, + { 465, "\xC7\x92" }, + { 913, "\xCE\xB1" }, + { 977, "\xCE\xB8" }, + { 1041, "\xD0\xB1" }, + { 1361, "\xD6\x81" }, + { 8081, "\xE1\xBC\xA1\xCE\xB9" }, + { 66577, "\xF0\x90\x90\xB9" }, +}, cm18[] = { + { 82, "r" }, + { 210, "\xC3\xB2" }, + { 274, "\xC4\x93" }, + { 338, "\xC5\x93" }, + { 530, "\xC8\x93" }, + { 914, "\xCE\xB2" }, + { 1042, "\xD0\xB2" }, + { 1170, "\xD2\x93" }, + { 1234, "\xD3\x93" }, + { 1362, "\xD6\x82" }, + { 7698, "\xE1\xB8\x93" }, + { 7762, "\xE1\xB9\x93" }, + { 7826, "\xE1\xBA\x93" }, + { 7890, "\xE1\xBB\x93" }, + { 8018, "\xCF\x85\xCC\x93\xCC\x80" }, + { 8082, "\xE1\xBC\xA2\xCE\xB9" }, + { 8146, "\xCE\xB9\xCC\x88\xCC\x80" }, + { 66578, "\xF0\x90\x90\xBA" }, +}, cm19[] = { + { 83, "s" }, + { 211, "\xC3\xB3" }, + { 403, "\xC9\xA0" }, + { 467, "\xC7\x94" }, + { 915, "\xCE\xB3" }, + { 1043, "\xD0\xB3" }, + { 1363, "\xD6\x83" }, + { 8083, "\xE1\xBC\xA3\xCE\xB9" }, + { 8147, "\xCE\xB9\xCC\x88\xCC\x81" }, + { 64275, "\xD5\xB4\xD5\xB6" }, + { 66579, "\xF0\x90\x90\xBB" }, +}, cm20[] = { + { 84, "t" }, + { 212, "\xC3\xB4" }, + { 276, "\xC4\x95" }, + { 340, "\xC5\x95" }, + { 404, "\xC9\xA3" }, + { 532, "\xC8\x95" }, + { 916, "\xCE\xB4" }, + { 1044, "\xD0\xB4" }, + { 1172, "\xD2\x95" }, + { 1236, "\xD3\x95" }, + { 1364, "\xD6\x84" }, + { 7700, "\xE1\xB8\x95" }, + { 7764, "\xE1\xB9\x95" }, + { 7828, "\xE1\xBA\x95" }, + { 7892, "\xE1\xBB\x95" }, + { 8020, "\xCF\x85\xCC\x93\xCC\x81" }, + { 8084, "\xE1\xBC\xA4\xCE\xB9" }, + { 64276, "\xD5\xB4\xD5\xA5" }, + { 66580, "\xF0\x90\x90\xBC" }, +}, cm21[] = { + { 85, "u" }, + { 213, "\xC3\xB5" }, + { 469, "\xC7\x96" }, + { 917, "\xCE\xB5" }, + { 981, "\xCF\x86" }, + { 1045, "\xD0\xB5" }, + { 1365, "\xD6\x85" }, + { 8085, "\xE1\xBC\xA5\xCE\xB9" }, + { 64277, "\xD5\xB4\xD5\xAB" }, + { 66581, "\xF0\x90\x90\xBD" }, +}, cm22[] = { + { 86, "v" }, + { 214, "\xC3\xB6" }, + { 278, "\xC4\x97" }, + { 342, "\xC5\x97" }, + { 406, "\xC9\xA9" }, + { 534, "\xC8\x97" }, + { 918, "\xCE\xB6" }, + { 982, "\xCF\x80" }, + { 1046, "\xD0\xB6" }, + { 1174, "\xD2\x97" }, + { 1238, "\xD3\x97" }, + { 1366, "\xD6\x86" }, + { 7702, "\xE1\xB8\x97" }, + { 7766, "\xE1\xB9\x97" }, + { 7830, "h\xCC\xB1" }, + { 7894, "\xE1\xBB\x97" }, + { 8022, "\xCF\x85\xCC\x93\xCD\x82" }, + { 8086, "\xE1\xBC\xA6\xCE\xB9" }, + { 8150, "\xCE\xB9\xCD\x82" }, + { 64278, "\xD5\xBE\xD5\xB6" }, + { 66582, "\xF0\x90\x90\xBE" }, +}, cm23[] = { + { 87, "w" }, + { 407, "\xC9\xA8" }, + { 471, "\xC7\x98" }, + { 919, "\xCE\xB7" }, + { 1047, "\xD0\xB7" }, + { 7831, "t\xCC\x88" }, + { 8087, "\xE1\xBC\xA7\xCE\xB9" }, + { 8151, "\xCE\xB9\xCC\x88\xCD\x82" }, + { 64279, "\xD5\xB4\xD5\xAD" }, + { 66583, "\xF0\x90\x90\xBF" }, +}, cm24[] = { + { 88, "x" }, + { 216, "\xC3\xB8" }, + { 280, "\xC4\x99" }, + { 344, "\xC5\x99" }, + { 408, "\xC6\x99" }, + { 536, "\xC8\x99" }, + { 920, "\xCE\xB8" }, + { 984, "\xCF\x99" }, + { 1048, "\xD0\xB8" }, + { 1176, "\xD2\x99" }, + { 1240, "\xD3\x99" }, + { 7704, "\xE1\xB8\x99" }, + { 7768, "\xE1\xB9\x99" }, + { 7832, "w\xCC\x8A" }, + { 7896, "\xE1\xBB\x99" }, + { 7960, "\xE1\xBC\x90" }, + { 8088, "\xE1\xBC\xA0\xCE\xB9" }, + { 8152, "\xE1\xBF\x90" }, + { 66584, "\xF0\x90\x91\x80" }, +}, cm25[] = { + { 89, "y" }, + { 217, "\xC3\xB9" }, + { 473, "\xC7\x9A" }, + { 921, "\xCE\xB9" }, + { 1049, "\xD0\xB9" }, + { 7833, "y\xCC\x8A" }, + { 7961, "\xE1\xBC\x91" }, + { 8025, "\xE1\xBD\x91" }, + { 8089, "\xE1\xBC\xA1\xCE\xB9" }, + { 8153, "\xE1\xBF\x91" }, + { 66585, "\xF0\x90\x91\x81" }, +}, cm26[] = { + { 90, "z" }, + { 218, "\xC3\xBA" }, + { 282, "\xC4\x9B" }, + { 346, "\xC5\x9B" }, + { 538, "\xC8\x9B" }, + { 922, "\xCE\xBA" }, + { 986, "\xCF\x9B" }, + { 1050, "\xD0\xBA" }, + { 1178, "\xD2\x9B" }, + { 1242, "\xD3\x9B" }, + { 7706, "\xE1\xB8\x9B" }, + { 7770, "\xE1\xB9\x9B" }, + { 7834, "a\xCA\xBE" }, + { 7898, "\xE1\xBB\x9B" }, + { 7962, "\xE1\xBC\x92" }, + { 8090, "\xE1\xBC\xA2\xCE\xB9" }, + { 8154, "\xE1\xBD\xB6" }, + { 66586, "\xF0\x90\x91\x82" }, +}, cm27[] = { + { 219, "\xC3\xBB" }, + { 475, "\xC7\x9C" }, + { 923, "\xCE\xBB" }, + { 1051, "\xD0\xBB" }, + { 7835, "\xE1\xB9\xA1" }, + { 7963, "\xE1\xBC\x93" }, + { 8027, "\xE1\xBD\x93" }, + { 8091, "\xE1\xBC\xA3\xCE\xB9" }, + { 8155, "\xE1\xBD\xB7" }, + { 66587, "\xF0\x90\x91\x83" }, +}, cm28[] = { + { 220, "\xC3\xBC" }, + { 284, "\xC4\x9D" }, + { 348, "\xC5\x9D" }, + { 412, "\xC9\xAF" }, + { 540, "\xC8\x9D" }, + { 924, "\xCE\xBC" }, + { 988, "\xCF\x9D" }, + { 1052, "\xD0\xBC" }, + { 1180, "\xD2\x9D" }, + { 1244, "\xD3\x9D" }, + { 7708, "\xE1\xB8\x9D" }, + { 7772, "\xE1\xB9\x9D" }, + { 7900, "\xE1\xBB\x9D" }, + { 7964, "\xE1\xBC\x94" }, + { 8092, "\xE1\xBC\xA4\xCE\xB9" }, + { 66588, "\xF0\x90\x91\x84" }, +}, cm29[] = { + { 221, "\xC3\xBD" }, + { 413, "\xC9\xB2" }, + { 925, "\xCE\xBD" }, + { 1053, "\xD0\xBD" }, + { 7965, "\xE1\xBC\x95" }, + { 8029, "\xE1\xBD\x95" }, + { 8093, "\xE1\xBC\xA5\xCE\xB9" }, + { 66589, "\xF0\x90\x91\x85" }, +}, cm30[] = { + { 222, "\xC3\xBE" }, + { 286, "\xC4\x9F" }, + { 350, "\xC5\x9F" }, + { 478, "\xC7\x9F" }, + { 542, "\xC8\x9F" }, + { 926, "\xCE\xBE" }, + { 990, "\xCF\x9F" }, + { 1054, "\xD0\xBE" }, + { 1182, "\xD2\x9F" }, + { 1246, "\xD3\x9F" }, + { 7710, "\xE1\xB8\x9F" }, + { 7774, "\xE1\xB9\x9F" }, + { 7902, "\xE1\xBB\x9F" }, + { 8094, "\xE1\xBC\xA6\xCE\xB9" }, + { 66590, "\xF0\x90\x91\x86" }, +}, cm31[] = { + { 223, "ss" }, + { 415, "\xC9\xB5" }, + { 927, "\xCE\xBF" }, + { 1055, "\xD0\xBF" }, + { 8031, "\xE1\xBD\x97" }, + { 8095, "\xE1\xBC\xA7\xCE\xB9" }, + { 66591, "\xF0\x90\x91\x87" }, +}, cm32[] = { + { 288, "\xC4\xA1" }, + { 352, "\xC5\xA1" }, + { 416, "\xC6\xA1" }, + { 480, "\xC7\xA1" }, + { 544, "\xC6\x9E" }, + { 928, "\xCF\x80" }, + { 992, "\xCF\xA1" }, + { 1056, "\xD1\x80" }, + { 1120, "\xD1\xA1" }, + { 1184, "\xD2\xA1" }, + { 1248, "\xD3\xA1" }, + { 7712, "\xE1\xB8\xA1" }, + { 7776, "\xE1\xB9\xA1" }, + { 7840, "\xE1\xBA\xA1" }, + { 7904, "\xE1\xBB\xA1" }, + { 8096, "\xE1\xBD\xA0\xCE\xB9" }, + { 8544, "\xE2\x85\xB0" }, + { 66592, "\xF0\x90\x91\x88" }, +}, cm33[] = { + { 929, "\xCF\x81" }, + { 1057, "\xD1\x81" }, + { 8097, "\xE1\xBD\xA1\xCE\xB9" }, + { 8545, "\xE2\x85\xB1" }, + { 65313, "\xEF\xBD\x81" }, + { 66593, "\xF0\x90\x91\x89" }, +}, cm34[] = { + { 290, "\xC4\xA3" }, + { 354, "\xC5\xA3" }, + { 418, "\xC6\xA3" }, + { 482, "\xC7\xA3" }, + { 546, "\xC8\xA3" }, + { 994, "\xCF\xA3" }, + { 1058, "\xD1\x82" }, + { 1122, "\xD1\xA3" }, + { 1186, "\xD2\xA3" }, + { 1250, "\xD3\xA3" }, + { 7714, "\xE1\xB8\xA3" }, + { 7778, "\xE1\xB9\xA3" }, + { 7842, "\xE1\xBA\xA3" }, + { 7906, "\xE1\xBB\xA3" }, + { 8098, "\xE1\xBD\xA2\xCE\xB9" }, + { 8162, "\xCF\x85\xCC\x88\xCC\x80" }, + { 8546, "\xE2\x85\xB2" }, + { 65314, "\xEF\xBD\x82" }, + { 66594, "\xF0\x90\x91\x8A" }, +}, cm35[] = { + { 931, "\xCF\x83" }, + { 1059, "\xD1\x83" }, + { 8099, "\xE1\xBD\xA3\xCE\xB9" }, + { 8163, "\xCF\x85\xCC\x88\xCC\x81" }, + { 8547, "\xE2\x85\xB3" }, + { 65315, "\xEF\xBD\x83" }, + { 66595, "\xF0\x90\x91\x8B" }, +}, cm36[] = { + { 292, "\xC4\xA5" }, + { 356, "\xC5\xA5" }, + { 420, "\xC6\xA5" }, + { 484, "\xC7\xA5" }, + { 548, "\xC8\xA5" }, + { 932, "\xCF\x84" }, + { 996, "\xCF\xA5" }, + { 1060, "\xD1\x84" }, + { 1124, "\xD1\xA5" }, + { 1188, "\xD2\xA5" }, + { 1252, "\xD3\xA5" }, + { 7716, "\xE1\xB8\xA5" }, + { 7780, "\xE1\xB9\xA5" }, + { 7844, "\xE1\xBA\xA5" }, + { 7908, "\xE1\xBB\xA5" }, + { 8100, "\xE1\xBD\xA4\xCE\xB9" }, + { 8164, "\xCF\x81\xCC\x93" }, + { 8548, "\xE2\x85\xB4" }, + { 65316, "\xEF\xBD\x84" }, + { 66596, "\xF0\x90\x91\x8C" }, +}, cm37[] = { + { 933, "\xCF\x85" }, + { 1061, "\xD1\x85" }, + { 8101, "\xE1\xBD\xA5\xCE\xB9" }, + { 8549, "\xE2\x85\xB5" }, + { 65317, "\xEF\xBD\x85" }, + { 66597, "\xF0\x90\x91\x8D" }, +}, cm38[] = { + { 294, "\xC4\xA7" }, + { 358, "\xC5\xA7" }, + { 422, "\xCA\x80" }, + { 486, "\xC7\xA7" }, + { 550, "\xC8\xA7" }, + { 934, "\xCF\x86" }, + { 998, "\xCF\xA7" }, + { 1062, "\xD1\x86" }, + { 1126, "\xD1\xA7" }, + { 1190, "\xD2\xA7" }, + { 1254, "\xD3\xA7" }, + { 7718, "\xE1\xB8\xA7" }, + { 7782, "\xE1\xB9\xA7" }, + { 7846, "\xE1\xBA\xA7" }, + { 7910, "\xE1\xBB\xA7" }, + { 8102, "\xE1\xBD\xA6\xCE\xB9" }, + { 8166, "\xCF\x85\xCD\x82" }, + { 8486, "\xCF\x89" }, + { 8550, "\xE2\x85\xB6" }, + { 65318, "\xEF\xBD\x86" }, + { 66598, "\xF0\x90\x91\x8E" }, +}, cm39[] = { + { 423, "\xC6\xA8" }, + { 935, "\xCF\x87" }, + { 1063, "\xD1\x87" }, + { 8103, "\xE1\xBD\xA7\xCE\xB9" }, + { 8167, "\xCF\x85\xCC\x88\xCD\x82" }, + { 8551, "\xE2\x85\xB7" }, + { 65319, "\xEF\xBD\x87" }, + { 66599, "\xF0\x90\x91\x8F" }, +}, cm40[] = { + { 296, "\xC4\xA9" }, + { 360, "\xC5\xA9" }, + { 488, "\xC7\xA9" }, + { 552, "\xC8\xA9" }, + { 936, "\xCF\x88" }, + { 1000, "\xCF\xA9" }, + { 1064, "\xD1\x88" }, + { 1128, "\xD1\xA9" }, + { 1192, "\xD2\xA9" }, + { 1256, "\xD3\xA9" }, + { 7720, "\xE1\xB8\xA9" }, + { 7784, "\xE1\xB9\xA9" }, + { 7848, "\xE1\xBA\xA9" }, + { 7912, "\xE1\xBB\xA9" }, + { 7976, "\xE1\xBC\xA0" }, + { 8040, "\xE1\xBD\xA0" }, + { 8104, "\xE1\xBD\xA0\xCE\xB9" }, + { 8168, "\xE1\xBF\xA0" }, + { 8552, "\xE2\x85\xB8" }, + { 65320, "\xEF\xBD\x88" }, +}, cm41[] = { + { 425, "\xCA\x83" }, + { 937, "\xCF\x89" }, + { 1065, "\xD1\x89" }, + { 7977, "\xE1\xBC\xA1" }, + { 8041, "\xE1\xBD\xA1" }, + { 8105, "\xE1\xBD\xA1\xCE\xB9" }, + { 8169, "\xE1\xBF\xA1" }, + { 8553, "\xE2\x85\xB9" }, + { 65321, "\xEF\xBD\x89" }, +}, cm42[] = { + { 298, "\xC4\xAB" }, + { 362, "\xC5\xAB" }, + { 490, "\xC7\xAB" }, + { 554, "\xC8\xAB" }, + { 938, "\xCF\x8A" }, + { 1002, "\xCF\xAB" }, + { 1066, "\xD1\x8A" }, + { 1130, "\xD1\xAB" }, + { 1194, "\xD2\xAB" }, + { 1258, "\xD3\xAB" }, + { 7722, "\xE1\xB8\xAB" }, + { 7786, "\xE1\xB9\xAB" }, + { 7850, "\xE1\xBA\xAB" }, + { 7914, "\xE1\xBB\xAB" }, + { 7978, "\xE1\xBC\xA2" }, + { 8042, "\xE1\xBD\xA2" }, + { 8106, "\xE1\xBD\xA2\xCE\xB9" }, + { 8170, "\xE1\xBD\xBA" }, + { 8490, "k" }, + { 8554, "\xE2\x85\xBA" }, + { 65322, "\xEF\xBD\x8A" }, +}, cm43[] = { + { 939, "\xCF\x8B" }, + { 1067, "\xD1\x8B" }, + { 7979, "\xE1\xBC\xA3" }, + { 8043, "\xE1\xBD\xA3" }, + { 8107, "\xE1\xBD\xA3\xCE\xB9" }, + { 8171, "\xE1\xBD\xBB" }, + { 8491, "\xC3\xA5" }, + { 8555, "\xE2\x85\xBB" }, + { 65323, "\xEF\xBD\x8B" }, +}, cm44[] = { + { 300, "\xC4\xAD" }, + { 364, "\xC5\xAD" }, + { 428, "\xC6\xAD" }, + { 492, "\xC7\xAD" }, + { 556, "\xC8\xAD" }, + { 1004, "\xCF\xAD" }, + { 1068, "\xD1\x8C" }, + { 1132, "\xD1\xAD" }, + { 1196, "\xD2\xAD" }, + { 1260, "\xD3\xAD" }, + { 7724, "\xE1\xB8\xAD" }, + { 7788, "\xE1\xB9\xAD" }, + { 7852, "\xE1\xBA\xAD" }, + { 7916, "\xE1\xBB\xAD" }, + { 7980, "\xE1\xBC\xA4" }, + { 8044, "\xE1\xBD\xA4" }, + { 8108, "\xE1\xBD\xA4\xCE\xB9" }, + { 8172, "\xE1\xBF\xA5" }, + { 8556, "\xE2\x85\xBC" }, + { 65324, "\xEF\xBD\x8C" }, +}, cm45[] = { + { 1069, "\xD1\x8D" }, + { 7981, "\xE1\xBC\xA5" }, + { 8045, "\xE1\xBD\xA5" }, + { 8109, "\xE1\xBD\xA5\xCE\xB9" }, + { 8557, "\xE2\x85\xBD" }, + { 65325, "\xEF\xBD\x8D" }, +}, cm46[] = { + { 302, "\xC4\xAF" }, + { 366, "\xC5\xAF" }, + { 430, "\xCA\x88" }, + { 494, "\xC7\xAF" }, + { 558, "\xC8\xAF" }, + { 1006, "\xCF\xAF" }, + { 1070, "\xD1\x8E" }, + { 1134, "\xD1\xAF" }, + { 1198, "\xD2\xAF" }, + { 1262, "\xD3\xAF" }, + { 7726, "\xE1\xB8\xAF" }, + { 7790, "\xE1\xB9\xAF" }, + { 7854, "\xE1\xBA\xAF" }, + { 7918, "\xE1\xBB\xAF" }, + { 7982, "\xE1\xBC\xA6" }, + { 8046, "\xE1\xBD\xA6" }, + { 8110, "\xE1\xBD\xA6\xCE\xB9" }, + { 8558, "\xE2\x85\xBE" }, + { 65326, "\xEF\xBD\x8E" }, +}, cm47[] = { + { 431, "\xC6\xB0" }, + { 1071, "\xD1\x8F" }, + { 7983, "\xE1\xBC\xA7" }, + { 8047, "\xE1\xBD\xA7" }, + { 8111, "\xE1\xBD\xA7\xCE\xB9" }, + { 8559, "\xE2\x85\xBF" }, + { 65327, "\xEF\xBD\x8F" }, +}, cm48[] = { + { 304, "i\xCC\x87" }, + { 368, "\xC5\xB1" }, + { 496, "j\xCC\x8C" }, + { 560, "\xC8\xB1" }, + { 944, "\xCF\x85\xCC\x88\xCC\x81" }, + { 1008, "\xCE\xBA" }, + { 1136, "\xD1\xB1" }, + { 1200, "\xD2\xB1" }, + { 1264, "\xD3\xB1" }, + { 7728, "\xE1\xB8\xB1" }, + { 7792, "\xE1\xB9\xB1" }, + { 7856, "\xE1\xBA\xB1" }, + { 7920, "\xE1\xBB\xB1" }, + { 65328, "\xEF\xBD\x90" }, +}, cm49[] = { + { 433, "\xCA\x8A" }, + { 497, "\xC7\xB3" }, + { 1009, "\xCF\x81" }, + { 1329, "\xD5\xA1" }, + { 65329, "\xEF\xBD\x91" }, +}, cm50[] = { + { 306, "\xC4\xB3" }, + { 370, "\xC5\xB3" }, + { 434, "\xCA\x8B" }, + { 498, "\xC7\xB3" }, + { 562, "\xC8\xB3" }, + { 1138, "\xD1\xB3" }, + { 1202, "\xD2\xB3" }, + { 1266, "\xD3\xB3" }, + { 1330, "\xD5\xA2" }, + { 7730, "\xE1\xB8\xB3" }, + { 7794, "\xE1\xB9\xB3" }, + { 7858, "\xE1\xBA\xB3" }, + { 7922, "\xE1\xBB\xB3" }, + { 8114, "\xE1\xBD\xB0\xCE\xB9" }, + { 8178, "\xE1\xBD\xBC\xCE\xB9" }, + { 65330, "\xEF\xBD\x92" }, +}, cm51[] = { + { 435, "\xC6\xB4" }, + { 1331, "\xD5\xA3" }, + { 8115, "\xCE\xB1\xCE\xB9" }, + { 8179, "\xCF\x89\xCE\xB9" }, + { 65331, "\xEF\xBD\x93" }, +}, cm52[] = { + { 308, "\xC4\xB5" }, + { 372, "\xC5\xB5" }, + { 500, "\xC7\xB5" }, + { 1012, "\xCE\xB8" }, + { 1140, "\xD1\xB5" }, + { 1204, "\xD2\xB5" }, + { 1268, "\xD3\xB5" }, + { 1332, "\xD5\xA4" }, + { 7732, "\xE1\xB8\xB5" }, + { 7796, "\xE1\xB9\xB5" }, + { 7860, "\xE1\xBA\xB5" }, + { 7924, "\xE1\xBB\xB5" }, + { 8116, "\xCE\xAC\xCE\xB9" }, + { 8180, "\xCF\x8E\xCE\xB9" }, + { 65332, "\xEF\xBD\x94" }, +}, cm53[] = { + { 181, "\xCE\xBC" }, + { 437, "\xC6\xB6" }, + { 1013, "\xCE\xB5" }, + { 1333, "\xD5\xA5" }, + { 65333, "\xEF\xBD\x95" }, +}, cm54[] = { + { 310, "\xC4\xB7" }, + { 374, "\xC5\xB7" }, + { 502, "\xC6\x95" }, + { 1142, "\xD1\xB7" }, + { 1206, "\xD2\xB7" }, + { 1334, "\xD5\xA6" }, + { 7734, "\xE1\xB8\xB7" }, + { 7798, "\xE1\xB9\xB7" }, + { 7862, "\xE1\xBA\xB7" }, + { 7926, "\xE1\xBB\xB7" }, + { 8118, "\xCE\xB1\xCD\x82" }, + { 8182, "\xCF\x89\xCD\x82" }, + { 9398, "\xE2\x93\x90" }, + { 65334, "\xEF\xBD\x96" }, +}, cm55[] = { + { 439, "\xCA\x92" }, + { 503, "\xC6\xBF" }, + { 1015, "\xCF\xB8" }, + { 1335, "\xD5\xA7" }, + { 8119, "\xCE\xB1\xCD\x82\xCE\xB9" }, + { 8183, "\xCF\x89\xCD\x82\xCE\xB9" }, + { 9399, "\xE2\x93\x91" }, + { 65335, "\xEF\xBD\x97" }, +}, cm56[] = { + { 376, "\xC3\xBF" }, + { 440, "\xC6\xB9" }, + { 504, "\xC7\xB9" }, + { 1144, "\xD1\xB9" }, + { 1208, "\xD2\xB9" }, + { 1272, "\xD3\xB9" }, + { 1336, "\xD5\xA8" }, + { 7736, "\xE1\xB8\xB9" }, + { 7800, "\xE1\xB9\xB9" }, + { 7864, "\xE1\xBA\xB9" }, + { 7928, "\xE1\xBB\xB9" }, + { 7992, "\xE1\xBC\xB0" }, + { 8120, "\xE1\xBE\xB0" }, + { 8184, "\xE1\xBD\xB8" }, + { 9400, "\xE2\x93\x92" }, + { 65336, "\xEF\xBD\x98" }, +}, cm57[] = { + { 313, "\xC4\xBA" }, + { 377, "\xC5\xBA" }, + { 1017, "\xCF\xB2" }, + { 1337, "\xD5\xA9" }, + { 7993, "\xE1\xBC\xB1" }, + { 8121, "\xE1\xBE\xB1" }, + { 8185, "\xE1\xBD\xB9" }, + { 9401, "\xE2\x93\x93" }, + { 65337, "\xEF\xBD\x99" }, +}, cm58[] = { + { 506, "\xC7\xBB" }, + { 1018, "\xCF\xBB" }, + { 1146, "\xD1\xBB" }, + { 1210, "\xD2\xBB" }, + { 1338, "\xD5\xAA" }, + { 7738, "\xE1\xB8\xBB" }, + { 7802, "\xE1\xB9\xBB" }, + { 7866, "\xE1\xBA\xBB" }, + { 7994, "\xE1\xBC\xB2" }, + { 8122, "\xE1\xBD\xB0" }, + { 8186, "\xE1\xBD\xBC" }, + { 9402, "\xE2\x93\x94" }, + { 65338, "\xEF\xBD\x9A" }, +}, cm59[] = { + { 315, "\xC4\xBC" }, + { 379, "\xC5\xBC" }, + { 1339, "\xD5\xAB" }, + { 7995, "\xE1\xBC\xB3" }, + { 8123, "\xE1\xBD\xB1" }, + { 8187, "\xE1\xBD\xBD" }, + { 9403, "\xE2\x93\x95" }, +}, cm60[] = { + { 444, "\xC6\xBD" }, + { 508, "\xC7\xBD" }, + { 1148, "\xD1\xBD" }, + { 1212, "\xD2\xBD" }, + { 1340, "\xD5\xAC" }, + { 7740, "\xE1\xB8\xBD" }, + { 7804, "\xE1\xB9\xBD" }, + { 7868, "\xE1\xBA\xBD" }, + { 7996, "\xE1\xBC\xB4" }, + { 8124, "\xCE\xB1\xCE\xB9" }, + { 8188, "\xCF\x89\xCE\xB9" }, + { 9404, "\xE2\x93\x96" }, +}, cm61[] = { + { 317, "\xC4\xBE" }, + { 381, "\xC5\xBE" }, + { 1341, "\xD5\xAD" }, + { 7997, "\xE1\xBC\xB5" }, + { 9405, "\xE2\x93\x97" }, +}, cm62[] = { + { 510, "\xC7\xBF" }, + { 1150, "\xD1\xBF" }, + { 1214, "\xD2\xBF" }, + { 1342, "\xD5\xAE" }, + { 7742, "\xE1\xB8\xBF" }, + { 7806, "\xE1\xB9\xBF" }, + { 7870, "\xE1\xBA\xBF" }, + { 7998, "\xE1\xBC\xB6" }, + { 8126, "\xCE\xB9" }, + { 9406, "\xE2\x93\x98" }, +}, cm63[] = { + { 319, "\xC5\x80" }, + { 383, "s" }, + { 1343, "\xD5\xAF" }, + { 7999, "\xE1\xBC\xB7" }, + { 9407, "\xE2\x93\x99" }, +}; + +static const struct cm *const cm[] = { cm0, cm1, cm2, cm3, cm4, cm5, cm6, cm7, cm8, cm9, cm10, cm11, cm12, cm13, cm14, cm15, cm16, cm17, cm18, cm19, cm20, cm21, cm22, cm23, cm24, cm25, cm26, cm27, cm28, cm29, cm30, cm31, cm32, cm33, cm34, cm35, cm36, cm37, cm38, cm39, cm40, cm41, cm42, cm43, cm44, cm45, cm46, cm47, cm48, cm49, cm50, cm51, cm52, cm53, cm54, cm55, cm56, cm57, cm58, cm59, cm60, cm61, cm62, cm63 }; +static const size_t cmn[] = { 15, 11, 18, 11, 18, 12, 18, 13, 19, 14, 22, 13, 20, 11, 19, 11, 19, 10, 18, 11, 19, 10, 21, 10, 19, 11, 18, 10, 16, 8, 15, 7, 18, 6, 19, 7, 20, 6, 21, 8, 20, 9, 21, 9, 20, 6, 19, 7, 14, 5, 16, 5, 15, 5, 14, 8, 16, 9, 13, 7, 12, 5, 10, 5 }; +#define CM_MASK 63 +/* arch-tag:2dc53cdcaba8a55c2982ad113f4ebae2 */ diff --git a/lib/charset.c b/lib/charset.c new file mode 100644 index 0000000..e8f577a --- /dev/null +++ b/lib/charset.c @@ -0,0 +1,146 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <iconv.h> +#include <string.h> +#include <errno.h> +#include <langinfo.h> + +#include "mem.h" +#include "log.h" +#include "charset.h" +#include "configuration.h" +#include "utf8.h" +#include "vector.h" + +static void *convert(const char *from, const char *to, + const void *ptr, size_t n) { + iconv_t i; + size_t len; + char *buf = 0, *s, *d; + size_t bufsize = 0, sl, dl; + + if((i = iconv_open(to, from)) == (iconv_t)-1) + fatal(errno, "error calling iconv_open"); + do { + bufsize = bufsize ? 2 * bufsize : 32; + buf = xrealloc_noptr(buf, bufsize); + iconv(i, 0, 0, 0, 0); + s = (char *)ptr; + sl = n; + d = buf; + dl = bufsize; + /* (void *) to work around FreeBSD's nonstandard iconv prototype */ + len = iconv(i, (void *)&s, &sl, &d, &dl); + } while(len == (size_t)-1 && errno == E2BIG); + iconv_close(i); + if(len == (size_t)-1) { + error(errno, "error converting from %s to %s", from, to); + return 0; + } + return buf; +} + +/* not everybody's iconv supports UCS-4, and it's inconvenient to have to know + * our endianness, and it's easy to convert it ourselves, so we do */ +uint32_t *utf82ucs4(const char *mb) { + struct dynstr_ucs4 d; + uint32_t c; + + dynstr_ucs4_init(&d); + while(*mb) { + PARSE_UTF8(mb, c, + error(0, "invalid UTF-8 sequence"); return 0;); + dynstr_ucs4_append(&d, c); + } + dynstr_ucs4_terminate(&d); + return d.vec; +} + +char *ucs42utf8(const uint32_t *u) { + struct dynstr d; + uint32_t c; + + dynstr_init(&d); + while((c = *u++)) { + if(c < 0x80) + dynstr_append(&d, c); + else if(c < 0x800) { + dynstr_append(&d, 0xC0 | (c >> 6)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else if(c < 0x10000) { + dynstr_append(&d, 0xE0 | (c >> 12)); + dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else if(c < 0x110000) { + dynstr_append(&d, 0xF0 | (c >> 18)); + dynstr_append(&d, 0x80 | ((c >> 12) & 0x3F)); + dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F)); + dynstr_append(&d, 0x80 | (c & 0x3F)); + } else { + error(0, "invalid UCS-4 character"); + return 0; + } + } + dynstr_terminate(&d); + return d.vec; +} + +char *mb2utf8(const char *mb) { + return convert(nl_langinfo(CODESET), "UTF-8", mb, strlen(mb) + 1); +} + +char *utf82mb(const char *utf8) { + return convert("UTF-8", nl_langinfo(CODESET), utf8, strlen(utf8) + 1); +} + +char *any2utf8(const char *from, const char *any) { + return convert(from, "UTF-8", any, strlen(any) + 1); +} + +char *any2mb(const char *from, const char *any) { + if(from) return convert(from, nl_langinfo(CODESET), any, strlen(any) + 1); + else return xstrdup(any); +} + +char *any2any(const char *from, + const char *to, + const char *any) { + if(from || to) return convert(from, to, any, strlen(any) + 1); + else return xstrdup(any); +} + +int ucs4cmp(const uint32_t *a, const uint32_t *b) { + while(*a && *b && *a == *b) ++a, ++b; + if(*a > *b) return 1; + else if(*a < *b) return -1; + else return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:30ec6c45260bef9d03ef04d194bf9e9e */ diff --git a/lib/charset.h b/lib/charset.h new file mode 100644 index 0000000..3dcd756 --- /dev/null +++ b/lib/charset.h @@ -0,0 +1,68 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef CHARSET_H +#define CHARSET_H + +/* Character encoding conversion routines */ + +uint32_t *utf82ucs4(const char *mb); +char *ucs42utf8(const uint32_t *u); +char *mb2utf8(const char *mb); +char *utf82mb(const char *utf8); +/* various conversions, between multibyte strings (mb) in + * whatever the current encoding is, and UTF-8 strings (utf8). On + * error, a null pointer is returned and @errno@ set. */ + +char *any2utf8(const char *from/*encoding*/, + const char *any/*string*/); +/* arbitrary conversions from any null-free byte-based encoding that + * iconv knows about to UTF-8 */ + +char *any2mb(const char *from/*encoding or 0*/, + const char *any/*string*/); +/* Arbitrary conversions from any null-free byte-based encoding that + * iconv knows about to a multibyte string. If FROM is 0 then ANY is + * returned unchanged. */ + +char *any2any(const char *from/*encoding or 0*/, + const char *to/*encoding to 0*/, + const char *any/*string*/); +/* Arbitrary conversions between any null-free byte-based encodings + * that iconv knows. If FROM and TO are both 0 then ANY is returned + * unchanged. */ + + +static inline char *nullcheck(char *s) { + if(!s) exitfn(1); /* assume an error already reported */ + return s; +} + +int ucs4cmp(const uint32_t *a, const uint32_t *b); +/* like strcmp */ + +#endif /* CHARSET_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:ca7783e592109d7b7078175bd301faf7 */ diff --git a/lib/client-common.c b/lib/client-common.c new file mode 100644 index 0000000..1e8fbac --- /dev/null +++ b/lib/client-common.c @@ -0,0 +1,86 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <errno.h> +#include <netdb.h> + +#include "log.h" +#include "configuration.h" +#include "client-common.h" +#include "addr.h" + +int with_sockaddr(void *c, + int (*function)(void *c, + const struct sockaddr *sa, + socklen_t len, + const char *ident)) { + const char *path; + struct sockaddr_un su; + struct addrinfo *res; + char *name; + int n; + + static const struct addrinfo pref = { + 0, + PF_INET, + SOCK_STREAM, + IPPROTO_TCP, + 0, + 0, + 0, + 0 + }; + + if(config->connect.n) { + res = get_address(&config->connect, &pref, &name); + if(!res) return -1; + n = function(c, res->ai_addr, res->ai_addrlen, name); + freeaddrinfo(res); + return n; + } else { + path = config_get_file("socket"); + if(strlen(path) >= sizeof su.sun_path) { + error(errno, "socket path is too long"); + return -1; + } + memset(&su, 0, sizeof su); + su.sun_family = AF_UNIX; + strcpy(su.sun_path, path); + return function(c, (struct sockaddr *)&su, sizeof su, path); + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:NhE5Xyy+Tzv6rhKtZ1jNlg */ diff --git a/lib/client-common.h b/lib/client-common.h new file mode 100644 index 0000000..0387eb8 --- /dev/null +++ b/lib/client-common.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CLIENT_COMMON_H +#define CLIENT_COMMON_H + +int with_sockaddr(void *c, + int (*function)(void *c, + const struct sockaddr *sa, + socklen_t len, + const char *ident)); + +#endif /* CLIENT_COMMON_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:S5yQBwyuy9z8e9CFf74pgA */ diff --git a/lib/client.c b/lib/client.c new file mode 100644 index 0000000..ddf88bb --- /dev/null +++ b/lib/client.c @@ -0,0 +1,613 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <netdb.h> +#include <stdlib.h> + +#include "log.h" +#include "mem.h" +#include "configuration.h" +#include "queue.h" +#include "client.h" +#include "charset.h" +#include "hex.h" +#include "split.h" +#include "vector.h" +#include "inputline.h" +#include "kvp.h" +#include "syscalls.h" +#include "printf.h" +#include "sink.h" +#include "addr.h" +#include "authhash.h" +#include "client-common.h" + +struct disorder_client { + FILE *fpin, *fpout; + char *ident; + char *user; + int verbose; +}; + +disorder_client *disorder_new(int verbose) { + disorder_client *c = xmalloc(sizeof (struct disorder_client)); + + c->verbose = verbose; + return c; +} + +/* read a response line. + * If @rp@ is not a null pointer, returns the whole response through it. + * Return value is the response code, or -1 on error. */ +static int response(disorder_client *c, char **rp) { + char *r; + + if(inputline(c->ident, c->fpin, &r, '\n')) + return -1; + D(("response: %s", r)); + if(rp) + *rp = r; + if(r[0] >= '0' && r[0] <= '9' + && r[1] >= '0' && r[1] <= '9' + && r[2] >= '0' && r[2] <= '9' + && r[3] == ' ') + return (r[0] * 10 + r[1]) * 10 + r[2] - 111 * '0'; + else { + error(0, "invalid reply format from %s", c->ident); + return -1; + } +} + +/* Read a response. + * If @rp@ is not a null pointer then the response text (excluding + * the status code) is returned through it, UNLESS the response code + * is xx9. + * Return value is 0 for 2xx responses and -1 otherwise. + */ +static int check_response(disorder_client *c, char **rp) { + int rc; + char *r; + + if((rc = response(c, &r)) == -1) + return -1; + else if(rc / 100 == 2) { + if(rp) + *rp = (rc % 10 == 9) ? 0 : xstrdup(r + 4); + return 0; + } else { + if(c->verbose) + error(0, "from %s: %s", c->ident, utf82mb(r)); + return -1; + } +} + +static int disorder_simple_v(disorder_client *c, + char **rp, + const char *cmd, va_list ap) { + const char *arg; + struct dynstr d; + + if(cmd) { + dynstr_init(&d); + dynstr_append_string(&d, cmd); + while((arg = va_arg(ap, const char *))) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(arg)); + } + dynstr_append(&d, '\n'); + dynstr_terminate(&d); + D(("command: %s", d.vec)); + if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) { + error(errno, "error writing to %s", c->ident); + return -1; + } + } + return check_response(c, rp); +} + +/* Execute a simple command with any number of arguments. + * @rp@ and return value as for check_response(). + */ +static int disorder_simple(disorder_client *c, + char **rp, + const char *cmd, ...) { + va_list ap; + int ret; + + va_start(ap, cmd); + ret = disorder_simple_v(c, rp, cmd, ap); + va_end(ap); + return ret; +} + +static int connect_sock(void *vc, + const struct sockaddr *sa, + socklen_t len, + const char *ident) { + const char *username, *password; + disorder_client *c = vc; + int n; + + if(!(username = config->username)) { + error(0, "no username configured"); + return -1; + } + if(!(password = config->password)) { + for(n = 0; (n < config->allow.n + && strcmp(config->allow.s[n].s[0], username)); ++n) + ; + if(n < config->allow.n) + password = config->allow.s[n].s[1]; + else { + error(0, "no password configured"); + return -1; + } + } + return disorder_connect_sock(c, sa, len, username, password, ident); +} + +int disorder_connect(disorder_client *c) { + return with_sockaddr(c, connect_sock); +} + +static int check_running(void attribute((unused)) *c, + const struct sockaddr *sa, + socklen_t len, + const char attribute((unused)) *ident) { + int fd, ret; + + if((fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) + fatal(errno, "error calling socket"); + if(connect(fd, sa, len) < 0) { + if(errno == ECONNREFUSED || errno == ENOENT) + ret = 0; + else + fatal(errno, "error calling connect"); + } else + ret = 1; + xclose(fd); + return ret; +} + +int disorder_running(disorder_client *c) { + return with_sockaddr(c, check_running); +} + +int disorder_connect_sock(disorder_client *c, + const struct sockaddr *sa, + socklen_t len, + const char *username, + const char *password, + const char *ident) { + int fd = -1, fd2 = -1; + unsigned char *nonce; + size_t nl; + const char *res; + char *r; + + if(!password) { + error(0, "no password found"); + return -1; + } + c->fpin = c->fpout = 0; + if((fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { + error(errno, "error calling socket"); + return -1; + } + if(connect(fd, sa, len) < 0) { + error(errno, "error calling connect"); + goto error; + } + if((fd2 = dup(fd)) < 0) { + error(errno, "error calling dup"); + goto error; + } + if(!(c->fpin = fdopen(fd, "rb"))) { + error(errno, "error calling fdopen"); + goto error; + } + fd = -1; + if(!(c->fpout = fdopen(fd2, "wb"))) { + error(errno, "error calling fdopen"); + goto error; + } + fd2 = -1; + c->ident = xstrdup(ident); + if(disorder_simple(c, &r, 0, (const char *)0)) + return -1; + if(!(nonce = unhex(r, &nl))) + return -1; + if(!(res = authhash(nonce, nl, password))) goto error; + if(disorder_simple(c, 0, "user", username, res, (char *)0)) + return -1; + c->user = xstrdup(username); + return 0; +error: + if(c->fpin) fclose(c->fpin); + if(c->fpout) fclose(c->fpout); + if(fd2 != -1) close(fd2); + if(fd != -1) close(fd); + return -1; +} + +int disorder_close(disorder_client *c) { + int ret = 0; + + if(c->fpin) { + if(fclose(c->fpin) < 0) { + error(errno, "error calling fclose"); + ret = -1; + } + c->fpin = 0; + } + if(c->fpout) { + if(fclose(c->fpout) < 0) { + error(errno, "error calling fclose"); + ret = -1; + } + c->fpout = 0; + } + return 0; +} + +int disorder_become(disorder_client *c, const char *user) { + if(disorder_simple(c, 0, "become", user, (char *)0)) return -1; + c->user = xstrdup(user); + return 0; +} + +int disorder_play(disorder_client *c, const char *track) { + return disorder_simple(c, 0, "play", track, (char *)0); +} + +int disorder_remove(disorder_client *c, const char *track) { + return disorder_simple(c, 0, "remove", track, (char *)0); +} + +int disorder_move(disorder_client *c, const char *track, int delta) { + char d[16]; + + byte_snprintf(d, sizeof d, "%d", delta); + return disorder_simple(c, 0, "move", track, d, (char *)0); +} + +int disorder_enable(disorder_client *c) { + return disorder_simple(c, 0, "enable", (char *)0); +} + +int disorder_disable(disorder_client *c) { + return disorder_simple(c, 0, "disable", (char *)0); +} + +int disorder_scratch(disorder_client *c, const char *id) { + return disorder_simple(c, 0, "scratch", id, (char *)0); +} + +int disorder_shutdown(disorder_client *c) { + return disorder_simple(c, 0, "shutdown", (char *)0); +} + +int disorder_reconfigure(disorder_client *c) { + return disorder_simple(c, 0, "reconfigure", (char *)0); +} + +int disorder_rescan(disorder_client *c) { + return disorder_simple(c, 0, "rescan", (char *)0); +} + +int disorder_version(disorder_client *c, char **rp) { + return disorder_simple(c, rp, "version", (char *)0); +} + +static void client_error(const char *msg, + void attribute((unused)) *u) { + error(0, "error parsing reply: %s", msg); +} + +int disorder_playing(disorder_client *c, struct queue_entry **qp) { + char *r; + struct queue_entry *q; + + if(disorder_simple(c, &r, "playing", (char *)0)) + return -1; + if(r) { + q = xmalloc(sizeof *q); + if(queue_unmarshall(q, r, client_error, 0)) + return -1; + *qp = q; + } else + *qp = 0; + return 0; +} + +static int disorder_somequeue(disorder_client *c, + const char *cmd, struct queue_entry **qp) { + struct queue_entry *qh, **qt = &qh, *q; + char *l; + + if(disorder_simple(c, 0, cmd, (char *)0)) + return -1; + while(inputline(c->ident, c->fpin, &l, '\n') >= 0) { + if(!strcmp(l, ".")) { + *qt = 0; + *qp = qh; + return 0; + } + q = xmalloc(sizeof *q); + if(!queue_unmarshall(q, l, client_error, 0)) { + *qt = q; + qt = &q->next; + } + } + if(ferror(c->fpin)) + error(errno, "error reading %s", c->ident); + else + error(0, "error reading %s: unexpected EOF", c->ident); + return -1; +} + +int disorder_recent(disorder_client *c, struct queue_entry **qp) { + return disorder_somequeue(c, "recent", qp); +} + +int disorder_queue(disorder_client *c, struct queue_entry **qp) { + return disorder_somequeue(c, "queue", qp); +} + +static int readlist(disorder_client *c, char ***vecp, int *nvecp) { + char *l; + struct vector v; + + vector_init(&v); + while(inputline(c->ident, c->fpin, &l, '\n') >= 0) { + if(!strcmp(l, ".")) { + vector_terminate(&v); + if(nvecp) + *nvecp = v.nvec; + *vecp = v.vec; + return 0; + } + vector_append(&v, l + (*l == '.')); + } + if(ferror(c->fpin)) + error(errno, "error reading %s", c->ident); + else + error(0, "error reading %s: unexpected EOF", c->ident); + return -1; +} + +static int disorder_simple_list(disorder_client *c, + char ***vecp, int *nvecp, + const char *cmd, ...) { + va_list ap; + int ret; + + va_start(ap, cmd); + ret = disorder_simple_v(c, 0, cmd, ap); + va_end(ap); + if(ret) return ret; + return readlist(c, vecp, nvecp); +} + +int disorder_directories(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "dirs", dir, re, (char *)0); +} + +int disorder_files(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "files", dir, re, (char *)0); +} + +int disorder_allfiles(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "allfiles", dir, re, (char *)0); +} + +char *disorder_user(disorder_client *c) { + return c->user; +} + +int disorder_set(disorder_client *c, const char *track, + const char *key, const char *value) { + return disorder_simple(c, 0, "set", track, key, value, (char *)0); +} + +int disorder_unset(disorder_client *c, const char *track, + const char *key) { + return disorder_simple(c, 0, "unset", track, key, (char *)0); +} + +int disorder_get(disorder_client *c, + const char *track, const char *key, char **valuep) { + return disorder_simple(c, valuep, "get", track, key, (char *)0); +} + +static void pref_error_handler(const char *msg, + void attribute((unused)) *u) { + error(0, "error handling 'prefs' reply: %s", msg); +} + +int disorder_prefs(disorder_client *c, const char *track, struct kvp **kp) { + char **vec, **pvec; + int nvec, npvec, n; + struct kvp *k; + + if(disorder_simple_list(c, &vec, &nvec, "prefs", track, (char *)0)) + return -1; + for(n = 0; n < nvec; ++n) { + if(!(pvec = split(vec[n], &npvec, SPLIT_QUOTES, pref_error_handler, 0))) + return -1; + if(npvec != 2) { + pref_error_handler("malformed response", 0); + return -1; + } + *kp = k = xmalloc(sizeof *k); + k->name = pvec[0]; + k->value = pvec[1]; + kp = &k->next; + } + *kp = 0; + return 0; +} + +static int boolean(const char *cmd, const char *value, + int *flagp) { + if(!strcmp(value, "yes")) *flagp = 1; + else if(!strcmp(value, "no")) *flagp = 0; + else { + error(0, "malformed response to '%s'", cmd); + return -1; + } + return 0; +} + +int disorder_exists(disorder_client *c, const char *track, int *existsp) { + char *v; + + if(disorder_simple(c, &v, "exists", track, (char *)0)) return -1; + return boolean("exists", v, existsp); +} + +int disorder_enabled(disorder_client *c, int *enabledp) { + char *v; + + if(disorder_simple(c, &v, "enabled", (char *)0)) return -1; + return boolean("enabled", v, enabledp); +} + +int disorder_length(disorder_client *c, const char *track, + long *valuep) { + char *value; + + if(disorder_simple(c, &value, "length", track, (char *)0)) return -1; + *valuep = atol(value); + return 0; +} + +int disorder_search(disorder_client *c, const char *terms, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "search", terms, (char *)0); +} + +int disorder_random_enable(disorder_client *c) { + return disorder_simple(c, 0, "random-enable", (char *)0); +} + +int disorder_random_disable(disorder_client *c) { + return disorder_simple(c, 0, "random-disable", (char *)0); +} + +int disorder_random_enabled(disorder_client *c, int *enabledp) { + char *v; + + if(disorder_simple(c, &v, "random-enabled", (char *)0)) return -1; + return boolean("random-enabled", v, enabledp); +} + +int disorder_stats(disorder_client *c, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "stats", (char *)0); +} + +int disorder_set_volume(disorder_client *c, int left, int right) { + char *ls, *rs; + + if(byte_asprintf(&ls, "%d", left) < 0 + || byte_asprintf(&rs, "%d", right) < 0) + return -1; + if(disorder_simple(c, 0, "volume", ls, rs, (char *)0)) return -1; + return 0; +} + +int disorder_get_volume(disorder_client *c, int *left, int *right) { + char *r; + + if(disorder_simple(c, &r, "volume", (char *)0)) return -1; + if(sscanf(r, "%d %d", left, right) != 2) { + error(0, "error parsing response to 'volume': '%s'", r); + return -1; + } + return 0; +} + +int disorder_log(disorder_client *c, struct sink *s) { + char *l; + + if(disorder_simple(c, 0, "log", (char *)0)) return -1; + while(inputline(c->ident, c->fpin, &l, '\n') >= 0 && strcmp(l, ".")) + if(sink_printf(s, "%s\n", l) < 0) return -1; + if(ferror(c->fpin) || feof(c->fpin)) return -1; + return 0; +} + +int disorder_part(disorder_client *c, char **partp, + const char *track, const char *context, const char *part) { + return disorder_simple(c, partp, "part", track, context, part, (char *)0); +} + +int disorder_resolve(disorder_client *c, char **trackp, const char *track) { + return disorder_simple(c, trackp, "resolve", track, (char *)0); +} + +int disorder_pause(disorder_client *c) { + return disorder_simple(c, 0, "pause", (char *)0); +} + +int disorder_resume(disorder_client *c) { + return disorder_simple(c, 0, "resume", (char *)0); +} + +int disorder_tags(disorder_client *c, + char ***vecp, int *nvecp) { + return disorder_simple_list(c, vecp, nvecp, "tags", (char *)0); +} + +int disorder_set_global(disorder_client *c, + const char *key, const char *value) { + return disorder_simple(c, 0, "set-global", key, value, (char *)0); +} + +int disorder_unset_global(disorder_client *c, const char *key) { + return disorder_simple(c, 0, "unset-global", key, (char *)0); +} + +int disorder_get_global(disorder_client *c, const char *key, char **valuep) { + return disorder_simple(c, valuep, "get-global", key, (char *)0); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3937adbfa9480384606631d8e0365885 */ diff --git a/lib/client.h b/lib/client.h new file mode 100644 index 0000000..b3c18a5 --- /dev/null +++ b/lib/client.h @@ -0,0 +1,192 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CLIENT_H +#define CLIENT_H + +/* A simple synchronous client interface. */ + +typedef struct disorder_client disorder_client; + +struct queue_entry; +struct kvp; +struct sink; + +/* Parameter strings (e.g. @track@) are UTF-8 unless specified + * otherwise. */ + +disorder_client *disorder_new(int verbose); +/* create a new disorder_client */ + +int disorder_running(disorder_client *c); +/* return 1 if the server is running, else 0 */ + +int disorder_connect(disorder_client *c); +/* connect a disorder_client using the default settings */ + +int disorder_connect_sock(disorder_client *c, + const struct sockaddr *sa, + socklen_t len, + const char *username, + const char *password, + const char *ident); +/* connect a disorder_client */ + +int disorder_close(disorder_client *c); +/* close a disorder_client */ + +int disorder_become(disorder_client *c, const char *user); +/* become another user (trusted users only) */ + +int disorder_version(disorder_client *c, char **versionp); +/* get the server version */ + +int disorder_play(disorder_client *c, const char *track); +/* add a track to the queue */ + +int disorder_remove(disorder_client *c, const char *track); +/* remove a track from the queue */ + +int disorder_move(disorder_client *c, const char *track, int delta); +/* move a track in the queue @delta@ steps towards the head */ + +int disorder_enable(disorder_client *c); +/* enable playing if it is not already enabled */ + +int disorder_disable(disorder_client *c); +/* disable playing if it is not already disabled. */ + +int disorder_scratch(disorder_client *c, const char *id); +/* scratch the currently playing track. If @id@ is not a null pointer + * then the scratch will be ignored if the ID does not mactch. */ + +int disorder_shutdown(disorder_client *c); +/* shut down the server immediately */ + +int disorder_reconfigure(disorder_client *c); +/* re-read the configuration file */ + +int disorder_rescan(disorder_client *c); +/* initiate a rescan */ + +int disorder_playing(disorder_client *c, struct queue_entry **qp); +/* get the details of the currently playing track (null pointer if + * nothing playing). The first entry in the list is the next track to + * be played. */ + +int disorder_recent(disorder_client *c, struct queue_entry **qp); +/* get a list of recently played tracks. The LAST entry in the list + * is last track to have been played. */ + +int disorder_queue(disorder_client *c, struct queue_entry **qp); +/* get the queue */ + +int disorder_directories(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get subdirectories of @dir@, or of the root if @dir@ is an null + * pointer */ + +int disorder_files(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get list of files in @dir@ */ + +int disorder_allfiles(disorder_client *c, const char *dir, const char *re, + char ***vecp, int *nvecp); +/* get list of files and directories in @dir@ */ + +char *disorder_user(disorder_client *c); +/* remind ourselves what user we went in as */ + +int disorder_exists(disorder_client *c, const char *track, int *existsp); +/* set @*existsp@ to 1 if the track exists, else 0 */ + +int disorder_enabled(disorder_client *c, int *enabledp); +/* set @*enabledp@ to 1 if playing enabled, else 0 */ + +int disorder_set(disorder_client *c, const char *track, + const char *key, const char *value); +int disorder_unset(disorder_client *c, const char *track, + const char *key); +int disorder_get(disorder_client *c, const char *track, const char *key, + char **valuep); +int disorder_prefs(disorder_client *c, const char *track, + struct kvp **kp); +/* set, unset, get, list preferences */ + +int disorder_length(disorder_client *c, const char *track, + long *valuep); +/* get the length of a track in seconds, if it is known */ + +int disorder_search(disorder_client *c, const char *terms, + char ***vecp, int *nvecp); +/* get a list of tracks matching @words@ */ + +int disorder_random_enable(disorder_client *c); +/* enable random play if it is not already enabled */ + +int disorder_random_disable(disorder_client *c); +/* disable random play if it is not already disabled */ + +int disorder_random_enabled(disorder_client *c, int *enabledp); +/* determine whether random play is enabled */ + +int disorder_stats(disorder_client *c, + char ***vecp, int *nvecp); +/* get server statistics */ + +int disorder_set_volume(disorder_client *c, int left, int right); +int disorder_get_volume(disorder_client *c, int *left, int *right); +/* get or set the volume */ + +int disorder_log(disorder_client *c, struct sink *s); +/* send log output to @s@ */ + +int disorder_part(disorder_client *c, char **partp, + const char *track, const char *context, const char *part); +/* get a track name part */ + +int disorder_resolve(disorder_client *c, char **trackp, const char *track); +/* resolve a track name */ + +int disorder_pause(disorder_client *c); +/* Pause the currently playing track. */ + +int disorder_resume(disorder_client *c); +/* Resume after a pause. */ + +int disorder_tags(disorder_client *c, + char ***vecp, int *nvecp); +/* get known tags */ + +int disorder_set_global(disorder_client *c, + const char *key, const char *value); +int disorder_unset_global(disorder_client *c, const char *key); +int disorder_get_global(disorder_client *c, const char *key, char **valuep); +/* get/unset/set global prefs */ + +#endif /* CLIENT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:9707959522fafb36d7bccfd1b154fd84 */ diff --git a/lib/configuration.c b/lib/configuration.c new file mode 100644 index 0000000..164f374 --- /dev/null +++ b/lib/configuration.c @@ -0,0 +1,934 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <ctype.h> +#include <stddef.h> +#include <pwd.h> +#include <langinfo.h> +#include <pcre.h> +#include <signal.h> + +#include "configuration.h" +#include "mem.h" +#include "log.h" +#include "split.h" +#include "syscalls.h" +#include "table.h" +#include "inputline.h" +#include "charset.h" +#include "defs.h" +#include "mixer.h" +#include "printf.h" +#include "regsub.h" +#include "signame.h" + +char *configfile; + +struct config_state { + const char *path; + int line; + struct config *config; +}; + +struct config *config; + +struct conf { + const char *name; + size_t offset; + const struct conftype *type; + int (*validate)(const struct config_state *cs, + int nvec, char **vec); +}; + +struct conftype { + int (*set)(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec); + void (*free)(struct config *c, const struct conf *whoami); +}; + +#define ADDRESS(C, TYPE) ((TYPE *)((char *)(C) + whoami->offset)) +#define VALUE(C, TYPE) (*ADDRESS(C, TYPE)) + +static int set_signal(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + + if(nvec != 1) { + error(0, "%s:%d: '%s' requires one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if((n = find_signal(vec[0])) == -1) { + error(0, "%s:%d: unknown signal '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + VALUE(cs->config, int) = n; + return 0; +} + +static int set_collections(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct collectionlist *cl; + + if(nvec != 3) { + error(0, "%s:%d: '%s' requires three arguments", + cs->path, cs->line, whoami->name); + return -1; + } + if(vec[2][0] != '/') { + error(0, "%s:%d: collection root must start with '/'", + cs->path, cs->line); + return -1; + } + if(vec[2][1] && vec[2][strlen(vec[2])-1] == '/') { + error(0, "%s:%d: collection root must not end with '/'", + cs->path, cs->line); + return -1; + } + cl = ADDRESS(cs->config, struct collectionlist); + ++cl->n; + cl->s = xrealloc(cl->s, cl->n * sizeof (struct collection)); + cl->s[cl->n - 1].module = xstrdup(vec[0]); + cl->s[cl->n - 1].encoding = xstrdup(vec[1]); + cl->s[cl->n - 1].root = xstrdup(vec[2]); + return 0; +} + +static int set_boolean(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int state; + + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if(!strcmp(vec[0], "yes")) state = 1; + else if(!strcmp(vec[0], "no")) state = 0; + else { + error(0, "%s:%d: argument to '%s' must be 'yes' or 'no'", + cs->path, cs->line, whoami->name); + return -1; + } + VALUE(cs->config, int) = state; + return 0; +} + +static int set_string(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + VALUE(cs->config, char *) = xstrdup(vec[0]); + return 0; +} + +static int set_stringlist(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *sl; + + sl = ADDRESS(cs->config, struct stringlist); + sl->n = 0; + for(n = 0; n < nvec; ++n) { + sl->n++; + sl->s = xrealloc(sl->s, (sl->n * sizeof (char *))); + sl->s[sl->n - 1] = xstrdup(vec[n]); + } + return 0; +} + +static int set_integer(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + char *e; + + if(nvec != 1) { + error(0, "%s:%d: '%s' takes only one argument", + cs->path, cs->line, whoami->name); + return -1; + } + if(xstrtol(ADDRESS(cs->config, long), vec[0], &e, 0)) { + error(errno, "%s:%d: converting integer", cs->path, cs->line); + return -1; + } + if(*e) { + error(0, "%s:%d: invalid integer syntax", cs->path, cs->line); + return -1; + } + return 0; +} + +static int set_stringlist_accum(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *s; + struct stringlistlist *sll; + + sll = ADDRESS(cs->config, struct stringlistlist); + sll->n++; + sll->s = xrealloc(sll->s, (sll->n * sizeof (struct stringlist))); + s = &sll->s[sll->n - 1]; + s->n = nvec; + s->s = xmalloc((nvec + 1) * sizeof (char *)); + for(n = 0; n < nvec; ++n) + s->s[n] = xstrdup(vec[n]); + return 0; +} + +static int set_string_accum(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + int n; + struct stringlist *sl; + + sl = ADDRESS(cs->config, struct stringlist); + for(n = 0; n < nvec; ++n) { + sl->n++; + sl->s = xrealloc(sl->s, (sl->n * sizeof (char *))); + sl->s[sl->n - 1] = xstrdup(vec[n]); + } + return 0; +} + +static int set_restrict(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + unsigned r = 0; + int n, i; + + static const struct restriction { + const char *name; + unsigned bit; + } restrictions[] = { + { "remove", RESTRICT_REMOVE }, + { "scratch", RESTRICT_SCRATCH }, + { "move", RESTRICT_MOVE }, + }; + + for(n = 0; n < nvec; ++n) { + if((i = TABLE_FIND(restrictions, struct restriction, name, vec[n])) < 0) { + error(0, "%s:%d: invalid restriction '%s'", + cs->path, cs->line, vec[n]); + return -1; + } + r |= restrictions[i].bit; + } + VALUE(cs->config, unsigned) = r; + return 0; +} + +static int set_namepart(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct namepartlist *npl = ADDRESS(cs->config, struct namepartlist); + unsigned reflags; + const char *errstr; + int erroffset, n; + pcre *re; + + if(nvec < 3) { + error(0, "%s:%d: namepart needs at least 3 arguments", cs->path, cs->line); + return -1; + } + if(nvec > 5) { + error(0, "%s:%d: namepart needs at most 5 arguments", cs->path, cs->line); + return -1; + } + reflags = nvec >= 5 ? regsub_flags(vec[4]) : 0; + if(!(re = pcre_compile(vec[1], + PCRE_UTF8 + |regsub_compile_options(reflags), + &errstr, &erroffset, 0))) { + error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)", + cs->path, cs->line, vec[1], errstr, erroffset); + return -1; + } + npl->s = xrealloc(npl->s, (npl->n + 1) * sizeof (struct namepart)); + npl->s[npl->n].part = xstrdup(vec[0]); + npl->s[npl->n].re = re; + npl->s[npl->n].replace = xstrdup(vec[2]); + npl->s[npl->n].context = xstrdup(vec[3]); + npl->s[npl->n].reflags = reflags; + ++npl->n; + /* XXX a bit of a bodge; relies on there being very few parts. */ + for(n = 0; (n < cs->config->nparts + && strcmp(cs->config->parts[n], vec[0])); ++n) + ; + if(n >= cs->config->nparts) { + cs->config->parts = xrealloc(cs->config->parts, + (cs->config->nparts + 1) * sizeof (char *)); + cs->config->parts[cs->config->nparts++] = xstrdup(vec[0]); + } + return 0; +} + +static int set_transform(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct transformlist *tl = ADDRESS(cs->config, struct transformlist); + pcre *re; + unsigned reflags; + const char *errstr; + int erroffset; + + if(nvec < 3) { + error(0, "%s:%d: transform needs at least 3 arguments", cs->path, cs->line); + return -1; + } + if(nvec > 5) { + error(0, "%s:%d: transform needs at most 5 arguments", cs->path, cs->line); + return -1; + } + reflags = (nvec >= 5 ? regsub_flags(vec[4]) : 0); + if(!(re = pcre_compile(vec[1], + PCRE_UTF8 + |regsub_compile_options(reflags), + &errstr, &erroffset, 0))) { + error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)", + cs->path, cs->line, vec[1], errstr, erroffset); + return -1; + } + tl->t = xrealloc(tl->t, (tl->n + 1) * sizeof (struct namepart)); + tl->t[tl->n].type = xstrdup(vec[0]); + tl->t[tl->n].context = xstrdup(vec[3] ? vec[3] : "*"); + tl->t[tl->n].re = re; + tl->t[tl->n].replace = xstrdup(vec[2]); + tl->t[tl->n].flags = reflags; + ++tl->n; + return 0; +} + +/* free functions */ + +static void free_none(struct config attribute((unused)) *c, + const struct conf attribute((unused)) *whoami) { +} + +static void free_string(struct config *c, + const struct conf *whoami) { + xfree(VALUE(c, char *)); +} + +static void free_stringlist(struct config *c, + const struct conf *whoami) { + int n; + struct stringlist *sl = ADDRESS(c, struct stringlist); + + for(n = 0; n < sl->n; ++n) + xfree(sl->s[n]); + xfree(sl->s); +} + +static void free_stringlistlist(struct config *c, + const struct conf *whoami) { + int n, m; + struct stringlistlist *sll = ADDRESS(c, struct stringlistlist); + struct stringlist *sl; + + for(n = 0; n < sll->n; ++n) { + sl = &sll->s[n]; + for(m = 0; m < sl->n; ++m) + xfree(sl->s[m]); + xfree(sl->s); + } + xfree(sll->s); +} + +static void free_collectionlist(struct config *c, + const struct conf *whoami) { + struct collectionlist *cll = ADDRESS(c, struct collectionlist); + struct collection *cl; + int n; + + for(n = 0; n < cll->n; ++n) { + cl = &cll->s[n]; + xfree(cl->module); + xfree(cl->encoding); + xfree(cl->root); + } + xfree(cll->s); +} + +static void free_namepartlist(struct config *c, + const struct conf *whoami) { + struct namepartlist *npl = ADDRESS(c, struct namepartlist); + struct namepart *np; + int n; + + for(n = 0; n < npl->n; ++n) { + np = &npl->s[n]; + xfree(np->part); + pcre_free(np->re); /* ...whatever pcre_free is set to. */ + xfree(np->replace); + xfree(np->context); + } + xfree(npl->s); +} + +static void free_transformlist(struct config *c, + const struct conf *whoami) { + struct transformlist *tl = ADDRESS(c, struct transformlist); + struct transform *t; + int n; + + for(n = 0; n < tl->n; ++n) { + t = &tl->t[n]; + xfree(t->type); + pcre_free(t->re); /* ...whatever pcre_free is set to. */ + xfree(t->replace); + xfree(t->context); + } + xfree(tl->t); +} + +/* configuration types */ + +static const struct conftype + type_signal = { set_signal, free_none }, + type_collections = { set_collections, free_collectionlist }, + type_boolean = { set_boolean, free_none }, + type_string = { set_string, free_string }, + type_stringlist = { set_stringlist, free_stringlist }, + type_integer = { set_integer, free_none }, + type_stringlist_accum = { set_stringlist_accum, free_stringlistlist }, + type_string_accum = { set_string_accum, free_stringlist }, + type_restrict = { set_restrict, free_none }, + type_namepart = { set_namepart, free_namepartlist }, + type_transform = { set_transform, free_transformlist }; + +/* specific validation routine */ + +#define VALIDATE_FILE(test, what) do { \ + struct stat sb; \ + int n; \ + \ + for(n = 0; n < nvec; ++n) { \ + if(stat(vec[n], &sb) < 0) { \ + error(errno, "%s:%d: %s", cs->path, cs->line, vec[n]); \ + return -1; \ + } \ + if(!test(sb.st_mode)) { \ + error(0, "%s:%d: %s is not a %s", \ + cs->path, cs->line, vec[n], what); \ + return -1; \ + } \ + } \ +} while(0) + +static int validate_isdir(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISDIR, "directory"); + return 0; +} + +static int validate_isreg(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISREG, "regular file"); + return 0; +} + +static int validate_ischr(const struct config_state *cs, + int nvec, char **vec) { + VALIDATE_FILE(S_ISCHR, "character device"); + return 0; +} + +static int validate_player(const struct config_state *cs, + int nvec, + char attribute((unused)) **vec) { + if(nvec < 2) { + error(0, "%s:%d: should be at least 'player PATTERN MODULE'", + cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_allow(const struct config_state *cs, + int nvec, + char attribute((unused)) **vec) { + if(nvec != 2) { + error(0, "%s:%d: must be 'allow NAME PASS'", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_non_negative(const struct config_state *cs, + int nvec, char **vec) { + long n; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + if(xstrtol(&n, vec[0], 0, 0)) { + error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno)); + return -1; + } + if(n < 0) { + error(0, "%s:%d: must not be negative", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_positive(const struct config_state *cs, + int nvec, char **vec) { + long n; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + if(xstrtol(&n, vec[0], 0, 0)) { + error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno)); + return -1; + } + if(n <= 0) { + error(0, "%s:%d: must be positive", cs->path, cs->line); + return -1; + } + return 0; +} + +static int validate_isauser(const struct config_state *cs, + int attribute((unused)) nvec, + char **vec) { + struct passwd *pw; + + if(!(pw = getpwnam(vec[0]))) { + error(0, "%s:%d: no such user as '%s'", cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +static int validate_channel(const struct config_state *cs, + int attribute((unused)) nvec, + char **vec) { + if(mixer_channel(vec[0]) == -1) { + error(0, "%s:%d: invalid channel '%s'", cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +static int validate_any(const struct config_state attribute((unused)) *cs, + int attribute((unused)) nvec, + char attribute((unused)) **vec) { + return 0; +} + +static int validate_url(const struct config_state attribute((unused)) *cs, + int attribute((unused)) nvec, + char **vec) { + const char *s; + int n; + /* absoluteURI = scheme ":" ( hier_part | opaque_part ) + scheme = alpha *( alpha | digit | "+" | "-" | "." ) */ + s = vec[0]; + n = strspn(s, ("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789")); + if(s[n] != ':') { + error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]); + return -1; + } + if(!strncmp(s, "http:", 5) + || !strncmp(s, "https:", 6)) { + s += n + 1; + /* we only do a rather cursory check */ + if(strncmp(s, "//", 2)) { + error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]); + return -1; + } + } + return 0; +} + +static int validate_alias(const struct config_state *cs, + int nvec, + char **vec) { + const char *s; + int in_brackets = 0, c; + + if(nvec < 1) { + error(0, "%s:%d: missing argument", cs->path, cs->line); + return -1; + } + if(nvec > 1) { + error(0, "%s:%d: too many arguments", cs->path, cs->line); + return -1; + } + s = vec[0]; + while((c = (unsigned char)*s++)) { + if(in_brackets) { + if(c == '}') + in_brackets = 0; + else if(!isalnum(c)) { + error(0, "%s:%d: invalid part name in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + } else { + if(c == '{') { + in_brackets = 1; + if(*s == '/') + ++s; + } else if(c == '\\') { + if(!(c = (unsigned char)*s++)) { + error(0, "%s:%d: unterminated escape in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } else if(c != '\\' && c != '{') { + error(0, "%s:%d: invalid escape in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + } + } + ++s; + } + if(in_brackets) { + error(0, "%s:%d: unterminated part name in alias expansion in '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + return 0; +} + +/* configuration table */ + +#define C(x) #x, offsetof(struct config, x) +#define C2(x,y) #x, offsetof(struct config, y) + +static const struct conf conf[] = { + { C(alias), &type_string, validate_alias }, + { C(allow), &type_stringlist_accum, validate_allow }, + { C(channel), &type_string, validate_channel }, + { C(checkpoint_kbyte), &type_integer, validate_non_negative }, + { C(checkpoint_min), &type_integer, validate_non_negative }, + { C(collection), &type_collections, validate_any }, + { C(connect), &type_stringlist, validate_any }, + { C(device), &type_string, validate_any }, + { C(gap), &type_integer, validate_non_negative }, + { C(history), &type_integer, validate_positive }, + { C(home), &type_string, validate_isdir }, + { C(listen), &type_stringlist, validate_any }, + { C(lock), &type_boolean, validate_any }, + { C(mixer), &type_string, validate_ischr }, + { C(namepart), &type_namepart, validate_any }, + { C2(nice, nice_rescan), &type_integer, validate_non_negative }, + { C(nice_rescan), &type_integer, validate_non_negative }, + { C(nice_server), &type_integer, validate_any }, + { C(nice_speaker), &type_integer, validate_any }, + { C(password), &type_string, validate_any }, + { C(player), &type_stringlist_accum, validate_player }, + { C(plugins), &type_string_accum, validate_isdir }, + { C(prefsync), &type_integer, validate_positive }, + { C(refresh), &type_integer, validate_positive }, + { C2(restrict, restrictions), &type_restrict, validate_any }, + { C(scratch), &type_string_accum, validate_isreg }, + { C(signal), &type_signal, validate_any }, + { C(stopword), &type_string_accum, validate_any }, + { C(templates), &type_string_accum, validate_isdir }, + { C(transform), &type_transform, validate_any }, + { C(trust), &type_string_accum, validate_any }, + { C(url), &type_string, validate_url }, + { C(user), &type_string, validate_isauser }, + { C(username), &type_string, validate_any }, +}; + +/* find a configuration item's definition by key */ +static const struct conf *find(const char *key) { + int n; + + if((n = TABLE_FIND(conf, struct conf, name, key)) < 0) + return 0; + return &conf[n]; +} + +/* set a new configuration value */ +static int config_set(const struct config_state *cs, + int nvec, char **vec) { + const struct conf *which; + + D(("config_set %s", vec[0])); + if(!(which = find(vec[0]))) { + error(0, "%s:%d: unknown configuration key '%s'", + cs->path, cs->line, vec[0]); + return -1; + } + return (which->validate(cs, nvec - 1, vec + 1) + || which->type->set(cs, which, nvec - 1, vec + 1)); +} + +static void config_error(const char *msg, void *u) { + const struct config_state *cs = u; + + error(0, "%s:%d: %s", cs->path, cs->line, msg); +} + +/* include a file by name */ +static int config_include(struct config *c, const char *path) { + FILE *fp; + char *buffer, *inputbuffer, **vec; + int n, ret = 0; + struct config_state cs; + + cs.path = path; + cs.line = 0; + cs.config = c; + D(("%s: reading configuration", path)); + if(!(fp = fopen(path, "r"))) { + error(errno, "error opening %s", path); + return -1; + } + while(!inputline(path, fp, &inputbuffer, '\n')) { + ++cs.line; + if(!(buffer = mb2utf8(inputbuffer))) { + error(errno, "%s:%d: cannot convert to UTF-8", cs.path, cs.line); + ret = -1; + xfree(inputbuffer); + continue; + } + xfree(inputbuffer); + if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES, + config_error, &cs))) { + ret = -1; + xfree(buffer); + continue; + } + if(n) { + if(!strcmp(vec[0], "include")) { + if(n != 2) { + error(0, "%s:%d: must be 'include PATH'", cs.path, cs.line); + ret = -1; + } else + config_include(c, vec[1]); + } else + ret |= config_set(&cs, n, vec); + } + for(n = 0; vec[n]; ++n) xfree(vec[n]); + xfree(vec); + xfree(buffer); + } + if(ferror(fp)) { + error(errno, "error reading %s", path); + ret = -1; + } + fclose(fp); + return ret; +} + +/* make a new default config */ +static struct config *config_default(void) { + struct config *c = xmalloc(sizeof *c); + const char *logname; + struct passwd *pw; + + /* Strings had better be xstrdup'd as they will get freed at some point. */ + c->gap = 2; + c->history = 60; + c->home = xstrdup(pkgstatedir); + if(!(pw = getpwuid(getuid()))) + fatal(0, "cannot determine our username"); + logname = pw->pw_name; + c->username = xstrdup(logname); + c->refresh = 15; + c->prefsync = 3600; + c->signal = SIGKILL; + c->alias = xstrdup("{/artist}{/album}{/title}{ext}"); + c->lock = 1; + c->device = xstrdup("default"); + c->nice_rescan = 10; + return c; +} + +static char *get_file(struct config *c, const char *name) { + char *s; + + byte_xasprintf(&s, "%s/%s", c->home, name); + return s; +} + +static void set_configfile(void) { + if(!configfile) + byte_xasprintf(&configfile, "%s/config", pkgconfdir); +} + +/* free the config file */ +static void config_free(struct config *c) { + int n; + + if(c) { + for(n = 0; n < (int)(sizeof conf / sizeof *conf); ++n) + conf[n].type->free(c, &conf[n]); + for(n = 0; n < c->nparts; ++n) + xfree(c->parts[n]); + xfree(c->parts); + xfree(c); + } +} + +static void config_postdefaults(struct config *c) { + struct config_state cs; + const struct conf *whoami; + int n; + + static const char *namepart[][4] = { + { "title", "/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" }, + { "title", "/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort" }, + { "album", "/([^/]+)/[^/]+$", "$1", "*" }, + { "artist", "/([^/]+)/[^/]+/[^/]+$", "$1", "*" }, + { "ext", "(\\.[a-zA-Z0-9]+)$", "$1", "*" }, + }; +#define NNAMEPART (int)(sizeof namepart / sizeof *namepart) + + static const char *transform[][5] = { + { "track", "^.*/([0-9]+:)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" }, + { "track", "^.*/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort", "" }, + { "dir", "^.*/([^/]+)$", "$1", "*", "" }, + { "dir", "^(the) ([^/]*)", "$2, $1", "sort", "i", }, + { "dir", "[[:punct:]]", "", "sort", "g", } + }; +#define NTRANSFORM (int)(sizeof transform / sizeof *transform) + + cs.path = "<internal>"; + cs.line = 0; + cs.config = c; + if(!c->namepart.n) { + whoami = find("namepart"); + for(n = 0; n < NNAMEPART; ++n) + set_namepart(&cs, whoami, 4, (char **)namepart[n]); + } + if(!c->transform.n) { + whoami = find("transform"); + for(n = 0; n < NTRANSFORM; ++n) + set_transform(&cs, whoami, 5, (char **)transform[n]); + } +} + +/* re-read the config file */ +int config_read() { + struct config *c; + char *privconf; + struct passwd *pw; + + set_configfile(); + c = config_default(); + if(config_include(c, configfile)) + return -1; + /* if we can read the private config file, do */ + if((privconf = config_private()) + && access(privconf, R_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* if there's a per-user system config file for this user, read it */ + if(!(pw = getpwuid(getuid()))) + fatal(0, "cannot determine our username"); + if((privconf = config_usersysconf(pw)) + && access(privconf, F_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* if we have a password file, read it */ + if((privconf = config_userconf(getenv("HOME"), pw)) + && access(privconf, F_OK) == 0 + && config_include(c, privconf)) + return -1; + xfree(privconf); + /* install default namepart and transform settings */ + config_postdefaults(c); + /* everything is good so we shall use the new config */ + config_free(config); + config = c; + return 0; +} + +char *config_private(void) { + char *s; + + set_configfile(); + byte_xasprintf(&s, "%s.private", configfile); + return s; +} + +char *config_userconf(const char *home, const struct passwd *pw) { + char *s; + + byte_xasprintf(&s, "%s/.disorder/passwd", home ? home : pw->pw_dir); + return s; +} + +char *config_usersysconf(const struct passwd *pw ) { + char *s; + + set_configfile(); + if(!strchr(pw->pw_name, '/')) { + byte_xasprintf(&s, "%s.%s", configfile, pw->pw_name); + return s; + } else + return 0; +} + +char *config_get_file(const char *name) { + return get_file(config, name); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:ede19ed49dca6136ba864ed0a4988b34 */ diff --git a/lib/configuration.h b/lib/configuration.h new file mode 100644 index 0000000..d647219 --- /dev/null +++ b/lib/configuration.h @@ -0,0 +1,155 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef CONFIGURATION_H +#define CONFIGURATION_H + +struct real_pcre; + +/* Configuration is kept in a @struct config@; the live configuration + * is always pointed to by @config@. Values in @config@ are UTF-8 encoded. + */ + +struct stringlist { + int n; + char **s; +}; + +struct stringlistlist { + int n; + struct stringlist *s; +}; + +struct collection { + char *module; + char *encoding; + char *root; +}; + +struct collectionlist { + int n; + struct collection *s; +}; + +struct namepart { + char *part; /* part */ + struct real_pcre *re; /* regexp */ + char *replace; /* replacement string */ + char *context; /* context glob */ + unsigned reflags; /* regexp flags */ +}; + +struct namepartlist { + int n; + struct namepart *s; +}; + +struct transform { + char *type; /* track or dir */ + char *context; /* sort or choose */ + char *replace; /* substitution string */ + struct real_pcre *re; /* compiled re */ + unsigned flags; /* regexp flags */ +}; + +struct transformlist { + int n; + struct transform *t; +}; + +struct config { + /* server config */ + struct stringlistlist player; /* players */ + struct stringlistlist allow; /* allowed users */ + struct stringlist scratch; /* scratch tracks */ + long gap; /* gap between tracks */ + long history; /* length of history */ + struct stringlist trust; /* trusted users */ + const char *user; /* user to run as */ + long nice_rescan; /* rescan subprocess niceness */ + struct stringlist plugins; /* plugin path */ + struct stringlist stopword; /* stopwords for track search */ + struct collectionlist collection; /* track collections */ + long checkpoint_kbyte; + long checkpoint_min; + char *mixer; /* mixer device file */ + char *channel; /* mixer channel */ + long prefsync; /* preflog sync intreval */ + struct stringlist listen; /* secondary listen address */ + const char *alias; /* alias format */ + int lock; /* server takes a lock */ + long nice_server; /* nice value for server */ + long nice_speaker; /* nice value for speaker */ + /* shared client/server config */ + const char *home; /* home directory for state files */ + /* client config */ + const char *username, *password; /* our own username and password */ + struct stringlist connect; /* connect address */ + /* web config */ + struct stringlist templates; /* template path */ + const char *url; /* canonical URL */ + long refresh; /* maximum refresh period */ + unsigned restrictions; /* restrictions */ +#define RESTRICT_SCRATCH 1 +#define RESTRICT_REMOVE 2 +#define RESTRICT_MOVE 4 + struct namepartlist namepart; /* transformations */ + int signal; /* termination signal */ + const char *device; /* ALSA output device */ + struct transformlist transform; /* path name transformations */ + + /* derived values: */ + int nparts; /* number of distinct name parts */ + char **parts; /* name part list */ +}; + +extern struct config *config; +/* the current configuration */ + +int config_read(void); +/* re-read config, return 0 on success or non-0 on error. + * Only updates @config@ if the new configuration is valid. */ + +char *config_get_file(const char *name); +/* get a filename within the home directory */ + +struct passwd; + +char *config_userconf(const char *home, const struct passwd *pw); +/* get the user's own private conffile, assuming their home dir is + * @home@ if not null and using @pw@ otherwise */ + +char *config_usersysconf(const struct passwd *pw ); +/* get the user's conffile in /etc */ + +char *config_private(void); +/* get the private config file */ + +extern char *configfile; + +#endif /* CONFIGURATION_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:bf8be7718c5e6348d0c922dfa66b85f9 */ diff --git a/lib/defs.c b/lib/defs.c new file mode 100644 index 0000000..2bc9296 --- /dev/null +++ b/lib/defs.c @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "defs.h" +#include "definitions.h" + +const char disorder_version_string[] = VERSION; +const char pkglibdir[] = PKGLIBDIR; +const char pkgconfdir[] = PKGCONFDIR; +const char pkgstatedir[] = PKGSTATEDIR; +const char pkgdatadir[] = PKGDATADIR; + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:m7TX2pyO7AO3F/rjc8kqYA */ diff --git a/lib/defs.h b/lib/defs.h new file mode 100644 index 0000000..52271b3 --- /dev/null +++ b/lib/defs.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DEFS_H +#define DEFS_H + +extern const char disorder_version_string[]; +extern const char pkglibdir[]; +extern const char pkgconfdir[]; +extern const char pkgstatedir[]; +extern const char pkgdatadir[]; + +#endif /* DEFS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:6CvTBnYnuaIeb/cbLdo9wg */ diff --git a/lib/disorder.h b/lib/disorder.h new file mode 100644 index 0000000..ded021f --- /dev/null +++ b/lib/disorder.h @@ -0,0 +1,215 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef DISORDER_H +#define DISORDER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* memory allocation **********************************************************/ + +void *disorder_malloc(size_t); +void *disorder_realloc(void *, size_t); +/* As malloc/realloc, but + * 1) succeed or call fatal + * 2) always clear (the unused part of) the new allocation + * 3) are garbage-collected + */ + +void *disorder_malloc_noptr(size_t); +void *disorder_realloc_noptr(void *, size_t); +char *disorder_strdup(const char *); +char *disorder_strndup(const char *, size_t); +/* As malloc/realloc/strdup, but + * 1) succeed or call fatal + * 2) are garbage-collected + * 3) allocated space must not contain any pointers + * + * {xmalloc,xrealloc}_noptr don't promise to clear the new space + */ + +#ifdef __GNUC__ + +int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) + __attribute__((format (printf, 3, 4))); +/* like snprintf */ + +int disorder_asprintf(char **rp, const char *fmt, ...) + __attribute__((format (printf, 2, 3))); +/* like asprintf but uses xmalloc_noptr() */ + +#else + +int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...); +/* like snprintf */ + +int disorder_asprintf(char **rp, const char *fmt, ...); +/* like asprintf but uses xmalloc_noptr() */ + +#endif + + +/* logging ********************************************************************/ + +void disorder_error(int errno_value, const char *fmt, ...); +/* report an error. If errno_value is nonzero then the errno string + * is included. */ + +void disorder_fatal(int errno_value, const char *fmt, ...); +/* report an error and terminate. If errno_value is nonzero then the + * errno string is included. This is the only safe way to terminate + * the process. */ + +void disorder_info(const char *fmt, ...); +/* log a message. */ + +/* track database *************************************************************/ + +int disorder_track_exists(const char *track); +/* return true if the track exists. */ + +const char *disorder_track_get_data(const char *track, const char *key); +/* get the value for @key@ (xstrdup'd) */ + +int disorder_track_set_data(const char *track, + const char *key, const char *value); +/* set the value of @key@ to @value@, or remove it if @value@ is a null + * pointer. Return 0 on success, -1 on error. */ + +const char *disorder_track_random(void); /* server plugins only */ +/* return the name of a random track */ + +/* plugin interfaces **********************************************************/ + +long disorder_tracklength(const char *track, const char *path); +/* compute the length of the track. @track@ is the UTF-8 name of the + * track, @path@ is the file system name (or 0 for tracks that don't + * exist in the filesystem). The return value should be a positive + * number of seconds, 0 for unknown or -1 if an error occurred. */ + +void disorder_scan(const char *root); +/* write a list of path names below @root@ to standard output. */ + +int disorder_check(const char *root, const char *path); +/* Recheck a track, given its root and path name. Return 1 if it + * exists, 0 if it does not exist and -1 if an error occurred. */ + +void disorder_notify_play(const char *track, + const char *submitter); +/* we're going to play @track@. It was submitted by @submitter@ + * (might be a null pointer) */ + +void disorder_notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +/* @scratcher@ scratched @track@ after @seconds@. It was submitted by + * @submitter@ (might be a null pointer) */ + +void disorder_notify_not_scratched(const char *track, + const char *submitter); +/* @track@ (submitted by @submitter@, which might be a null pointer) + * was not scratched. */ + +void disorder_notify_queue(const char *track, + const char *submitter); +/* @track@ added to the queue by @submitter@ (never a null pointer) */ + +void disorder_notify_queue_remove(const char *track, + const char *remover); +/* @track@ removed from the queue by @remover@ (never a null pointer) */ + +void disorder_notify_queue_move(const char *track, + const char *mover); +/* @track@ moved in the queue by @mover@ (never a null pointer) */ + +void disorder_notify_pause(const char *track, + const char *pauser); +/* TRACK was paused by PAUSER (might be a null pointer) */ + +void disorder_notify_resume(const char *track, + const char *resumer); +/* TRACK was resumed by PAUSER (might be a null pointer) */ + +/* player plugin interface ****************************************************/ + +extern const unsigned long disorder_player_type; + +#define DISORDER_PLAYER_STANDALONE 0x00000000 +/* this player plays sound directly */ + +#define DISORDER_PLAYER_RAW 0x00000001 +/* player that sends raw samples to $DISORDER_RAW_FD */ + +#define DISORDER_PLAYER_TYPEMASK 0x000000ff +/* mask for player types */ + +#define DISORDER_PLAYER_PREFORK 0x00000100 +/* call prefork function */ + +#define DISORDER_PLAYER_PAUSES 0x00000200 +/* supports pausing */ + +void *disorder_play_prefork(const char *track); +/* Called outside the fork. Should not block. Returns a null pointer + * on error. + * + * If _play_prefork is called then its return value is used as the + * DATA argument to the following functions. Otherwise the value of + * DATA argument is indeterminate and must not be used. */ + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void *data); +/* Called to play a track. Should either exec or only return when the + * track has finished. Should not call exit() (except after a + * succesful exec). Allowed to call _Exit(). */ + +int disorder_play_pause(long *playedp, void *data); +/* Pauses the playing track. If the track can be paused returns 0 and + * stores the number of seconds so far played via PLAYEDP, or sets it + * to -1 if this is not known. If the track cannot be paused then + * returns -1. Should not block. + */ + +void disorder_play_resume(void *data); +/* Restarts play after a pause. PLAYED is the value returned from the + * original pause operation. Should not block. */ + +void disorder_play_cleanup(void *data); +/* called to clean up DATA. Should not block. */ + +#ifdef __cplusplus +}; +#endif + +#endif /* DISORDER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e9581e408f14d9382e27fbc06755baa6 */ diff --git a/lib/eclient.c b/lib/eclient.c new file mode 100644 index 0000000..5e6cd6f --- /dev/null +++ b/lib/eclient.c @@ -0,0 +1,1234 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <netdb.h> +#include <stdlib.h> +#include <assert.h> +#include <inttypes.h> +#include <stddef.h> + +#include "log.h" +#include "mem.h" +#include "configuration.h" +#include "queue.h" +#include "eclient.h" +#include "charset.h" +#include "hex.h" +#include "split.h" +#include "vector.h" +#include "inputline.h" +#include "kvp.h" +#include "syscalls.h" +#include "printf.h" +#include "addr.h" +#include "authhash.h" +#include "table.h" +#include "client-common.h" + +/* TODO: more commands */ + +/* Types *********************************************************************/ + +enum client_state { + state_disconnected, /* not connected */ + state_connecting, /* waiting for connect() */ + state_connected, /* connected but not authenticated */ + state_idle, /* not doing anything */ + state_cmdresponse, /* waiting for command resonse */ + state_body, /* accumulating body */ + state_log, /* monitoring log */ +}; + +static const char *const states[] = { + "disconnected", + "connecting", + "connected", + "idle", + "cmdresponse", + "body", + "log" +}; + +struct operation; /* forward decl */ + +typedef void operation_callback(disorder_eclient *c, struct operation *op); + +/* A pending operation. This can be either a command or part of the + * authentication protocol. In the former case new commands are appended to + * the list, in the latter case they are inserted at the front. */ +struct operation { + struct operation *next; /* next operation */ + char *cmd; /* command to send or 0 */ + operation_callback *opcallback; /* internal completion callback */ + void (*completed)(); /* user completion callback or 0 */ + void *v; /* data for COMPLETED */ + disorder_eclient *client; /* owning client */ + int sent; /* true if sent to server */ +}; + +struct disorder_eclient { + const char *ident; + int fd; + enum client_state state; /* current state */ + int authenticated; /* true when authenicated */ + struct dynstr output; /* output buffer */ + struct dynstr input; /* input buffer */ + int eof; /* input buffer is at EOF */ + /* error reporting callbacks */ + const disorder_eclient_callbacks *callbacks; + void *u; + /* operation queuue */ + struct operation *ops, **opstail; + /* accumulated response */ + int rc; /* response code */ + char *line; /* complete line */ + struct vector vec; /* body */ + /* log client callback */ + const disorder_eclient_log_callbacks *log_callbacks; + void *log_v; + unsigned long statebits; /* current state */ +}; + +/* Forward declarations ******************************************************/ + +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident); +static void process_line(disorder_eclient *c, char *line); +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident); +static void maybe_connected(disorder_eclient *c); +static void authbanner_opcallback(disorder_eclient *c, + struct operation *op); +static void authuser_opcallback(disorder_eclient *c, + struct operation *op); +static void complete(disorder_eclient *c); +static void send_output(disorder_eclient *c); +static void put(disorder_eclient *c, const char *s, size_t n); +static void read_input(disorder_eclient *c); +static void stash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, + ...); +static void log_opcallback(disorder_eclient *c, struct operation *op); +static void logline(disorder_eclient *c, const char *line); +static void logentry_completed(disorder_eclient *c, int nvec, char **vec); +static void logentry_failed(disorder_eclient *c, int nvec, char **vec); +static void logentry_moved(disorder_eclient *c, int nvec, char **vec); +static void logentry_playing(disorder_eclient *c, int nvec, char **vec); +static void logentry_queue(disorder_eclient *c, int nvec, char **vec); +static void logentry_recent_added(disorder_eclient *c, int nvec, char **vec); +static void logentry_recent_removed(disorder_eclient *c, int nvec, char **vec); +static void logentry_removed(disorder_eclient *c, int nvec, char **vec); +static void logentry_scratched(disorder_eclient *c, int nvec, char **vec); +static void logentry_state(disorder_eclient *c, int nvec, char **vec); +static void logentry_volume(disorder_eclient *c, int nvec, char **vec); + +/* Tables ********************************************************************/ + +static const struct logentry_handler { + const char *name; + int min, max; + void (*handler)(disorder_eclient *c, + int nvec, + char **vec); +} logentry_handlers[] = { +#define LE(X, MIN, MAX) { #X, MIN, MAX, logentry_##X } + LE(completed, 1, 1), + LE(failed, 2, 2), + LE(moved, 1, 1), + LE(playing, 1, 2), + LE(queue, 2, INT_MAX), + LE(recent_added, 2, INT_MAX), + LE(recent_removed, 1, 1), + LE(removed, 1, 2), + LE(scratched, 2, 2), + LE(state, 1, 1), + LE(volume, 2, 2) +}; + +/* Setup and teardown ********************************************************/ + +disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, + void *u) { + disorder_eclient *c = xmalloc(sizeof *c); + D(("disorder_eclient_new")); + c->fd = -1; + c->callbacks = cb; + c->u = u; + c->opstail = &c->ops; + vector_init(&c->vec); + dynstr_init(&c->input); + dynstr_init(&c->output); + return c; +} + +void disorder_eclient_close(disorder_eclient *c) { + struct operation *op; + + D(("disorder_eclient_close")); + if(c->fd != -1) { + D(("disorder_eclient_close closing fd %d", c->fd)); + c->callbacks->poll(c->u, c, c->fd, 0); + xclose(c->fd); + c->fd = -1; + c->state = state_disconnected; + } + c->output.nvec = 0; + c->input.nvec = 0; + c->eof = 0; + c->authenticated = 0; + /* We'll need to resend all operations */ + for(op = c->ops; op; op = op->next) + op->sent = 0; +} + +/* Error reporting ***********************************************************/ + +/* called when a connection error occurs */ +static int comms_error(disorder_eclient *c, const char *fmt, ...) { + va_list ap; + char *s; + + D(("comms_error")); + va_start(ap, fmt); + byte_xvasprintf(&s, fmt, ap); + va_end(ap); + disorder_eclient_close(c); + c->callbacks->comms_error(c->u, s); + return -1; +} + +/* called when the server reports an error */ +static int protocol_error(disorder_eclient *c, struct operation *op, + int code, const char *fmt, ...) { + va_list ap; + char *s; + + D(("protocol_error")); + va_start(ap, fmt); + byte_xvasprintf(&s, fmt, ap); + va_end(ap); + c->callbacks->protocol_error(c->u, op->v, code, s); + return -1; +} + +/* State machine *************************************************************/ + +void disorder_eclient_polled(disorder_eclient *c, unsigned mode) { + struct operation *op; + + D(("disorder_eclient_polled fd=%d state=%s mode=[%s %s]", + c->fd, states[c->state], + mode & DISORDER_POLL_READ ? "READ" : "", + mode & DISORDER_POLL_WRITE ? "WRITE" : "")); + /* The pattern here is to check each possible state in turn and try to + * advance (though on error we might go back). If we advance we leave open + * the possibility of falling through to the next state, but we set the mode + * bits to 0, to avoid false positives (which matter more in some cases than + * others). */ + + if(c->state == state_disconnected) { + D(("state_disconnected")); + with_sockaddr(c, start_connect); + /* might now be state_disconnected (on error), state_connecting (slow + * connect) or state_connected (fast connect). If state_disconnected then + * we just rely on a periodic callback from the event loop sometime. */ + mode = 0; + } + + if(c->state == state_connecting && mode) { + D(("state_connecting")); + maybe_connected(c); + /* Might be state_disconnected (on error) or state_connected (on success). + * In the former case we rely on the event loop for a periodic callback to + * retry. */ + mode = 0; + } + + if(c->state == state_connected) { + D(("state_connected")); + /* We just connected. Initiate the authentication protocol. */ + stash_command(c, 1/*queuejump*/, authbanner_opcallback, + 0/*completed*/, 0/*v*/, 0/*cmd*/); + /* We never stay is state_connected very long. We could in principle jump + * straight to state_cmdresponse since there's actually no command to + * send, but that would arguably be cheating. */ + c->state = state_idle; + } + + if(c->state == state_idle) { + D(("state_idle")); + /* We are connected, and have finished any command we set off, look for + * some work to do */ + if(c->ops) { + D(("have ops")); + if(c->authenticated) { + /* Transmit all unsent operations */ + for(op = c->ops; op; op = op->next) { + if(!op->sent) { + put(c, op->cmd, strlen(op->cmd)); + op->sent = 1; + } + } + } else { + /* Just send the head operation */ + if(c->ops->cmd && !c->ops->sent) { + put(c, c->ops->cmd, strlen(c->ops->cmd)); + c->ops->sent = 1; + } + } + /* Awaiting response for the operation at the head of the list */ + c->state = state_cmdresponse; + } else + /* genuinely idle */ + c->callbacks->report(c->u, 0); + } + + if(c->state == state_cmdresponse + || c->state == state_body + || c->state == state_log) { + D(("state_%s", states[c->state])); + /* We are awaiting a response */ + if(mode & DISORDER_POLL_WRITE) send_output(c); + if(mode & DISORDER_POLL_READ) read_input(c); + /* There are a couple of reasons we might want to re-enter the state + * machine from the top. state_idle is obvious: there may be further + * commands to process. Re-entering on state_disconnected means that we + * immediately retry connection if a comms error occurs during a command. + * This is different to the case where a connection fails, where we await a + * spontaneous call to initiate the retry. */ + switch(c->state) { + case state_disconnected: /* lost connection */ + case state_idle: /* completed a command */ + D(("retrying")); + disorder_eclient_polled(c, 0); + return; + default: + break; + } + } + + /* Figure out what to set the mode to */ + switch(c->state) { + case state_disconnected: + D(("state_disconnected (2)")); + /* Probably an error occurred. Await a retry. */ + mode = 0; + break; + case state_connecting: + D(("state_connecting (2)")); + /* Waiting for connect to complete */ + mode = DISORDER_POLL_READ|DISORDER_POLL_WRITE; + break; + case state_connected: + D(("state_connected (2)")); + assert(!"should never be in state_connected here"); + break; + case state_idle: + D(("state_idle (2)")); + /* Connected but nothing to do. */ + mode = 0; + break; + case state_cmdresponse: + case state_body: + case state_log: + D(("state_%s (2)", states[c->state])); + /* Gathering a response. Wait for input. */ + mode = DISORDER_POLL_READ; + /* Flush any pending output. */ + if(c->output.nvec) mode |= DISORDER_POLL_WRITE; + break; + } + D(("fd=%d new mode [%s %s]", + c->fd, + mode & DISORDER_POLL_READ ? "READ" : "", + mode & DISORDER_POLL_WRITE ? "WRITE" : "")); + if(c->fd != -1) c->callbacks->poll(c->u, c, c->fd, mode); +} + +/* Called to start connecting */ +static int start_connect(void *cc, + const struct sockaddr *sa, + socklen_t len, + const char *ident) { + disorder_eclient *c = cc; + + D(("start_connect")); + c->ident = xstrdup(ident); + if(c->fd != -1) { + xclose(c->fd); + c->fd = -1; + } + if((c->fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) + return comms_error(c, "socket: %s", strerror(errno)); + c->eof = 0; + nonblock(c->fd); + if(connect(c->fd, sa, len) < 0) { + switch(errno) { + case EINTR: + case EINPROGRESS: + c->state = state_connecting; + /* We are called from _polled so the state machine will get to do its + * thing */ + return 0; + default: + /* Signal the error to the caller. */ + return comms_error(c, "connecting to %s: %s", ident, strerror(errno)); + } + } else + c->state = state_connected; + return 0; +} + +/* Called when maybe connected */ +static void maybe_connected(disorder_eclient *c) { + /* We either connected, or got an error. */ + int err; + socklen_t len = sizeof err; + + D(("maybe_connected")); + /* Work around over-enthusiastic error slippage */ + if(getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) + err = errno; + if(err) { + /* The connection failed */ + comms_error(c, "connecting to %s: %s", c->ident, strerror(err)); + /* sets state_disconnected */ + } else { + /* The connection succeeded */ + c->state = state_connected; + } +} + +/* Authentication ************************************************************/ + +static void authbanner_opcallback(disorder_eclient *c, + struct operation *op) { + size_t nonce_len; + const unsigned char *nonce; + const char *res; + + D(("authbanner_opcallback")); + if(c->rc / 100 != 2) { + /* Banner told us to go away. We cannot proceed. */ + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + disorder_eclient_close(c); + return; + } + nonce = unhex(c->line + 4, &nonce_len); + res = authhash(nonce, nonce_len, config->password); + stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/, + "user", quoteutf8(config->username), quoteutf8(res), + (char *)0); +} + +static void authuser_opcallback(disorder_eclient *c, + struct operation *op) { + D(("authuser_opcallback")); + if(c->rc / 100 != 2) { + /* Wrong password or something. We cannot proceed. */ + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + disorder_eclient_close(c); + return; + } + /* OK, we're authenticated now. */ + c->authenticated = 1; + if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback)) + /* We are a log client, switch to logging mode */ + stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v, + "log", (char *)0); +} + +/* Output ********************************************************************/ + +/* Chop N bytes off the front of a dynstr */ +static void consume(struct dynstr *d, int n) { + D(("consume %d", n)); + assert(d->nvec >= n); + memmove(d->vec, d->vec + n, d->nvec - n); + d->nvec -= n; +} + +/* Write some bytes */ +static void put(disorder_eclient *c, const char *s, size_t n) { + D(("put %d %.*s", c->fd, (int)n, s)); + dynstr_append_bytes(&c->output, s, n); +} + +/* Called when we can write to our FD, or at any other time */ +static void send_output(disorder_eclient *c) { + int n; + + D(("send_output %d bytes pending", c->output.nvec)); + if(c->state > state_connecting && c->output.nvec) { + n = write(c->fd, c->output.vec, c->output.nvec); + if(n < 0) { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + comms_error(c, "writing to %s: %s", c->ident, strerror(errno)); + break; + } + } else + consume(&c->output, n); + } +} + +/* Input *********************************************************************/ + +/* Called when c->fd might be readable, or at any other time */ +static void read_input(disorder_eclient *c) { + char *nl; + int n; + char buffer[512]; + + D(("read_input in state %s", states[c->state])); + if(c->state <= state_connected) return; /* ignore bogus calls */ + /* read some more input */ + n = read(c->fd, buffer, sizeof buffer); + if(n < 0) { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + comms_error(c, "reading from %s: %s", c->ident, strerror(errno)); + break; + } + return; /* no new input to process */ + } else if(n) { + D(("read %d bytes: [%.*s]", n, n, buffer)); + dynstr_append_bytes(&c->input, buffer, n); + } else + c->eof = 1; + /* might have more than one line to process */ + while(c->state > state_connecting + && (nl = memchr(c->input.vec, '\n', c->input.nvec))) { + process_line(c, xstrndup(c->input.vec, nl - c->input.vec)); + /* we might have disconnected along the way, which zogs the input buffer */ + if(c->state > state_connecting) + consume(&c->input, (nl - c->input.vec) + 1); + } + if(c->eof) + comms_error(c, "reading from %s: server disconnected", c->ident); +} + +/* called with a line that has just been read */ +static void process_line(disorder_eclient *c, char *line) { + D(("process_line %d [%s]", c->fd, line)); + switch(c->state) { + case state_cmdresponse: + /* This is the first line of a response */ + if(!(line[0] >= '0' && line[0] <= '9' + && line[1] >= '0' && line[1] <= '9' + && line[2] >= '0' && line[2] <= '9' + && line[3] == ' ')) + fatal(0, "invalid response from server: %s", line); + c->rc = (line[0] * 10 + line[1]) * 10 + line[2] - 111 * '0'; + c->line = line; + switch(c->rc % 10) { + case 3: + /* We need to collect the body. */ + c->state = state_body; + c->vec.nvec = 0; + break; + case 4: + assert(c->log_callbacks != 0); + if(c->log_callbacks->connected) + c->log_callbacks->connected(c->log_v); + c->state = state_log; + break; + default: + /* We've got the whole response. Go into the idle state so the state + * machine knows we're done and then call the operation callback. */ + complete(c); + break; + } + break; + case state_body: + if(strcmp(line, ".")) { + /* A line from the body */ + vector_append(&c->vec, line + (line[0] == '.')); + } else { + /* End of the body. */ + vector_terminate(&c->vec); + complete(c); + } + break; + case state_log: + if(strcmp(line, ".")) { + logline(c, line + (line[0] == '.')); + } else + complete(c); + break; + default: + assert(!"wrong state for location"); + break; + } +} + +/* Called when an operation completes */ +static void complete(disorder_eclient *c) { + struct operation *op; + + D(("complete")); + /* Pop the operation off the queue */ + op = c->ops; + c->ops = op->next; + if(c->opstail == &op->next) + c->opstail = &c->ops; + /* If we've pipelined a command ahead then we go straight to cmdresponser. + * Otherwise we go to idle, which will arrange further sends. */ + c->state = c->ops && c->ops->sent ? state_cmdresponse : state_idle; + op->opcallback(c, op); + /* Note that we always call the opcallback even on error, though command + * opcallbacks generally always do the same error handling, i.e. just call + * protocol_error(). It's the auth* opcallbacks that have different + * behaviour. */ +} + +/* Operation setup ***********************************************************/ + +static void stash_command_vector(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + int ncmd, + char **cmd) { + struct operation *op = xmalloc(sizeof *op); + struct dynstr d; + int n; + + if(cmd) { + dynstr_init(&d); + for(n = 0; n < ncmd; ++n) { + if(n) + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(cmd[n])); + } + dynstr_append(&d, '\n'); + dynstr_terminate(&d); + op->cmd = d.vec; + } else + op->cmd = 0; /* usually, awaiting challenge */ + op->opcallback = opcallback; + op->completed = completed; + op->v = v; + op->next = 0; + op->client = c; + assert(op->sent == 0); + if(queuejump) { + /* Authentication operations jump the queue of useful commands */ + op->next = c->ops; + c->ops = op; + if(c->opstail == &c->ops) + c->opstail = &op->next; + for(op = c->ops; op; op = op->next) + assert(!op->sent); + } else { + *c->opstail = op; + c->opstail = &op->next; + } +} + +static void vstash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, va_list ap) { + char *arg; + struct vector vec; + + D(("vstash_command %s", cmd ? cmd : "NULL")); + if(cmd) { + vector_init(&vec); + vector_append(&vec, (char *)cmd); + while((arg = va_arg(ap, char *))) + vector_append(&vec, arg); + stash_command_vector(c, queuejump, opcallback, completed, v, + vec.nvec, vec.vec); + } else + stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0); +} + +static void stash_command(disorder_eclient *c, + int queuejump, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, + ...) { + va_list ap; + + va_start(ap, cmd); + vstash_command(c, queuejump, opcallback, completed, v, cmd, ap); + va_end(ap); +} + +/* Command support ***********************************************************/ + +/* for commands with a simple string response */ +static void string_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("string_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_string_response *)op->completed)(op->v, c->line + 4); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands with a simple integer response */ +static void integer_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("string_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_integer_response *)op->completed) + (op->v, strtol(c->line + 4, 0, 10)); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands with no response */ +static void no_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("no_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_no_response *)op->completed)(op->v); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* error callback for queue_unmarshall */ +static void eclient_queue_error(const char *msg, + void *u) { + struct operation *op = u; + + protocol_error(op->client, op, -1, "error parsing queue entry: %s", msg); +} + +/* for commands that expect a queue dump */ +static void queue_response_opcallback(disorder_eclient *c, + struct operation *op) { + int n; + struct queue_entry *q, *qh = 0, **qtail = &qh, *qlast = 0; + + D(("queue_response_callback")); + if(c->rc / 100 == 2) { + /* parse the queue */ + for(n = 0; n < c->vec.nvec; ++n) { + q = xmalloc(sizeof *q); + D(("queue_unmarshall %s", c->vec.vec[n])); + if(!queue_unmarshall(q, c->vec.vec[n], eclient_queue_error, op)) { + q->prev = qlast; + *qtail = q; + qtail = &q->next; + qlast = q; + } + } + if(op->completed) + ((disorder_eclient_queue_response *)op->completed)(op->v, qh); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for 'playing' */ +static void playing_response_opcallback(disorder_eclient *c, + struct operation *op) { + struct queue_entry *q; + + D(("playing_response_callback")); + if(c->rc / 100 == 2) { + switch(c->rc % 10) { + case 2: + if(queue_unmarshall(q = xmalloc(sizeof *q), c->line + 4, + eclient_queue_error, c)) + return; + break; + case 9: + q = 0; + break; + default: + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + return; + } + if(op->completed) + ((disorder_eclient_queue_response *)op->completed)(op->v, q); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for commands that expect a list of some sort */ +static void list_response_opcallback(disorder_eclient *c, + struct operation *op) { + D(("list_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) + ((disorder_eclient_list_response *)op->completed)(op->v, + c->vec.nvec, + c->vec.vec); + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +/* for volume */ +static void volume_response_opcallback(disorder_eclient *c, + struct operation *op) { + int l, r; + + D(("volume_response_callback")); + if(c->rc / 100 == 2) { + if(op->completed) { + if(sscanf(c->line + 4, "%d %d", &l, &r) != 2 || l < 0 || r < 0) + protocol_error(c, op, -1, "%s: invalid volume response: %s", + c->ident, c->line); + else + ((disorder_eclient_volume_response *)op->completed)(op->v, l, r); + } + } else + protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); +} + +static int simple(disorder_eclient *c, + operation_callback *opcallback, + void (*completed)(), + void *v, + const char *cmd, ...) { + va_list ap; + + va_start(ap, cmd); + vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap); + va_end(ap); + /* Give the state machine a kick, since we might be in state_idle */ + disorder_eclient_polled(c, 0); + return 0; +} + +/* Commands ******************************************************************/ + +int disorder_eclient_version(disorder_eclient *c, + disorder_eclient_string_response *completed, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "version", (char *)0); +} + +int disorder_eclient_namepart(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + const char *context, + const char *part, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "part", track, context, part, (char *)0); +} + +int disorder_eclient_play(disorder_eclient *c, + const char *track, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "play", track, (char *)0); +} + +int disorder_eclient_pause(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "pause", (char *)0); +} + +int disorder_eclient_resume(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "resume", (char *)0); +} + +int disorder_eclient_scratch(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "scratch", id, (char *)0); +} + +int disorder_eclient_scratch_playing(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return disorder_eclient_scratch(c, 0, completed, v); +} + +int disorder_eclient_remove(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "remove", id, (char *)0); +} + +int disorder_eclient_moveafter(disorder_eclient *c, + const char *target, + int nids, + const char **ids, + disorder_eclient_no_response *completed, + void *v) { + struct vector vec; + int n; + + vector_init(&vec); + vector_append(&vec, (char *)"moveafter"); + vector_append(&vec, (char *)target); + for(n = 0; n < nids; ++n) + vector_append(&vec, (char *)ids[n]); + stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v, + vec.nvec, vec.vec); + disorder_eclient_polled(c, 0); + return 0; +} + +int disorder_eclient_recent(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, queue_response_opcallback, (void (*)())completed, v, + "recent", (char *)0); +} + +int disorder_eclient_queue(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, queue_response_opcallback, (void (*)())completed, v, + "queue", (char *)0); +} + +int disorder_eclient_files(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "files", dir, re, (char *)0); +} + +int disorder_eclient_dirs(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "dirs", dir, re, (char *)0); +} + +int disorder_eclient_playing(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v) { + return simple(c, playing_response_opcallback, (void (*)())completed, v, + "playing", (char *)0); +} + +int disorder_eclient_length(disorder_eclient *c, + disorder_eclient_integer_response *completed, + const char *track, + void *v) { + return simple(c, integer_response_opcallback, (void (*)())completed, v, + "length", track, (char *)0); +} + +int disorder_eclient_volume(disorder_eclient *c, + disorder_eclient_volume_response *completed, + int l, int r, + void *v) { + char sl[64], sr[64]; + + if(l < 0 && r < 0) { + return simple(c, volume_response_opcallback, (void (*)())completed, v, + "volume", (char *)0); + } else if(l >= 0 && r >= 0) { + assert(l <= 100); + assert(r <= 100); + byte_snprintf(sl, sizeof sl, "%d", l); + byte_snprintf(sr, sizeof sr, "%d", r); + return simple(c, volume_response_opcallback, (void (*)())completed, v, + "volume", sl, sr, (char *)0); + } else { + assert(!"invalid arguments to disorder_eclient_volume"); + return -1; /* gcc is being dim */ + } +} + +int disorder_eclient_enable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "enable", (char *)0); +} + +int disorder_eclient_disable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "disable", (char *)0); +} + +int disorder_eclient_random_enable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "random-enable", (char *)0); +} + +int disorder_eclient_random_disable(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v){ + return simple(c, no_response_opcallback, (void (*)())completed, v, + "random-disable", (char *)0); +} + +int disorder_eclient_get(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, const char *pref, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "get", track, pref, (char *)0); +} + +int disorder_eclient_set(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + const char *value, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "set", track, pref, value, (char *)0); +} + +int disorder_eclient_unset(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "unset", track, pref, (char *)0); +} + +int disorder_eclient_resolve(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "resolve", track, (char *)0); +} + +int disorder_eclient_search(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *terms, + void *v) { + if(!split(terms, 0, SPLIT_QUOTES, 0, 0)) return -1; + return simple(c, list_response_opcallback, (void (*)())completed, v, + "search", terms, (char *)0); +} + +/* Log clients ***************************************************************/ + +int disorder_eclient_log(disorder_eclient *c, + const disorder_eclient_log_callbacks *callbacks, + void *v) { + if(c->log_callbacks) return -1; + c->log_callbacks = callbacks; + c->log_v = v; + stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v, + "log", (char *)0); + return 0; +} + +/* If we get here we've stopped being a log client */ +static void log_opcallback(disorder_eclient *c, + struct operation attribute((unused)) *op) { + D(("log_opcallback")); + c->log_callbacks = 0; + c->log_v = 0; +} + +/* error callback for log line parsing */ +static void logline_error(const char *msg, void *u) { + disorder_eclient *c = u; + protocol_error(c, c->ops, -1, "error parsing log line: %s", msg); +} + +/* process a single log line */ +static void logline(disorder_eclient *c, const char *line) { + int nvec, n; + char **vec; + uintmax_t when; + + D(("log_opcallback [%s]", line)); + vec = split(line, &nvec, SPLIT_QUOTES, logline_error, c); + if(nvec < 2) return; /* probably an error, already + * reported */ + if(sscanf(vec[0], "%"SCNxMAX, &when) != 1) { + /* probably the wrong side of a format change */ + protocol_error(c, c->ops, -1, "invalid log timestamp '%s'", vec[0]); + return; + } + /* TODO: do something with the time */ + n = TABLE_FIND(logentry_handlers, struct logentry_handler, name, vec[1]); + if(n < 0) return; /* probably a future command */ + vec += 2; + nvec -= 2; + if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max) + return; + logentry_handlers[n].handler(c, nvec, vec); +} + +static void logentry_completed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->completed) return; + c->log_callbacks->completed(c->log_v, vec[0]); +} + +static void logentry_failed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->failed)return; + c->log_callbacks->failed(c->log_v, vec[0], vec[1]); +} + +static void logentry_moved(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->moved) return; + c->log_callbacks->moved(c->log_v, vec[0]); +} + +static void logentry_playing(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->playing) return; + c->log_callbacks->playing(c->log_v, vec[0], vec[1]); +} + +static void logentry_queue(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + struct queue_entry *q; + + if(!c->log_callbacks->completed) return; + q = xmalloc(sizeof *q); + if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c)) + return; /* bogus */ + c->log_callbacks->queue(c->log_v, q); +} + +static void logentry_recent_added(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + struct queue_entry *q; + + if(!c->log_callbacks->recent_added) return; + q = xmalloc(sizeof *q); + if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c)) + return; /* bogus */ + c->log_callbacks->recent_added(c->log_v, q); +} + +static void logentry_recent_removed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->recent_removed) return; + c->log_callbacks->recent_removed(c->log_v, vec[0]); +} + +static void logentry_removed(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->removed) return; + c->log_callbacks->removed(c->log_v, vec[0], vec[1]); +} + +static void logentry_scratched(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + if(!c->log_callbacks->scratched) return; + c->log_callbacks->scratched(c->log_v, vec[0], vec[1]); +} + +static const struct { + unsigned long bit; + const char *enable; + const char *disable; +} statestrings[] = { + { DISORDER_PLAYING_ENABLED, "enable_play", "disable_play" }, + { DISORDER_RANDOM_ENABLED, "enable_random", "disable_random" }, + { DISORDER_TRACK_PAUSED, "pause", "resume" }, +}; +#define NSTATES (int)(sizeof states / sizeof *states) + +static void logentry_state(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + int n; + + for(n = 0; n < NSTATES; ++n) + if(!strcmp(vec[0], statestrings[n].enable)) { + c->statebits |= statestrings[n].bit; + break; + } else if(!strcmp(vec[0], statestrings[n].disable)) { + c->statebits &= ~statestrings[n].bit; + break; + } + if(!c->log_callbacks->state) return; + c->log_callbacks->state(c->log_v, c->statebits); +} + +static void logentry_volume(disorder_eclient *c, + int attribute((unused)) nvec, char **vec) { + long l, r; + + if(!c->log_callbacks->volume) return; + if(xstrtol(&l, vec[0], 0, 10) + || xstrtol(&r, vec[1], 0, 10) + || l < 0 || l > INT_MAX + || r < 0 || r > INT_MAX) + return; /* bogus */ + c->log_callbacks->volume(c->log_v, (int)l, (int)r); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:61ONz2p/LWaDRnToGI2+fg */ diff --git a/lib/eclient.h b/lib/eclient.h new file mode 100644 index 0000000..caeb81c --- /dev/null +++ b/lib/eclient.h @@ -0,0 +1,271 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef ECLIENT_H +#define ECLIENT_H + +/* Asynchronous client interface. You must provide disorder_client_poll(). */ + +typedef struct disorder_eclient disorder_eclient; + +struct queue_entry; + +#define DISORDER_POLL_READ 1u /* want to read FD */ +#define DISORDER_POLL_WRITE 2u /* want to write FD */ + +/* Callbacks for all clients. These must all be valid. */ +typedef struct disorder_eclient_callbacks { + void (*comms_error)(void *u, const char *msg); + /* Called when a communication error (e.g. connected refused) occurs. U + * comes from the _new() call and MSG describes the problem.*/ + + void (*protocol_error)(void *u, void *v, int code, const char *msg); + /* Called when a command fails (including initial authorization). U comes + * from the _new() call, V from the failed command or a null pointer if the + * error is in setup and MSG describes the problem. */ + + void (*poll)(void *u, disorder_eclient *c, int fd, unsigned mode); + /* Set poll/select flags for FD according to MODE. FD will never be -1. + * Before FD is closed, you will always get a call with MODE=0. U comes from + * the _new() call. */ + + void (*report)(void *u, const char *msg); + /* Called from time to time to report what's doing. Called with MSG=0 + * when the client goes idle.*/ +} disorder_eclient_callbacks; + +/* Callbacks for log clients. All of these are allowed to be a null pointers + * in which case you don't get told about that log event. */ +typedef struct disorder_eclient_log_callbacks { + void (*connected)(void *v); + /* Called on (re-)connection */ + + /* See disorder_protocol(5) for documentation for the rest */ + + void (*completed)(void *v, const char *track); + void (*failed)(void *v, const char *track, const char *status); + void (*moved)(void *v, const char *user); + void (*playing)(void *v, const char *track, const char *user/*maybe 0*/); + void (*queue)(void *v, struct queue_entry *q); + void (*recent_added)(void *v, struct queue_entry *q); + void (*recent_removed)(void *v, const char *id); + void (*removed)(void *v, const char *id, const char *user/*maybe 0*/); + void (*scratched)(void *v, const char *track, const char *user); + void (*state)(void *v, unsigned long state); + void (*volume)(void *v, int left, int right); +} disorder_eclient_log_callbacks; + +/* State bits */ +#define DISORDER_PLAYING_ENABLED 0x00000001 /* play is enabled */ +#define DISORDER_RANDOM_ENABLED 0x00000002 /* random play is enabled */ +#define DISORDER_TRACK_PAUSED 0x00000004 /* track is paused */ + +struct queue_entry; +struct kvp; +struct sink; + +/* Completion callbacks. These provide the result of operations to the caller. + * It is always allowed for these to be null pointers if you don't care about + * the result. */ + +typedef void disorder_eclient_no_response(void *v); +/* completion callback with no data */ + +typedef void disorder_eclient_string_response(void *v, const char *value); +/* completion callback with a string result */ + +typedef void disorder_eclient_integer_response(void *v, long value); +/* completion callback with a integer result */ + +typedef void disorder_eclient_volume_response(void *v, int l, int r); +/* completion callback with a pair of integer results */ + +typedef void disorder_eclient_queue_response(void *v, struct queue_entry *q); +/* completion callback for queue/recent listing */ + +typedef void disorder_eclient_list_response(void *v, int nvec, char **vec); +/* completion callback for file listing etc */ + +disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, + void *u); +/* Create a new client */ + +void disorder_eclient_close(disorder_eclient *c); +/* Close C */ + +void disorder_eclient_polled(disorder_eclient *c, unsigned mode); +/* Should be called when c's FD is readable and/or writable, and in any case + * from time to time (so that retries work). */ + +int disorder_eclient_version(disorder_eclient *c, + disorder_eclient_string_response *completed, + void *v); +/* fetch the server version */ + +int disorder_eclient_play(disorder_eclient *c, + const char *track, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_pause(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_resume(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* add a track to the queue */ + +int disorder_eclient_scratch(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v); +/* scratch a track by ID */ + +int disorder_eclient_scratch_playing(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v); +/* scratch the playing track whatever it is */ + +int disorder_eclient_remove(disorder_eclient *c, + const char *id, + disorder_eclient_no_response *completed, + void *v); +/* remove a track from the queue */ + +int disorder_eclient_moveafter(disorder_eclient *c, + const char *target, + int nids, + const char **ids, + disorder_eclient_no_response *completed, + void *v); +/* move tracks within the queue */ + +int disorder_eclient_playing(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* find the currently playing track (0 for none) */ + +int disorder_eclient_queue(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* list recently played tracks */ + +int disorder_eclient_recent(disorder_eclient *c, + disorder_eclient_queue_response *completed, + void *v); +/* list recently played tracks */ + +int disorder_eclient_files(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v); +/* list files in a directory, matching RE if not a null pointer */ + +int disorder_eclient_dirs(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *dir, + const char *re, + void *v); +/* list directories in a directory, matching RE if not a null pointer */ + +int disorder_eclient_namepart(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + const char *context, + const char *part, + void *v); +/* look up a track name part */ + +int disorder_eclient_length(disorder_eclient *c, + disorder_eclient_integer_response *completed, + const char *track, + void *v); +/* look up a track name length */ + +int disorder_eclient_volume(disorder_eclient *c, + disorder_eclient_volume_response *callback, + int l, int r, + void *v); +/* If L and R are both -ve gets the volume. + * If neither are -ve then sets the volume. + * Otherwise asserts! + */ + +int disorder_eclient_enable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_disable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_random_enable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +int disorder_eclient_random_disable(disorder_eclient *c, + disorder_eclient_no_response *callback, + void *v); +/* Enable/disable play/random play */ + +int disorder_eclient_resolve(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, + void *v); +/* Resolve aliases */ + +int disorder_eclient_log(disorder_eclient *c, + const disorder_eclient_log_callbacks *callbacks, + void *v); +/* Make this a log client (forever - it automatically becomes one again upon + * reconnection) */ + +int disorder_eclient_get(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *track, const char *pref, + void *v); +int disorder_eclient_set(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + const char *value, + void *v); +int disorder_eclient_unset(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *track, const char *pref, + void *v); +/* Get/set preference values */ + +int disorder_eclient_search(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *terms, + void *v); + +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:YrGF8JfZVK701dhnUAll8w */ diff --git a/lib/event.c b/lib/event.c new file mode 100644 index 0000000..b49b2e1 --- /dev/null +++ b/lib/event.c @@ -0,0 +1,844 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <fcntl.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/resource.h> +#include <sys/wait.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <errno.h> +#include <string.h> +#include <limits.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <stdio.h> +#include "event.h" +#include "mem.h" +#include "log.h" +#include "syscalls.h" +#include "printf.h" +#include "sink.h" + +struct timeout { + struct timeout *next; + struct timeval when; + ev_timeout_callback *callback; + void *u; + int resolve; +}; + +struct fd { + int fd; + ev_fd_callback *callback; + void *u; +}; + +struct fdmode { + fd_set enabled; + fd_set tripped; + int nfds, fdslots; + struct fd *fds; + int maxfd; +}; + +struct signal { + struct sigaction oldsa; + ev_signal_callback *callback; + void *u; +}; + +struct child { + pid_t pid; + int options; + ev_child_callback *callback; + void *u; +}; + +struct ev_source { + struct fdmode mode[ev_nmodes]; + struct timeout *timeouts; + struct signal signals[NSIG]; + sigset_t sigmask; + int escape; + int sigpipe[2]; + int nchildren, nchildslots; + struct child *children; +}; + +static const char *modenames[] = { "read", "write", "except" }; + +/* utilities ******************************************************************/ + +static inline int gt(const struct timeval *a, const struct timeval *b) { + if(a->tv_sec > b->tv_sec) + return 1; + if(a->tv_sec == b->tv_sec + && a->tv_usec > b->tv_usec) + return 1; + return 0; +} + +static inline int ge(const struct timeval *a, const struct timeval *b) { + return !gt(b, a); +} + +/* creation *******************************************************************/ + +ev_source *ev_new(void) { + ev_source *ev = xmalloc(sizeof *ev); + int n; + + memset(ev, 0, sizeof *ev); + for(n = 0; n < ev_nmodes; ++n) + FD_ZERO(&ev->mode[n].enabled); + ev->sigpipe[0] = ev->sigpipe[1] = -1; + sigemptyset(&ev->sigmask); + return ev; +} + +/* event loop *****************************************************************/ + +int ev_run(ev_source *ev) { + for(;;) { + struct timeval now; + struct timeval delta; + int n, mode; + int ret; + int maxfd; + struct timeout *t, **tt; + + xgettimeofday(&now, 0); + /* Handle timeouts. We don't want to handle any timeouts that are added + * while we're handling them (otherwise we'd have to break out of infinite + * loops, preferrably without starving better-behaved subsystems). Hence + * the slightly complicated two-phase approach here. */ + for(t = ev->timeouts; + t && ge(&now, &t->when); + t = t->next) { + t->resolve = 1; + D(("calling timeout for %ld.%ld callback %p %p", + (long)t->when.tv_sec, (long)t->when.tv_usec, + (void *)t->callback, t->u)); + ret = t->callback(ev, &now, t->u); + if(ret) + return ret; + } + tt = &ev->timeouts; + while((t = *tt)) { + if(t->resolve) + *tt = t->next; + else + tt = &t->next; + } + maxfd = 0; + for(mode = 0; mode < ev_nmodes; ++mode) { + ev->mode[mode].tripped = ev->mode[mode].enabled; + if(ev->mode[mode].maxfd > maxfd) + maxfd = ev->mode[mode].maxfd; + } + xsigprocmask(SIG_UNBLOCK, &ev->sigmask, 0); + do { + if(ev->timeouts) { + xgettimeofday(&now, 0); + delta.tv_sec = ev->timeouts->when.tv_sec - now.tv_sec; + delta.tv_usec = ev->timeouts->when.tv_usec - now.tv_usec; + if(delta.tv_usec < 0) { + delta.tv_usec += 1000000; + --delta.tv_sec; + } + if(delta.tv_sec < 0) + delta.tv_sec = delta.tv_usec = 0; + n = select(maxfd + 1, + &ev->mode[ev_read].tripped, + &ev->mode[ev_write].tripped, + &ev->mode[ev_except].tripped, + &delta); + } else { + n = select(maxfd + 1, + &ev->mode[ev_read].tripped, + &ev->mode[ev_write].tripped, + &ev->mode[ev_except].tripped, + 0); + } + } while(n < 0 && errno == EINTR); + xsigprocmask(SIG_BLOCK, &ev->sigmask, 0); + if(n < 0) { + error(errno, "error calling select"); + return -1; + } + if(n > 0) { + /* if anything deranges the meaning of an fd, or re-orders the + * fds[] tables, we'd better give up; such operations will + * therefore set @escape@. */ + ev->escape = 0; + for(mode = 0; mode < ev_nmodes && !ev->escape; ++mode) + for(n = 0; n < ev->mode[mode].nfds && !ev->escape; ++n) { + int fd = ev->mode[mode].fds[n].fd; + if(FD_ISSET(fd, &ev->mode[mode].tripped)) { + D(("calling %s fd %d callback %p %p", modenames[mode], fd, + (void *)ev->mode[mode].fds[n].callback, + ev->mode[mode].fds[n].u)); + ret = ev->mode[mode].fds[n].callback(ev, fd, + ev->mode[mode].fds[n].u); + if(ret) + return ret; + } + } + } + /* we'll pick up timeouts back round the loop */ + } +} + +/* file descriptors ***********************************************************/ + +int ev_fd(ev_source *ev, + ev_fdmode mode, + int fd, + ev_fd_callback *callback, + void *u) { + int n; + + D(("registering %s fd %d callback %p %p", modenames[mode], fd, + (void *)callback, u)); + assert(mode < ev_nmodes); + if(ev->mode[mode].nfds >= ev->mode[mode].fdslots) { + ev->mode[mode].fdslots = (ev->mode[mode].fdslots + ? 2 * ev->mode[mode].fdslots : 16); + D(("expanding %s fd table to %d entries", modenames[mode], + ev->mode[mode].fdslots)); + ev->mode[mode].fds = xrealloc(ev->mode[mode].fds, + ev->mode[mode].fdslots * sizeof (struct fd)); + } + n = ev->mode[mode].nfds++; + FD_SET(fd, &ev->mode[mode].enabled); + ev->mode[mode].fds[n].fd = fd; + ev->mode[mode].fds[n].callback = callback; + ev->mode[mode].fds[n].u = u; + if(fd > ev->mode[mode].maxfd) + ev->mode[mode].maxfd = fd; + ev->escape = 1; + return 0; +} + +int ev_fd_cancel(ev_source *ev, ev_fdmode mode, int fd) { + int n; + int maxfd; + + D(("cancelling mode %s fd %d", modenames[mode], fd)); + /* find the right struct fd */ + for(n = 0; n < ev->mode[mode].nfds && fd != ev->mode[mode].fds[n].fd; ++n) + ; + assert(n < ev->mode[mode].nfds); + /* swap in the last fd and reduce the count */ + if(n != ev->mode[mode].nfds - 1) + ev->mode[mode].fds[n] = ev->mode[mode].fds[ev->mode[mode].nfds - 1]; + --ev->mode[mode].nfds; + /* if that was the biggest fd, find the new biggest one */ + if(fd == ev->mode[mode].maxfd) { + maxfd = 0; + for(n = 0; n < ev->mode[mode].nfds; ++n) + if(ev->mode[mode].fds[n].fd > maxfd) + maxfd = ev->mode[mode].fds[n].fd; + ev->mode[mode].maxfd = maxfd; + } + /* don't tell select about this fd any more */ + FD_CLR(fd, &ev->mode[mode].enabled); + ev->escape = 1; + return 0; +} + +int ev_fd_enable(ev_source *ev, ev_fdmode mode, int fd) { + D(("enabling mode %s fd %d", modenames[mode], fd)); + FD_SET(fd, &ev->mode[mode].enabled); + return 0; +} + +int ev_fd_disable(ev_source *ev, ev_fdmode mode, int fd) { + D(("disabling mode %s fd %d", modenames[mode], fd)); + FD_CLR(fd, &ev->mode[mode].enabled); + FD_CLR(fd, &ev->mode[mode].tripped); + return 0; +} + +/* timeouts *******************************************************************/ + +int ev_timeout(ev_source *ev, + ev_timeout_handle *handlep, + const struct timeval *when, + ev_timeout_callback *callback, + void *u) { + struct timeout *t, *p, **pp; + + D(("registering timeout at %ld.%ld callback %p %p", + when ? (long)when->tv_sec : 0, when ? (long)when->tv_usec : 0, + (void *)callback, u)); + t = xmalloc(sizeof *t); + if(when) + t->when = *when; + t->callback = callback; + t->u = u; + pp = &ev->timeouts; + while((p = *pp) && gt(&t->when, &p->when)) + pp = &p->next; + t->next = p; + *pp = t; + if(handlep) + *handlep = t; + return 0; +} + +int ev_timeout_cancel(ev_source *ev, + ev_timeout_handle handle) { + struct timeout *t = handle, *p, **pp; + + for(pp = &ev->timeouts; (p = *pp) && p != t; pp = &p->next) + ; + if(p) { + *pp = p->next; + return 0; + } else + return -1; +} + +/* signals ********************************************************************/ + +static int sigfd[NSIG]; + +static void sighandler(int s) { + unsigned char sc = s; + static const char errmsg[] = "error writing to signal pipe"; + + /* probably the reader has stopped listening for some reason */ + if(write(sigfd[s], &sc, 1) < 0) { + write(2, errmsg, sizeof errmsg - 1); + abort(); + } +} + +static int signal_read(ev_source *ev, + int attribute((unused)) fd, + void attribute((unused)) *u) { + unsigned char s; + int n; + int ret; + + if((n = read(ev->sigpipe[0], &s, 1)) == 1) + if((ret = ev->signals[s].callback(ev, s, ev->signals[s].u))) + return ret; + assert(n != 0); + if(n < 0 && (errno != EINTR && errno != EAGAIN)) { + error(errno, "error reading from signal pipe %d", ev->sigpipe[0]); + return -1; + } + return 0; +} + +static void close_sigpipe(ev_source *ev) { + int save_errno = errno; + + xclose(ev->sigpipe[0]); + xclose(ev->sigpipe[1]); + ev->sigpipe[0] = ev->sigpipe[1] = -1; + errno = save_errno; +} + +int ev_signal(ev_source *ev, + int sig, + ev_signal_callback *callback, + void *u) { + int n; + struct sigaction sa; + + D(("registering signal %d handler callback %p %p", sig, (void *)callback, u)); + assert(sig > 0); + assert(sig < NSIG); + assert(sig <= UCHAR_MAX); + if(ev->sigpipe[0] == -1) { + D(("creating signal pipe")); + xpipe(ev->sigpipe); + D(("signal pipe is %d, %d", ev->sigpipe[0], ev->sigpipe[1])); + for(n = 0; n < 2; ++n) { + nonblock(ev->sigpipe[n]); + cloexec(ev->sigpipe[n]); + } + if(ev_fd(ev, ev_read, ev->sigpipe[0], signal_read, 0)) { + close_sigpipe(ev); + return -1; + } + } + sigaddset(&ev->sigmask, sig); + xsigprocmask(SIG_BLOCK, &ev->sigmask, 0); + sigfd[sig] = ev->sigpipe[1]; + ev->signals[sig].callback = callback; + ev->signals[sig].u = u; + sa.sa_handler = sighandler; + sigfillset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + xsigaction(sig, &sa, &ev->signals[sig].oldsa); + ev->escape = 1; + return 0; +} + +int ev_signal_cancel(ev_source *ev, + int sig) { + sigset_t ss; + + xsigaction(sig, &ev->signals[sig].oldsa, 0); + ev->signals[sig].callback = 0; + ev->escape = 1; + sigdelset(&ev->sigmask, sig); + sigemptyset(&ss); + sigaddset(&ss, sig); + xsigprocmask(SIG_UNBLOCK, &ss, 0); + return 0; +} + +void ev_signal_atfork(ev_source *ev) { + int sig; + + if(ev->sigpipe[0] != -1) { + /* revert any handled signals to their original state */ + for(sig = 1; sig < NSIG; ++sig) { + if(ev->signals[sig].callback != 0) + xsigaction(sig, &ev->signals[sig].oldsa, 0); + } + /* and then unblock them */ + xsigprocmask(SIG_UNBLOCK, &ev->sigmask, 0); + /* don't want a copy of the signal pipe open inside the fork */ + xclose(ev->sigpipe[0]); + xclose(ev->sigpipe[1]); + } +} + +/* child processes ************************************************************/ + +static int sigchld_callback(ev_source *ev, + int attribute((unused)) sig, + void attribute((unused)) *u) { + struct rusage ru; + pid_t r; + int status, n, ret, revisit; + + do { + revisit = 0; + for(n = 0; n < ev->nchildren; ++n) { + r = wait4(ev->children[n].pid, + &status, + ev->children[n].options | WNOHANG, + &ru); + if(r > 0) { + ev_child_callback *c = ev->children[n].callback; + void *cu = ev->children[n].u; + + if(WIFEXITED(status) || WIFSIGNALED(status)) + ev_child_cancel(ev, r); + revisit = 1; + if((ret = c(ev, r, status, &ru, cu))) + return ret; + } else if(r < 0) { + /* We should "never" get an ECHILD but it can in fact happen. For + * instance on Linux 2.4.31, and probably other versions, if someone + * straces a child process and then a different child process + * terminates, when we wait4() the trace process we will get ECHILD + * because it has been reparented to strace. Obviously this is a + * hopeless design flaw in the tracing infrastructure, but we don't + * want the disorder server to bomb out because of it. So we just log + * the problem and ignore it. + */ + error(errno, "error calling wait4 for PID %lu (broken ptrace?)", + (unsigned long)ev->children[n].pid); + if(errno != ECHILD) + return -1; + } + } + } while(revisit); + return 0; +} + +int ev_child_setup(ev_source *ev) { + D(("installing SIGCHLD handler")); + return ev_signal(ev, SIGCHLD, sigchld_callback, 0); +} + +int ev_child(ev_source *ev, + pid_t pid, + int options, + ev_child_callback *callback, + void *u) { + int n; + + D(("registering child handling %ld options %d callback %p %p", + (long)pid, options, (void *)callback, u)); + assert(ev->signals[SIGCHLD].callback == sigchld_callback); + if(ev->nchildren >= ev->nchildslots) { + ev->nchildslots = ev->nchildslots ? 2 * ev->nchildslots : 16; + ev->children = xrealloc(ev->children, + ev->nchildslots * sizeof (struct child)); + } + n = ev->nchildren++; + ev->children[n].pid = pid; + ev->children[n].options = options; + ev->children[n].callback = callback; + ev->children[n].u = u; + return 0; +} + +int ev_child_cancel(ev_source *ev, + pid_t pid) { + int n; + + for(n = 0; n < ev->nchildren && ev->children[n].pid != pid; ++n) + ; + assert(n < ev->nchildren); + if(n != ev->nchildren - 1) + ev->children[n] = ev->children[ev->nchildren - 1]; + --ev->nchildren; + return 0; +} + +/* socket listeners ***********************************************************/ + +struct listen_state { + ev_listen_callback *callback; + void *u; +}; + +static int listen_callback(ev_source *ev, int fd, void *u) { + const struct listen_state *l = u; + int newfd; + union { + struct sockaddr_in in; +#if HAVE_STRUCT_SOCKADDR_IN6 + struct sockaddr_in6 in6; +#endif + struct sockaddr_un un; + struct sockaddr sa; + } addr; + socklen_t addrlen; + int ret; + + D(("callback for listener fd %d", fd)); + while((addrlen = sizeof addr), + (newfd = accept(fd, &addr.sa, &addrlen)) >= 0) { + if((ret = l->callback(ev, newfd, &addr.sa, addrlen, l->u))) + return ret; + } + switch(errno) { + case EINTR: + case EAGAIN: + break; +#ifdef ECONNABORTED + case ECONNABORTED: + error(errno, "error calling accept"); + break; +#endif +#ifdef EPROTO + case EPROTO: + /* XXX on some systems EPROTO should be fatal, but we don't know if + * we're running on one of them */ + error(errno, "error calling accept"); + break; +#endif + default: + fatal(errno, "error calling accept"); + break; + } + if(errno != EINTR && errno != EAGAIN) + error(errno, "error calling accept"); + return 0; +} + +int ev_listen(ev_source *ev, + int fd, + ev_listen_callback *callback, + void *u) { + struct listen_state *l = xmalloc(sizeof *l); + + D(("registering listener fd %d callback %p %p", fd, (void *)callback, u)); + l->callback = callback; + l->u = u; + return ev_fd(ev, ev_read, fd, listen_callback, l); +} + +int ev_listen_cancel(ev_source *ev, int fd) { + D(("cancelling listener fd %d", fd)); + return ev_fd_cancel(ev, ev_read, fd); +} + +/* buffer *********************************************************************/ + +struct buffer { + char *base, *start, *end, *top; +}; + +/* make sure there is @bytes@ available at @b->end@ */ +static void buffer_space(struct buffer *b, size_t bytes) { + D(("buffer_space %p %p %p %p want %lu", + (void *)b->base, (void *)b->start, (void *)b->end, (void *)b->top, + (unsigned long)bytes)); + if(b->start == b->end) + b->start = b->end = b->base; + if((size_t)(b->top - b->end) < bytes) { + if((size_t)((b->top - b->end) + (b->start - b->base)) < bytes) { + size_t newspace = b->end - b->start + bytes, n; + char *newbase; + + for(n = 16; n < newspace; n *= 2) + ; + newbase = xmalloc_noptr(n); + memcpy(newbase, b->start, b->end - b->start); + b->base = newbase; + b->end = newbase + (b->end - b->start); + b->top = newbase + n; + b->start = newbase; /* must be last */ + } else { + memmove(b->base, b->start, b->end - b->start); + b->end = b->base + (b->end - b->start); + b->start = b->base; + } + } + D(("result %p %p %p %p", + (void *)b->base, (void *)b->start, (void *)b->end, (void *)b->top)); +} + +/* buffered writer ************************************************************/ + +struct ev_writer { + struct sink s; + struct buffer b; + int fd; + int eof; + ev_error_callback *callback; + void *u; + ev_source *ev; +}; + +static int writer_callback(ev_source *ev, int fd, void *u) { + ev_writer *w = u; + int n; + + n = write(fd, w->b.start, w->b.end - w->b.start); + D(("callback for writer fd %d, %ld bytes, n=%d, errno=%d", + fd, (long)(w->b.end - w->b.start), n, errno)); + if(n >= 0) { + w->b.start += n; + if(w->b.start == w->b.end) { + if(w->eof) { + ev_fd_cancel(ev, ev_write, fd); + return w->callback(ev, fd, 0, w->u); + } else + ev_fd_disable(ev, ev_write, fd); + } + } else { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + ev_fd_cancel(ev, ev_write, fd); + return w->callback(ev, fd, errno, w->u); + } + } + return 0; +} + +static int ev_writer_write(struct sink *sk, const void *s, int n) { + ev_writer *w = (ev_writer *)sk; + + buffer_space(&w->b, n); + if(w->b.start == w->b.end) + ev_fd_enable(w->ev, ev_write, w->fd); + memcpy(w->b.end, s, n); + w->b.end += n; + return 0; +} + +ev_writer *ev_writer_new(ev_source *ev, + int fd, + ev_error_callback *callback, + void *u) { + ev_writer *w = xmalloc(sizeof *w); + + D(("registering writer fd %d callback %p %p", fd, (void *)callback, u)); + w->s.write = ev_writer_write; + w->fd = fd; + w->callback = callback; + w->u = u; + w->ev = ev; + if(ev_fd(ev, ev_write, fd, writer_callback, w)) + return 0; + ev_fd_disable(ev, ev_write, fd); + return w; +} + +struct sink *ev_writer_sink(ev_writer *w) { + return &w->s; +} + +static int writer_shutdown(ev_source *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_writer *w = u; + + return w->callback(ev, w->fd, 0, w->u); +} + +int ev_writer_close(ev_writer *w) { + D(("close writer fd %d", w->fd)); + w->eof = 1; + if(w->b.start == w->b.end) { + /* we're already finished */ + ev_fd_cancel(w->ev, ev_write, w->fd); + return ev_timeout(w->ev, 0, 0, writer_shutdown, w); + } + return 0; +} + +int ev_writer_cancel(ev_writer *w) { + D(("cancel writer fd %d", w->fd)); + return ev_fd_cancel(w->ev, ev_write, w->fd); +} + +int ev_writer_flush(ev_writer *w) { + return writer_callback(w->ev, w->fd, w); +} + +/* buffered reader ************************************************************/ + +struct ev_reader { + struct buffer b; + int fd; + ev_reader_callback *callback; + ev_error_callback *error_callback; + void *u; + ev_source *ev; + int eof; +}; + +static int reader_callback(ev_source *ev, int fd, void *u) { + ev_reader *r = u; + int n; + + buffer_space(&r->b, 1); + n = read(fd, r->b.end, r->b.top - r->b.end); + D(("read fd %d buffer %d returned %d errno %d", + fd, (int)(r->b.top - r->b.end), n, errno)); + if(n > 0) { + r->b.end += n; + return r->callback(ev, r, fd, r->b.start, r->b.end - r->b.start, 0, r->u); + } else if(n == 0) { + r->eof = 1; + ev_fd_cancel(ev, ev_read, fd); + return r->callback(ev, r, fd, r->b.start, r->b.end - r->b.start, 1, r->u); + } else { + switch(errno) { + case EINTR: + case EAGAIN: + break; + default: + ev_fd_cancel(ev, ev_read, fd); + return r->error_callback(ev, fd, errno, r->u); + } + } + return 0; +} + +ev_reader *ev_reader_new(ev_source *ev, + int fd, + ev_reader_callback *callback, + ev_error_callback *error_callback, + void *u) { + ev_reader *r = xmalloc(sizeof *r); + + D(("registering reader fd %d callback %p %p %p", + fd, (void *)callback, (void *)error_callback, u)); + r->fd = fd; + r->callback = callback; + r->error_callback = error_callback; + r->u = u; + r->ev = ev; + if(ev_fd(ev, ev_read, fd, reader_callback, r)) + return 0; + return r; +} + +void ev_reader_buffer(ev_reader *r, size_t nbytes) { + buffer_space(&r->b, nbytes - (r->b.end - r->b.start)); +} + +void ev_reader_consume(ev_reader *r, size_t n) { + r->b.start += n; +} + +int ev_reader_cancel(ev_reader *r) { + D(("cancel reader fd %d", r->fd)); + return ev_fd_cancel(r->ev, ev_read, r->fd); +} + +int ev_reader_disable(ev_reader *r) { + D(("disable reader fd %d", r->fd)); + return r->eof ? 0 : ev_fd_disable(r->ev, ev_read, r->fd); +} + +static int reader_continuation(ev_source attribute((unused)) *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_reader *r = u; + + D(("reader continuation callback fd %d", r->fd)); + if(ev_fd_enable(r->ev, ev_read, r->fd)) return -1; + return r->callback(ev, r, r->fd, r->b.start, r->b.end - r->b.start, r->eof, r->u); +} + +int ev_reader_incomplete(ev_reader *r) { + if(ev_fd_disable(r->ev, ev_read, r->fd)) return -1; + return ev_timeout(r->ev, 0, 0, reader_continuation, r); +} + +static int reader_enabled(ev_source *ev, + const attribute((unused)) struct timeval *now, + void *u) { + ev_reader *r = u; + + D(("reader enabled callback fd %d", r->fd)); + return r->callback(ev, r, r->fd, r->b.start, r->b.end - r->b.start, r->eof, r->u); +} + +int ev_reader_enable(ev_reader *r) { + D(("enable reader fd %d", r->fd)); + return ((r->eof ? 0 : ev_fd_enable(r->ev, ev_read, r->fd)) + || ev_timeout(r->ev, 0, 0, reader_enabled, r)) ? -1 : 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:a81dd5068039481faac3eea28c995570 */ diff --git a/lib/event.h b/lib/event.h new file mode 100644 index 0000000..32bc1d4 --- /dev/null +++ b/lib/event.h @@ -0,0 +1,248 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef EVENT_H +#define EVENT_H + +typedef struct ev_source ev_source; + +struct rusage; +struct sink; + +ev_source *ev_new(void); +/* create a new event loop */ + +int ev_run(ev_source *ev); +/* run an event loop. If any callback returns nonzero then that value + * is returned. If an error occurs then -1 is returned and @error@ is + * called. */ + +/* file descriptors ***********************************************************/ + +typedef enum { + ev_read, + ev_write, + ev_except, + + ev_nmodes +} ev_fdmode; + +typedef int ev_fd_callback(ev_source *ev, int fd, void *u); +/* signature for fd callback functions */ + +int ev_fd(ev_source *ev, + ev_fdmode mode, + int fd, + ev_fd_callback *callback, + void *u); +/* register a callback on a file descriptor */ + +int ev_fd_cancel(ev_source *ev, + ev_fdmode mode, + int fd); +/* cancel a callback on a file descriptor */ + +int ev_fd_disable(ev_source *ev, + ev_fdmode mode, + int fd); +/* temporarily disable callbacks on a file descriptor */ + +int ev_fd_enable(ev_source *ev, + ev_fdmode mode, + int fd); +/* re-enable callbacks on a file descriptor */ + +/* timeouts *******************************************************************/ + +typedef int ev_timeout_callback(ev_source *ev, + const struct timeval *now, + void *u); +/* signature for timeout callback functions */ + +typedef void *ev_timeout_handle; + +int ev_timeout(ev_source *ev, + ev_timeout_handle *handlep, + const struct timeval *when, + ev_timeout_callback *callback, + void *u); +/* register a timeout callback. If @handlep@ is not a null pointer then a + * handle suitable for ev_timeout_cancel() below is returned through it. */ + +int ev_timeout_cancel(ev_source *ev, + ev_timeout_handle handle); +/* cancel a timeout callback */ + +/* signals ********************************************************************/ + +typedef int ev_signal_callback(ev_source *ev, + int sig, + void *u); +/* signature for signal callback functions */ + +int ev_signal(ev_source *ev, + int sig, + ev_signal_callback *callback, + void *u); +/* register a signal callback */ + +int ev_signal_cancel(ev_source *ev, + int sig); +/* cancel a signal callback */ + +void ev_signal_atfork(ev_source *ev); +/* unhandle and unblock handled signals - call after calling fork and + * then setting @exitfn@ */ + +/* child processes ************************************************************/ + +typedef int ev_child_callback(ev_source *ev, + pid_t pid, + int status, + const struct rusage *rusage, + void *u); +/* signature for child wait callbacks */ + +int ev_child_setup(ev_source *ev); +/* must be called exactly once before @ev_child@ */ + +int ev_child(ev_source *ev, + pid_t pid, + int options, + ev_child_callback *callback, + void *u); +/* register a child callback. @options@ must be 0 or WUNTRACED. */ + +int ev_child_cancel(ev_source *ev, + pid_t pid); +/* cancel a child callback. */ + +/* socket listeners ***********************************************************/ + +typedef int ev_listen_callback(ev_source *ev, + int newfd, + const struct sockaddr *remote, + socklen_t rlen, + void *u); +/* callback when a connection arrives. */ + +int ev_listen(ev_source *ev, + int fd, + ev_listen_callback *callback, + void *u); +/* register a socket listener callback. @bind@ and @listen@ should + * already have been called. */ + +int ev_listen_cancel(ev_source *ev, + int fd); +/* cancel a socket listener callback */ + +/* buffered writer ************************************************************/ + +typedef struct ev_writer ev_writer; + +typedef int ev_error_callback(ev_source *ev, + int fd, + int errno_value, + void *u); +/* called when an error occurs on a writer. Called with @errno_value@ + * of 0 when finished. */ + +ev_writer *ev_writer_new(ev_source *ev, + int fd, + ev_error_callback *callback, + void *u); +/* create a new buffered writer, writing to @fd@. Calls @error@ if an + * error occurs. */ + +int ev_writer_close(ev_writer *w); +/* close a writer (i.e. promise not to write to it any more) */ + +int ev_writer_cancel(ev_writer *w); +/* cancel a writer */ + +int ev_writer_flush(ev_writer *w); +/* attempt to flush the buffer */ + +struct sink *ev_writer_sink(ev_writer *w) attribute((const)); +/* return a sink for the writer - use this to actually write to it */ + +/* buffered reader ************************************************************/ + +typedef struct ev_reader ev_reader; + +typedef int ev_reader_callback(ev_source *ev, + ev_reader *reader, + int fd, + void *ptr, + size_t bytes, + int eof, + void *u); +/* Called when data is read or an error occurs. @ptr@ and @bytes@ + * indicate the amount of data available. @eof@ will be 1 at eof. */ + +ev_reader *ev_reader_new(ev_source *ev, + int fd, + ev_reader_callback *callback, + ev_error_callback *error_callback, + void *u); +/* register a new reader. @callback@ will be called whenever data is + * available. */ + +void ev_reader_buffer(ev_reader *r, size_t nbytes); +/* specify a buffer size *case */ + +void ev_reader_consume(ev_reader *r, size_t nbytes); +/* consume @nbytes@ bytes. */ + +int ev_reader_cancel(ev_reader *r); +/* cancel a reader */ + +int ev_reader_disable(ev_reader *r); +/* disable reading */ + +int ev_reader_incomplete(ev_reader *r); +/* callback didn't fully process buffer, but would like another + * callback (used where processing more would block too long) */ + +int ev_reader_enable(ev_reader *r); +/* enable reading. If there is unconsumed data then you get a + * callback next time round the event loop even if nothing new has + * been read. + * + * The idea is in your read callback you come across a line (or + * whatever) that can't be processed immediately. So you set up + * processing and disable reading. Later when you finish processing + * you re-enable. You'll automatically get another callback pretty + * much direct from the event loop (not from inside ev_reader_enable) + * so you can handle the next line (or whatever) if the whole thing + * has in fact already arrived. + */ + +#endif /* EVENT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:8e6f230cabf206361c14897f1e03b536 */ diff --git a/lib/eventlog.c b/lib/eventlog.c new file mode 100644 index 0000000..c374e4a --- /dev/null +++ b/lib/eventlog.c @@ -0,0 +1,93 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "mem.h" +#include "vector.h" +#include "printf.h" +#include "eventlog.h" +#include "split.h" + +static struct eventlog_output *outputs; + +void eventlog_add(struct eventlog_output *lo) { + lo->next = outputs; + outputs = lo; +} + +void eventlog_remove(struct eventlog_output *lo) { + struct eventlog_output *p, **pp; + + for(pp = &outputs; (p = *pp) && p != lo; pp = &p->next) + ; + if(p == lo) + *pp = lo->next; +} + +static void veventlog(const char *keyword, const char *raw, va_list ap) { + struct eventlog_output *p; + struct dynstr d; + const char *param; + + dynstr_init(&d); + dynstr_append_string(&d, keyword); + while((param = va_arg(ap, const char *))) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, quoteutf8(param)); + } + if(raw) { + dynstr_append(&d, ' '); + dynstr_append_string(&d, raw); + } + dynstr_terminate(&d); + for(p = outputs; p; p = p->next) + p->fn(d.vec, p->user); +} + +void eventlog_raw(const char *keyword, const char *raw, ...) { + va_list ap; + + va_start(ap, raw); + veventlog(keyword, raw, ap); + va_end(ap); +} + +void eventlog(const char *keyword, ...) { + va_list ap; + + va_start(ap, keyword); + veventlog(keyword, 0, ap); + va_end(ap); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:Io9E4IlqSMg7Kum3sq080Q */ diff --git a/lib/eventlog.h b/lib/eventlog.h new file mode 100644 index 0000000..102956e --- /dev/null +++ b/lib/eventlog.h @@ -0,0 +1,51 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef EVENTLOG_H +#define EVENTLOG_H + +/* definition of an event log output. The caller must allocate these + * (since log.c isn't allowed to perform memory allocation). */ +struct eventlog_output { + struct eventlog_output *next; + void (*fn)(const char *msg, void *user); + void *user; +}; + +void eventlog_add(struct eventlog_output *lo); +/* add a log output */ + +void eventlog_remove(struct eventlog_output *lo); +/* remove a log output */ + +void eventlog(const char *keyword, ...); +void eventlog_raw(const char *keyword, const char *raw, ...); +/* send a message to the event log */ + +#endif /* EVENTLOG_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:CdZJyFnrtJFEjyo4+7W3cA */ diff --git a/lib/filepart.c b/lib/filepart.c new file mode 100644 index 0000000..575cbc5 --- /dev/null +++ b/lib/filepart.c @@ -0,0 +1,71 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "filepart.h" +#include "mem.h" + +char *d_dirname(const char *path) { + const char *s; + + if((s = strrchr(path, '/'))) { + if(s == path) + return xstrdup("/"); + else + return xstrndup(path, s - path); + } else + return xstrdup("."); +} + +static const char *find_extension(const char *path) { + const char *q = path + strlen(path); + + while(q > path && strchr("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789", *q)) + --q; + return *q == '.' ? q : 0; +} + +const char *strip_extension(const char *path) { + const char *q = find_extension(path); + + return q ? xstrndup(path, q - path) : path; +} + +const char *extension(const char *path) { + const char *q = find_extension(path); + + return q ? q : ""; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:4WKGtvPyLNQ6h3I0n2cMIg */ diff --git a/lib/filepart.h b/lib/filepart.h new file mode 100644 index 0000000..3e616d4 --- /dev/null +++ b/lib/filepart.h @@ -0,0 +1,43 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef FILEPART_H +#define FILEPART_H + +char *d_dirname(const char *path); +/* return the directory name part of @path@ */ + +const char *strip_extension(const char *path); +/* return a filename with the extension stripped */ + +const char *extension(const char *path); +/* return just the extension, or "" */ + +#endif /* FILEPART_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:3bMQUaQm+LrL1Lr12rjgfw */ diff --git a/lib/fprintf.c b/lib/fprintf.c new file mode 100644 index 0000000..f86ede3 --- /dev/null +++ b/lib/fprintf.c @@ -0,0 +1,52 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +int byte_vfprintf(FILE *fp, const char *fmt, va_list ap) { + return byte_vsinkprintf(sink_stdio(0, fp), fmt, ap); +} + +int byte_fprintf(FILE *fp, const char *fmt, ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vfprintf(fp, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:40b957f66080ab957a2f7ca2d963a12f */ diff --git a/lib/hash.c b/lib/hash.c new file mode 100644 index 0000000..2fc19c1 --- /dev/null +++ b/lib/hash.c @@ -0,0 +1,174 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> + +#include "hash.h" +#include "mem.h" +#include "log.h" + +struct entry { + struct entry *next; /* next entry same key */ + size_t h; /* hash of KEY */ + const char *key; /* key of this entry */ + void *value; /* value of this entry */ +}; + +struct hash { + size_t nslots; /* number of slots */ + size_t nitems; /* total number of entries */ + struct entry **slots; /* table of slots */ + size_t valuesize; /* size of a value */ +}; + +static size_t hashfn(const char *key) { + size_t i = 0; + + while(*key) + i = 33 * i + (unsigned char)*key++; + return i; +} + +static void grow(hash *h) { + size_t n, newnslots; + struct entry **newslots, *e, *f; + + /* Allocate a new, larger array */ + newnslots = 2 * h->nslots; + newslots = xcalloc(newnslots, sizeof (struct entry *)); + /* Copy everything to it */ + for(n = 0; n < h->nslots; ++n) { + for(e = h->slots[n]; e; e = f) { + f = e->next; + e->next = newslots[e->h & (newnslots - 1)]; + newslots[e->h & (newnslots - 1)] = e; + } + } + h->slots = newslots; + h->nslots = newnslots; +} + +hash *hash_new(size_t valuesize) { + hash *h = xmalloc(sizeof *h); + + h->nslots = 256; + h->slots = xcalloc(h->nslots, sizeof (struct slot *)); + h->valuesize = valuesize; + return h; +} + +int hash_add(hash *h, const char *key, const void *value, int mode) { + size_t n = hashfn(key); + struct entry *e; + + for(e = h->slots[n & (h->nslots - 1)]; e; e = e->next) + if(e->h == n || !strcmp(e->key, key)) + break; + if(e) { + /* This key is already present. */ + if(mode == HASH_INSERT) return -1; + if(value) memcpy(e->value, value, h->valuesize); + return 0; + } else { + /* This key is absent. */ + if(mode == HASH_REPLACE) return -1; + if(h->nitems >= h->nslots) /* bound mean chain length */ + grow(h); + e = xmalloc(sizeof *e); + e->next = h->slots[n & (h->nslots - 1)]; + e->h = n; + e->key = xstrdup(key); + e->value = xmalloc(h->valuesize); + if(value) memcpy(e->value, value, h->valuesize); + h->slots[n & (h->nslots - 1)] = e; + ++h->nitems; + return 0; + } +} + +int hash_remove(hash *h, const char *key) { + size_t n = hashfn(key); + struct entry *e, **ee; + + for(ee = &h->slots[n & (h->nslots - 1)]; (e = *ee); ee = &e->next) + if(e->h == n || !strcmp(e->key, key)) + break; + if(e) { + *ee = e->next; + --h->nitems; + return 0; + } else + return -1; +} + +void *hash_find(hash *h, const char *key) { + size_t n = hashfn(key); + struct entry *e; + + for(e = h->slots[n & (h->nslots - 1)]; e; e = e->next) + if(e->h == n || !strcmp(e->key, key)) + return e->value; + return 0; +} + +int hash_foreach(hash *h, + int (*callback)(const char *key, void *value, void *u), + void *u) { + size_t n; + int ret; + struct entry *e, *f; + + for(n = 0; n < h->nslots; ++n) + for(e = h->slots[n]; e; e = f) { + f = e->next; + if((ret = callback(e->key, e->value, u))) + return ret; + } + return 0; +} + +size_t hash_count(hash *h) { + return h->nitems; +} + +char **hash_keys(hash *h) { + size_t n; + char **vec = xcalloc(h->nitems + 1, sizeof (char *)), **vp = vec; + struct entry *e; + + for(n = 0; n < h->nslots; ++n) + for(e = h->slots[n]; e; e = e->next) + *vp++ = (char *)e->key; + *vp = 0; + return vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:8JLNu2iwue7D0MlS0/nR2g */ diff --git a/lib/hash.h b/lib/hash.h new file mode 100644 index 0000000..d8698f0 --- /dev/null +++ b/lib/hash.h @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef HASH_H +#define HASH_H + +typedef struct hash hash; + +hash *hash_new(size_t valuesize); +/* Create a new hash */ + +int hash_add(hash *h, const char *key, const void *value, int mode); +#define HASH_INSERT 0 +#define HASH_REPLACE 1 +#define HASH_INSERT_OR_REPLACE 2 +/* Insert/replace a value in the hash. Returns 0 on success, -1 on + * error. */ + +int hash_remove(hash *h, const char *key); +/* Remove a value in the hash. Returns 0 on success, -1 on error. */ + +void *hash_find(hash *h, const char *key); +/* Find a value in the hash. Returns a null pointer if not found. */ + +int hash_foreach(hash *h, + int (*callback)(const char *key, void *value, void *u), + void *u); +/* Visit all the elements in a hash in any old order. It's safe to remove + * items from inside the callback including the visited one. It is not safe to + * add items from inside the callback however. + * + * If the callback ever returns non-0 then that value is immediately returned. + * Otherwise the return value is 0. + */ + +size_t hash_count(hash *h); +/* Return the number of items in the hash */ + +char **hash_keys(hash *h); +/* Return all the keys of H */ + +#endif /* HASH_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:/YrjtmzK8ADmZ6iZKmX3og */ diff --git a/lib/hex.c b/lib/hex.c new file mode 100644 index 0000000..d2a2a00 --- /dev/null +++ b/lib/hex.c @@ -0,0 +1,94 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> + +#include "hex.h" +#include "mem.h" +#include "log.h" + +char *hex(const uint8_t *ptr, size_t n) { + char *buf = xmalloc_noptr(n * 2 + 1), *p = buf; + + while(n-- > 0) + p += sprintf(p, "%02x", (unsigned)*ptr++); + return buf; +} + +int unhexdigitq(int c) { + switch(c) { + case '0': return 0; + case '1': return 1; + case '2': return 2; + case '3': return 3; + case '4': return 4; + case '5': return 5; + case '6': return 6; + case '7': return 7; + case '8': return 8; + case '9': return 9; + case 'a': case 'A': return 10; + case 'b': case 'B': return 11; + case 'c': case 'C': return 12; + case 'd': case 'D': return 13; + case 'e': case 'E': return 14; + case 'f': case 'F': return 15; + default: return -1; + } +} + +int unhexdigit(int c) { + int d; + + if((d = unhexdigitq(c)) < 0) error(0, "invalid hex digit"); + return d; +} + +uint8_t *unhex(const char *s, size_t *np) { + size_t l; + uint8_t *buf, *p; + int d1, d2; + + if((l = strlen(s)) & 1) { + error(0, "hex string has odd length"); + return 0; + } + p = buf = xmalloc_noptr(l / 2); + while(*s) { + if((d1 = unhexdigit(*s++)) < 0) return 0; + if((d2 = unhexdigit(*s++)) < 0) return 0; + *p++ = d1 * 16 + d2; + } + if(np) + *np = l / 2; + return buf; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:3d2adfde608c6d54dc2cf2b42d78e462 */ diff --git a/lib/hex.h b/lib/hex.h new file mode 100644 index 0000000..c6f9c96 --- /dev/null +++ b/lib/hex.h @@ -0,0 +1,44 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef HEX_H +#define HEX_H + +char *hex(const uint8_t *ptr, size_t n); +/* convert an octet-string to hex */ + +uint8_t *unhex(const char *s, size_t *np); +/* convert a hex string back to an octet string */ + +int unhexdigit(int c); +/* if @c@ is a hex digit, return its value. else return -1 and log an error. */ + +int unhexdigitq(int c); +/* same as unhexdigit() but doesn't issue an error message */ + +#endif /* HEX_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:fe1aee378d7330eb7a39283bfb195905 */ diff --git a/lib/inputline.c b/lib/inputline.c new file mode 100644 index 0000000..891bade --- /dev/null +++ b/lib/inputline.c @@ -0,0 +1,61 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <errno.h> +#include <string.h> + +#include "log.h" +#include "mem.h" +#include "vector.h" +#include "charset.h" +#include "inputline.h" + +int inputline(const char *tag, FILE *fp, char **lp, int newline) { + struct dynstr d; + int ch; + + dynstr_init(&d); + while((ch = getc(fp)), + (!ferror(fp) && !feof(fp) && ch != newline)) + dynstr_append(&d, ch); + if(ferror(fp)) { + error(errno, "error reading %s", tag); + return -1; + } else if(feof(fp)) { + if(d.nvec != 0) + error(0, "error reading %s: unexpected EOF", tag); + return -1; + } + dynstr_terminate(&d); + *lp = d.vec; + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2df12a23b06659ceea8a6019550a65b4 */ diff --git a/lib/inputline.h b/lib/inputline.h new file mode 100644 index 0000000..74a8cbc --- /dev/null +++ b/lib/inputline.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef INPUTLINE_H +#define INPUTLINE_H + +int inputline(const char *tag, FILE *fp, char **lp, int newline); +/* read characters from @fp@ until @newline@ is encountered. Store + * them (excluding @newline@) via @lp@. Return 0 on success, -1 on + * error/eof. */ + +#endif /* INPUTLINE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:8baeb420a4c9595d8a5d1105d5e8a4f9 */ diff --git a/lib/kvp.c b/lib/kvp.c new file mode 100644 index 0000000..d3a80f5 --- /dev/null +++ b/lib/kvp.c @@ -0,0 +1,195 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stdio.h> + +#include "mem.h" +#include "kvp.h" +#include "log.h" +#include "vector.h" +#include "hex.h" +#include "sink.h" + +int urldecode(struct sink *sink, const char *ptr, size_t n) { + int c, d1, d2; + + while(n-- > 0) { + switch(c = *ptr++) { + case '%': + if((d1 = unhexdigit(ptr[0])) == -1 + || (d2 = unhexdigit(ptr[1])) == -1) + return -1; + c = d1 * 16 + d2; + ptr += 2; + n -= 2; + break; + case '+': + c = ' '; + break; + default: + break; + } + if(sink_writec(sink,c) < 0) + return -1; + } + return 0; +} + +static char *decode(const char *ptr, size_t n) { + struct dynstr d; + struct sink *s; + + dynstr_init(&d); + s = sink_dynstr(&d); + if(urldecode(s, ptr, n)) + return 0; + dynstr_terminate(&d); + return d.vec; +} + +struct kvp *kvp_urldecode(const char *ptr, size_t n) { + struct kvp *kvp, **kk = &kvp, *k; + const char *q, *r, *top = ptr + n, *next; + + while(ptr < top) { + *kk = k = xmalloc(sizeof *k); + if(!(q = memchr(ptr, '=', top - ptr))) + break; + if(!(k->name = decode(ptr, q - ptr))) break; + if((r = memchr(ptr, '&', top - ptr))) + next = r + 1; + else + next = r = top; + if(r < q) + break; + if(!(k->value = decode(q + 1, r - (q + 1)))) break; + kk = &k->next; + ptr = next; + } + *kk = 0; + return kvp; +} + +int urlencode(struct sink *sink, const char *s, size_t n) { + unsigned char c; + + while(n > 0) { + c = *s++; + n--; + switch(c) { + default: + if((c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z')) { + /* RFC2396 2.3 unreserved characters */ + case '-': + case '_': + case '.': + case '!': + case '~': + case '*': + case '\'': + case '(': + case ')': + /* additional unreserved characters */ + case '/': + if(sink_writec(sink, c) < 0) + return -1; + } else + if(sink_printf(sink, "%%%02x", (unsigned int)c) < 0) + return -1; + } + } + return 0; +} + +const char *urlencodestring(const char *s) { + struct dynstr d; + + dynstr_init(&d); + urlencode(sink_dynstr(&d), s, strlen(s)); + dynstr_terminate(&d); + return d.vec; +} + +char *kvp_urlencode(const struct kvp *kvp, size_t *np) { + struct dynstr d; + struct sink *sink; + + dynstr_init(&d); + sink = sink_dynstr(&d); + while(kvp) { + urlencode(sink, kvp->name, strlen(kvp->name)); + dynstr_append(&d, '='); + urlencode(sink, kvp->value, strlen(kvp->value)); + if((kvp = kvp->next)) + dynstr_append(&d, '&'); + + } + dynstr_terminate(&d); + if(np) + *np = d.nvec; + return d.vec; +} + +int kvp_set(struct kvp **kvpp, const char *name, const char *value) { + struct kvp *k, **kk; + + for(kk = kvpp; (k = *kk) && strcmp(name, k->name); kk = &k->next) + ; + if(k) { + if(value) { + if(strcmp(k->value, value)) { + k->value = xstrdup(value); + return 1; + } else + return 0; + } else { + *kk = k->next; + return 1; + } + } else { + if(value) { + *kk = k = xmalloc(sizeof *k); + k->name = xstrdup(name); + k->value = xstrdup(value); + return 1; + } else + return 0; + } +} + +const char *kvp_get(const struct kvp *kvp, const char *name) { + for(;kvp && strcmp(kvp->name, name); kvp = kvp->next) + ; + return kvp ? kvp->value : 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7c983bde915ec06fbda8d8cdc465dd9d */ diff --git a/lib/kvp.h b/lib/kvp.h new file mode 100644 index 0000000..52d9e49 --- /dev/null +++ b/lib/kvp.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef KVP_H +#define KVP_H + +struct dynstr; +struct sink; + +struct kvp { + struct kvp *next; /* next entry */ + const char *name; /* name */ + const char *value; /* value */ +}; + +struct kvp *kvp_urldecode(const char *ptr, size_t n); +/* url-decode [ptr,ptr+n) */ + +char *kvp_urlencode(const struct kvp *kvp, size_t *np); +/* url-encode @kvp@ into a null-terminated string. If @np@ is not + * null return the length thru it. */ + +int kvp_set(struct kvp **kvpp, const char *name, const char *value); +/* set @name@ to @value@. If @value@ is 0, remove @name@. + * Returns 1 if we made a real change, else 0. */ + +const char *kvp_get(const struct kvp *kvp, const char *name); +/* Get the value of @name@ */ + +int urldecode(struct sink *sink, const char *ptr, size_t n); +/* url-decode the @n@ bytes at @ptr@, writing the results to @s@. + * Return 0 on success, -1 on error. */ + +int urlencode(struct sink *sink, const char *s, size_t n); +/* url-encode the @n@ bytes at @s@, writing to @sink@. Return 0 on + * success, -1 on error. */ + +const char *urlencodestring(const char *s); +/* return the url-encoded form of @s@ */ + +#endif /* KVP_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:edb5787b529ef7b694efa4ce2c44ff3f */ diff --git a/lib/log-impl.h b/lib/log-impl.h new file mode 100644 index 0000000..27f0004 --- /dev/null +++ b/lib/log-impl.h @@ -0,0 +1,53 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +void disorder_fatal(int errno_value, const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_CRIT, errno_value, msg, ap); + va_end(ap); + if(getenv("DISORDER_FATAL_ABORT")) abort(); + exitfn(EXIT_FAILURE); +} + +void disorder_error(int errno_value, const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_ERR, errno_value, msg, ap); + va_end(ap); +} + +void disorder_info(const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + elog(LOG_INFO, 0, msg, ap); + va_end(ap); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e499f9971df6553a14994bb186235869 */ diff --git a/lib/log.c b/lib/log.c new file mode 100644 index 0000000..e6539cf --- /dev/null +++ b/lib/log.c @@ -0,0 +1,180 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because the memory allocation functions report errors */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <syslog.h> +#include <sys/time.h> + +#include "log.h" +#include "disorder.h" +#include "printf.h" + +struct log_output { + void (*fn)(int pri, const char *msg, void *user); + void *user; +}; + +void (*exitfn)(int) attribute((noreturn)) = exit; +int debugging; +const char *progname; +const char *debug_filename; +int debug_lineno; +struct log_output *log_default = &log_stderr; + +static const char *debug_only; + +/* we might be receiving things in any old encoding, or binary rubbish in no + * encoding at all, so escape anything we don't like the look of */ +static void format(char buffer[], size_t bufsize, const char *fmt, va_list ap) { + char t[1024]; + const char *p; + int ch; + size_t n = 0; + + if(byte_vsnprintf(t, sizeof t, fmt, ap) < 0) + strcpy(t, "[byte_vsnprintf failed]"); + p = t; + while((ch = (unsigned char)*p++)) { + if(ch >= ' ' && ch <= 126) { + if(n < bufsize) buffer[n++] = ch; + } else { + if(n < bufsize) buffer[n++] = '\\'; + if(n < bufsize) buffer[n++] = '0' + ((ch >> 6) & 7); + if(n < bufsize) buffer[n++] = '0' + ((ch >> 3) & 7); + if(n < bufsize) buffer[n++] = '0' + ((ch >> 0) & 7); + } + } + if(n >= bufsize) + n = bufsize - 1; + buffer[n] = 0; +} + +/* log to a file */ +static void logfp(int pri, const char *msg, void *user) { + struct timeval tv; + FILE *fp = user ? user : stderr; + /* ...because stderr is not a constant so we can't initialize log_stderr + * sanely */ + const char *p; + + if(progname) + fprintf(fp, "%s: ", progname); + if(pri <= LOG_ERR) + fputs("ERROR: ", fp); + else if(pri < LOG_DEBUG) + fputs("INFO: ", fp); + else { + if(!debug_only) { + if(!(debug_only = getenv("DISORDER_DEBUG_ONLY"))) + debug_only = ""; + } + gettimeofday(&tv, 0); + p = debug_filename; + while(!strncmp(p, "../", 3)) p += 3; + if(*debug_only && strcmp(p, debug_only)) + return; + fprintf(fp, "%llu.%06lu: %s:%d: ", + (unsigned long long)tv.tv_sec, (unsigned long)tv.tv_usec, + p, debug_lineno); + } + fputs(msg, fp); + fputc('\n', fp); +} + +/* log to syslog */ +static void logsyslog(int pri, const char *msg, + void attribute((unused)) *user) { + if(pri < LOG_DEBUG) + syslog(pri, "%s", msg); + else + syslog(pri, "%s:%d: %s", debug_filename, debug_lineno, msg); +} + +struct log_output log_stderr = { logfp, 0 }; +struct log_output log_syslog = { logsyslog, 0 }; + +/* log to all log outputs */ +static void vlogger(int pri, const char *fmt, va_list ap) { + char buffer[1024]; + + format(buffer, sizeof buffer, fmt, ap); + log_default->fn(pri, buffer, log_default->user); +} + +/* wrapper for vlogger */ +static void logger(int pri, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vlogger(pri, fmt, ap); + va_end(ap); +} + +/* internals of fatal/error/info */ +void elog(int pri, int errno_value, const char *fmt, va_list ap) { + char buffer[1024]; + + if(errno_value == 0) + vlogger(pri, fmt, ap); + else { + memset(buffer, 0, sizeof buffer); + byte_vsnprintf(buffer, sizeof buffer, fmt, ap); + buffer[sizeof buffer - 1] = 0; + logger(pri, "%s: %s", buffer, strerror(errno_value)); + } +} + +#define disorder_fatal fatal +#define disorder_error error +#define disorder_info info + +/* shared implementation of vararg functions */ +#include "log-impl.h" + +void debug(const char *msg, ...) { + va_list ap; + + va_start(ap, msg); + vlogger(LOG_DEBUG, msg, ap); + va_end(ap); +} + +void set_progname(char **argv) { + if((progname = strrchr(argv[0], '/'))) + ++progname; + else + progname = argv[0]; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:78385d5240eab4439cb7eca7dad5154d */ diff --git a/lib/log.h b/lib/log.h new file mode 100644 index 0000000..e251a0e --- /dev/null +++ b/lib/log.h @@ -0,0 +1,89 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef LOG_H +#define LOG_H + +/* All messages are initially emitted by one of the four functions below. + * debug() is generally invoked via D() so that mostly you just do a test + * rather than a complete subroutine call. + * + * Messages are dispatched via log_default. This defaults to log_stderr. + * daemonize() will turn off log_stderr and use log_syslog instead. + * + * fatal() will call exitfn() with a nonzero status. The default value is + * exit(), but it should be set to _exit() anywhere but the 'main line' of the + * program, to guarantee that exit() gets called at most once. + */ + +#include <stdarg.h> + +struct log_output; + +void set_progname(char **argv); +/* set progname from argv[0] */ + +void elog(int pri, int errno_value, const char *fmt, va_list ap); +/* internals of fatal/error/info/debug */ + +void fatal(int errno_value, const char *msg, ...) attribute((noreturn)) + attribute((format (printf, 2, 3))); +void error(int errno_value, const char *msg, ...) + attribute((format (printf, 2, 3))); +void info(const char *msg, ...) + attribute((format (printf, 1, 2))); +void debug(const char *msg, ...) + attribute((format (printf, 1, 2))); +/* report a message of the given class. @errno_value@ if present an + * non-zero is included. @fatal@ terminates the process. */ + +extern int debugging; +/* set when debugging enabled */ + +extern void (*exitfn)(int) attribute((noreturn)); +/* how to exit the program (for fatal) */ + +extern const char *progname; +/* program name */ + +extern struct log_output log_stderr, log_syslog, *log_default; +/* some typical outputs */ + +extern const char *debug_filename; +extern int debug_lineno; + +#define D(x) do { \ + if(debugging) { \ + debug_filename=__FILE__; \ + debug_lineno=__LINE__; \ + debug x; \ + } \ +} while(0) + +#endif /* LOG_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:6350679c7069ec3b2709aa51004a804a */ diff --git a/lib/logfd.c b/lib/logfd.c new file mode 100644 index 0000000..e644e6c --- /dev/null +++ b/lib/logfd.c @@ -0,0 +1,96 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include "syscalls.h" +#include "logfd.h" +#include "event.h" +#include "log.h" + +struct logfd_state { + const char *tag; +}; + +/* called when bytes are available and at eof */ +static int logfd_readable(ev_source attribute((unused)) *ev, + ev_reader *reader, + int fd, + void *ptr, + size_t bytes, + int eof, + void *u) { + char *nl; + const char *tag = u; + int len; + + while((nl = memchr(ptr, '\n', bytes))) { + len = nl - (char *)ptr; + ev_reader_consume(reader, len + 1); + info("%s: %.*s", tag, len, (char *)ptr); + ptr = nl + 1; + bytes -= len + 1; + } + if(eof && bytes) { + info("%s: %.*s", tag, (int)bytes, (char *)ptr); + ev_reader_consume(reader, bytes); + } + if(eof) + xclose(fd); + return 0; +} + +/* called when a read error occurs */ +static int logfd_error(ev_source attribute((unused)) *ev, + int fd, + int errno_value, + void *u) { + const char *tag = u; + + error(errno_value, "error reading log pipe from %s", tag); + xclose(fd); + return 0; +} + +int logfd(ev_source *ev, const char *tag) { + int p[2]; + + xpipe(p); + cloexec(p[0]); + nonblock(p[0]); + if(!ev_reader_new(ev, p[0], logfd_readable, logfd_error, (void *)tag)) + fatal(errno, "error calling ev_reader_new"); + return p[1]; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:zVrHlzk2xWMLo9nDM803pA */ diff --git a/lib/logfd.h b/lib/logfd.h new file mode 100644 index 0000000..4815fa4 --- /dev/null +++ b/lib/logfd.h @@ -0,0 +1,39 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef LOGFD_H +#define LOGFD_H + +struct ev_source; + +int logfd(struct ev_source *ev, const char *tag); +/* return an FD to write log messages to */ + +#endif /* LOGFD_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:NBzZxO2J269xJpf8eiBw+Q */ diff --git a/lib/mem-impl.h b/lib/mem-impl.h new file mode 100644 index 0000000..a1ad0d1 --- /dev/null +++ b/lib/mem-impl.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +int disorder_asprintf(char **rp, const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = byte_vasprintf(rp, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:89630239839ffe383cc381e9d2e2fb19 */ diff --git a/lib/mem.c b/lib/mem.c new file mode 100644 index 0000000..07c3495 --- /dev/null +++ b/lib/mem.c @@ -0,0 +1,124 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <gc.h> +#include <errno.h> +#include <string.h> +#include <stdio.h> +#include <stdlib.h> + +#include "mem.h" +#include "log.h" +#include "printf.h" + +#include "disorder.h" + +static void *(*do_malloc)(size_t) = GC_malloc; +static void *(*do_realloc)(void *, size_t) = GC_realloc; +static void *(*do_malloc_atomic)(size_t) = GC_malloc_atomic; +static void (*do_free)(void *) = GC_free; + +static void *malloc_and_zero(size_t n) { + void *ptr = malloc(n); + + if(ptr) memset(ptr, 0, n); + return ptr; +} + +void mem_init(int gc) { + const char *e; + + if(!gc || ((e = getenv("DISORDER_GC")) && !strcmp(e, "no"))) { + do_malloc = malloc_and_zero; + do_malloc_atomic = malloc; + do_realloc = realloc; + do_free = free; + } else + GC_init(); +} + +void *xmalloc(size_t n) { + void *ptr; + + if(!(ptr = do_malloc(n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xrealloc(void *ptr, size_t n) { + if(!(ptr = do_realloc(ptr, n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xcalloc(size_t count, size_t size) { + if(count > SIZE_MAX / size) + fatal(0, "excessively large calloc"); + return xmalloc(count * size); +} + +void *xmalloc_noptr(size_t n) { + void *ptr; + + if(!(ptr = do_malloc_atomic(n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +void *xrealloc_noptr(void *ptr, size_t n) { + if(ptr == 0) + return xmalloc_noptr(n); + if(!(ptr = do_realloc(ptr, n)) && n) + fatal(errno, "error allocating memory"); + return ptr; +} + +char *xstrdup(const char *s) { + char *t; + + if(!(t = do_malloc_atomic(strlen(s) + 1))) + fatal(errno, "error allocating memory"); + return strcpy(t, s); +} + +char *xstrndup(const char *s, size_t n) { + char *t; + + if(!(t = do_malloc_atomic(n + 1))) + fatal(errno, "error allocating memory"); + memcpy(t, s, n); + t[n] = 0; + return t; +} + +void xfree(void *ptr) { + do_free(ptr); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:fceaf81fe79b2f4f87c9541774a2b8f2 */ diff --git a/lib/mem.h b/lib/mem.h new file mode 100644 index 0000000..ab1529f --- /dev/null +++ b/lib/mem.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MEM_H +#define MEM_H + +#ifdef NO_MEMORY_ALLOCATION +# error including source file not allowed to perform memory allocation +#endif + +#include <stdarg.h> + +void mem_init(int gc); +/* initialize memory management. Set GC to 1 if garbage collection is + * desired. */ + +void *xmalloc(size_t); +void *xrealloc(void *, size_t); +void *xcalloc(size_t count, size_t size); +/* As malloc/realloc/calloc, but + * 1) succeed or call fatal + * 2) always clear (the unused part of) the new allocation + * 3) are garbage-collected + */ + +void *xmalloc_noptr(size_t); +void *xrealloc_noptr(void *, size_t); +char *xstrdup(const char *); +char *xstrndup(const char *, size_t); +/* As malloc/realloc/strdup, but + * 1) succeed or call fatal + * 2) are garbage-collected + * 3) allocated space must not contain any pointers + * + * {xmalloc,xrealloc}_noptr don't promise to clear the new space + */ + +void xfree(void *ptr); +/* As free, but calls GC_free instead if gc is enabled */ + +#endif /* MEM_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:333db053cab9e5ef91a151040d3256fb */ diff --git a/lib/mime.c b/lib/mime.c new file mode 100644 index 0000000..d80ae5d --- /dev/null +++ b/lib/mime.c @@ -0,0 +1,374 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <ctype.h> + +#include "mem.h" +#include "mime.h" +#include "vector.h" +#include "hex.h" + +static int whitespace(int c) { + switch(c) { + case ' ': + case '\t': + case '\r': + case '\n': + return 1; + default: + return 0; + } +} + +static int tspecial(int c) { + switch(c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + return 1; + default: + return 0; + } +} + +static const char *skipwhite(const char *s) { + int c, depth; + + for(;;) { + switch(c = *s) { + case ' ': + case '\t': + case '\r': + case '\n': + ++s; + break; + case '(': + ++s; + depth = 1; + while(*s && depth) { + c = *s++; + switch(c) { + case '(': ++depth; break; + case ')': --depth; break; + case '\\': + if(!*s) return 0; + ++s; + break; + } + } + if(depth) return 0; + break; + default: + return s; + } + } +} + +static const char *parsestring(const char *s, char **valuep) { + struct dynstr value; + int c; + + dynstr_init(&value); + ++s; + while((c = *s++) != '"') { + switch(c) { + case '\\': + if(!(c = *s++)) return 0; + default: + dynstr_append(&value, c); + break; + } + } + if(!c) return 0; + dynstr_terminate(&value); + *valuep = value.vec; + return s; +} + +int mime_content_type(const char *s, + char **typep, + char **parameternamep, + char **parametervaluep) { + struct dynstr type, parametername, parametervalue; + + dynstr_init(&type); + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&type, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '/') return -1; + dynstr_append(&type, '/'); + if(!(s = skipwhite(s))) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&type, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + + if(*s == ';') { + dynstr_init(¶metername); + ++s; + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metername, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '=') return -1; + if(!(s = skipwhite(s))) return -1; + if(*s == '"') { + if(!(s = parsestring(s, parametervaluep))) return -1; + } else { + dynstr_init(¶metervalue); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metervalue, *s++); + dynstr_terminate(¶metervalue); + *parametervaluep = parametervalue.vec; + } + if(!(s = skipwhite(s))) return -1; + dynstr_terminate(¶metername); + *parameternamep = parametername.vec; + } else + *parametervaluep = *parameternamep = 0; + dynstr_terminate(&type); + *typep = type.vec; + return 0; +} + +static int iscrlf(const char *ptr) { + return ptr[0] == '\r' && ptr[1] == '\n'; +} + +const char *mime_parse(const char *s, + int (*callback)(const char *name, const char *value, + void *u), + void *u) { + struct dynstr name, value; + char *cte = 0, *p; + + while(*s && !iscrlf(s)) { + dynstr_init(&name); + dynstr_init(&value); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&name, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return 0; + if(*s != ':') return 0; + ++s; + while(*s && !(*s == '\n' && !(s[1] == ' ' || s[1] == '\t'))) + dynstr_append(&value, *s++); + if(*s) ++s; + dynstr_terminate(&name); + dynstr_terminate(&value); + if(!strcmp(name.vec, "content-transfer-encoding")) { + cte = xstrdup(value.vec); + for(p = cte; *p; p++) + *p = tolower((unsigned char)*p); + } + if(callback(name.vec, value.vec, u)) return 0; + } + if(*s) s += 2; + if(cte) { + if(!strcmp(cte, "base64")) return mime_base64(s); + if(!strcmp(cte, "quoted-printable")) return mime_qp(s); + } + return s; +} + +static int isboundary(const char *ptr, const char *boundary, size_t bl) { + return (ptr[0] == '-' + && ptr[1] == '-' + && !strncmp(ptr + 2, boundary, bl) + && (iscrlf(ptr + bl + 2) + || (ptr[bl + 2] == '-' + && ptr[bl + 3] == '-' + && iscrlf(ptr + bl + 4)))); +} + +static int isfinal(const char *ptr, const char *boundary, size_t bl) { + return (ptr[0] == '-' + && ptr[1] == '-' + && !strncmp(ptr + 2, boundary, bl) + && ptr[bl + 2] == '-' + && ptr[bl + 3] == '-' + && iscrlf(ptr + bl + 4)); +} + +int mime_multipart(const char *s, + int (*callback)(const char *s, void *u), + const char *boundary, + void *u) { + size_t bl = strlen(boundary); + const char *start, *e; + int ret; + + if(!isboundary(s, boundary, bl)) return -1; + while(!isfinal(s, boundary, bl)) { + s = strstr(s, "\r\n") + 2; + start = s; + while(!isboundary(s, boundary, bl)) { + if(!(e = strstr(s, "\r\n"))) return -1; + s = e + 2; + } + if((ret = callback(xstrndup(start, + s == start ? 0 : s - start - 2), + u))) + return ret; + } + return 0; +} + +int mime_rfc2388_content_disposition(const char *s, + char **dispositionp, + char **parameternamep, + char **parametervaluep) { + struct dynstr disposition, parametername, parametervalue; + + dynstr_init(&disposition); + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(&disposition, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + + if(*s == ';') { + dynstr_init(¶metername); + ++s; + if(!(s = skipwhite(s))) return -1; + if(!*s) return -1; + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metername, tolower((unsigned char)*s++)); + if(!(s = skipwhite(s))) return -1; + if(*s++ != '=') return -1; + if(!(s = skipwhite(s))) return -1; + if(*s == '"') { + if(!(s = parsestring(s, parametervaluep))) return -1; + } else { + dynstr_init(¶metervalue); + while(*s && !tspecial(*s) && !whitespace(*s)) + dynstr_append(¶metervalue, *s++); + dynstr_terminate(¶metervalue); + *parametervaluep = parametervalue.vec; + } + if(!(s = skipwhite(s))) return -1; + dynstr_terminate(¶metername); + *parameternamep = parametername.vec; + } else + *parametervaluep = *parameternamep = 0; + dynstr_terminate(&disposition); + *dispositionp = disposition.vec; + return 0; +} + +char *mime_qp(const char *s) { + struct dynstr d; + int c, a, b; + const char *t; + + dynstr_init(&d); + while((c = *s++)) { + switch(c) { + case '=': + if((a = unhexdigitq(s[0])) != -1 + && (b = unhexdigitq(s[1])) != -1) { + dynstr_append(&d, a * 16 + b); + s += 2; + } else { + t = s; + while(*t == ' ' || *t == '\t') ++t; + if(iscrlf(t)) { + /* soft line break */ + s = t + 2; + } else + return 0; + } + break; + case ' ': + case '\t': + t = s; + while(*t == ' ' || *t == '\t') ++t; + if(iscrlf(t)) + /* trailing space is always eliminated */ + s = t; + else + dynstr_append(&d, c); + break; + default: + dynstr_append(&d, c); + break; + } + } + dynstr_terminate(&d); + return d.vec; +} + +char *mime_base64(const char *s) { + struct dynstr d; + const char *t; + int b[4], n, c; + static const char table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + dynstr_init(&d); + n = 0; + while((c = (unsigned char)*s++)) { + if((t = strchr(table, c))) { + b[n++] = t - table; + if(n == 4) { + dynstr_append(&d, (b[0] << 2) + (b[1] >> 4)); + dynstr_append(&d, (b[1] << 4) + (b[2] >> 2)); + dynstr_append(&d, (b[2] << 6) + b[3]); + n = 0; + } + } else if(c == '=') { + if(n >= 2) { + dynstr_append(&d, (b[0] << 2) + (b[1] >> 4)); + if(n == 3) + dynstr_append(&d, (b[1] << 4) + (b[2] >> 2)); + } + break; + } + } + dynstr_terminate(&d); + return d.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:vz7S54dWl7x/NNx5kvg5cg */ diff --git a/lib/mime.h b/lib/mime.h new file mode 100644 index 0000000..14cb809 --- /dev/null +++ b/lib/mime.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MIME_H +#define MIME_H + +int mime_content_type(const char *s, + char **typep, + char **parameternamep, + char **parametervaluep); +/* Parse a content-type value. returns 0 on success, -1 on error. + * paramaternamep and parametervaluep are only set if present. + * type and parametername are forced to lower case. + */ + +const char *mime_parse(const char *s, + int (*callback)(const char *name, const char *value, + void *u), + void *u); +/* Parse a MIME message. Calls CALLBACK for each header field, then returns a + * pointer to the decoded body (might or might not point back into the original + * string). */ + +int mime_multipart(const char *s, + int (*callback)(const char *s, void *u), + const char *boundary, + void *u); +/* call CALLBACK with each part of multipart document [s,s+n) */ + +int mime_rfc2388_content_disposition(const char *s, + char **dispositionp, + char **parameternamep, + char **parametervaluep); +/* Parse an RFC2388-style content-disposition field */ + +char *mime_qp(const char *s); +char *mime_base64(const char *s); +/* convert quoted-printable or base64 data */ + +#endif /* MIME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:JMq56pGj+/kWY7mZDnHpWg */ diff --git a/lib/mixer.c b/lib/mixer.c new file mode 100644 index 0000000..5293ffd --- /dev/null +++ b/lib/mixer.c @@ -0,0 +1,114 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <stdio.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <stddef.h> +#include <sys/ioctl.h> + +#include "configuration.h" +#include "mixer.h" +#include "log.h" +#include "syscalls.h" + +#if HAVE_SYS_SOUNDCARD_H +#include <sys/soundcard.h> + +/* documentation does not match implementation! */ +#ifndef SOUND_MIXER_READ +# define SOUND_MIXER_READ(x) MIXER_READ(x) +#endif +#ifndef SOUND_MIXER_WRITE +# define SOUND_MIXER_WRITE(x) MIXER_WRITE(x) +#endif + +static const char *channels[] = SOUND_DEVICE_NAMES; + +int mixer_channel(const char *c) { + unsigned n; + + if(!c[strspn(c, "0123456789")]) + return atoi(c); + else { + for(n = 0; n < sizeof channels / sizeof *channels; ++n) + if(!strcmp(channels[n], c)) + return n; + return -1; + } +} + +int mixer_control(int *left, int *right, int set) { + int fd, ch, r; + + if(config->mixer + && config->channel + && (ch = mixer_channel(config->channel)) != -1) { + if((fd = open(config->mixer, O_RDWR, 0)) < 0) { + error(errno, "error opening %s", config->mixer); + return -1; + } + if(set) { + r = (*left & 0xff) + (*right & 0xff) * 256; + if(ioctl(fd, SOUND_MIXER_WRITE(ch), &r) == -1) { + error(errno, "error changing %s channel %s", + config->mixer, config->channel); + xclose(fd); + return -1; + } + } + if(ioctl(fd, SOUND_MIXER_READ(ch), &r) == -1) { + error(errno, "error reading %s channel %s", + config->mixer, config->channel); + xclose(fd); + return -1; + } + *left = r & 0xff; + *right = (r >> 8) & 0xff; + xclose(fd); + return 0; + } else + return -1; +} +#else +int mixer_channel(const char attribute((unused)) *c) { + return 0; +} + +int mixer_control(int attribute((unused)) *left, + int attribute((unused)) *right, + int attribute((unused)) set) { + error(0, "don't know how to set volume on this platform"); + return -1; +} +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2c047b85dbe68f45f2ebfc8c051ba967 */ diff --git a/lib/mixer.h b/lib/mixer.h new file mode 100644 index 0000000..99fbef2 --- /dev/null +++ b/lib/mixer.h @@ -0,0 +1,43 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MIXER_H +#define MIXER_H + +int mixer_channel(const char *c); +/* convert @c@ to a channel number, or return -1 if it does not match */ + +int mixer_control(int *left, int *right, int set); +/* get/set the current level. If @set@ is true then the level is set + * according to the pointers. In either case the eventual level is + * returned via the pointers. + * + * Returns 0 on success and -1 on error. + */ + +#endif /* MIXER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:d70a8b1bc1e79efcad02d20259246454 */ diff --git a/lib/plugin.c b/lib/plugin.c new file mode 100644 index 0000000..bf41797 --- /dev/null +++ b/lib/plugin.c @@ -0,0 +1,304 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <dlfcn.h> +#include <unistd.h> +#include <string.h> +#include <stdio.h> + +#include "plugin.h" +#include "configuration.h" +#include "log.h" +#include "mem.h" +#include "defs.h" +#include "disorder.h" +#include "printf.h" + +/* generic plugin support *****************************************************/ + +#ifndef SOSUFFIX +# define SOSUFFIX ".so" +#endif + +struct plugin { + struct plugin *next; + void *dlhandle; + const char *name; +}; + +static struct plugin *plugins; + +const struct plugin *open_plugin(const char *name, + unsigned flags) { + void *h = 0; + char *p; + int n; + struct plugin *pl; + + for(pl = plugins; pl && strcmp(pl->name, name); pl = pl->next) + ; + if(pl) return pl; + for(n = 0; n <= config->plugins.n; ++n) { + byte_xasprintf(&p, "%s/%s" SOSUFFIX, + n == config->plugins.n ? pkglibdir : config->plugins.s[n], + name); + if(access(p, R_OK) == 0) { + h = dlopen(p, RTLD_NOW); + if(!h) { + error(0, "error opening %s: %s", p, dlerror()); + continue; + } + pl = xmalloc(sizeof *pl); + pl->dlhandle = h; + pl->name = xstrdup(name); + pl->next = plugins; + plugins = pl; + return pl; + } + } + (flags & PLUGIN_FATAL ? fatal : error)(0, "cannot find plugin '%s'", name); + return 0; +} + +function_t *get_plugin_function(const struct plugin *pl, + const char *symbol) { + function_t *f; + const char *e; + + f = (function_t *)dlsym(pl->dlhandle, symbol); + if((e = dlerror())) + fatal(0, "error looking up function '%s' in '%s': %s",symbol, pl->name, e); + return f; +} + +const void *get_plugin_object(const struct plugin *pl, + const char *symbol) { + void *o; + const char *e; + + o = dlsym(pl->dlhandle, symbol); + if((e = dlerror())) + fatal(0, "error looking up object '%s' in '%s': %s", symbol, pl->name, e); + return o; +} + +/* specific plugin interfaces *************************************************/ + +typedef long tracklength_fn(const char *track, const char *path); + +long tracklength(const char *track, const char *path) { + static tracklength_fn *f = 0; + + if(!f) + f = (tracklength_fn *)get_plugin_function(open_plugin("tracklength", + PLUGIN_FATAL), + "disorder_tracklength"); + return (*f)(track, path); +} + +typedef void scan_fn(const char *root); + +void scan(const char *module, const char *root) { + ((scan_fn *)get_plugin_function(open_plugin(module, PLUGIN_FATAL), + "disorder_scan"))(root); +} + +typedef int check_fn(const char *root, const char *path); + + +int check(const char *module, const char *root, const char *path) { + return ((check_fn *)get_plugin_function(open_plugin(module, PLUGIN_FATAL), + "disorder_check"))(root, path); +} + +typedef void notify_play_fn(const char *track, const char *submitter); + +void notify_play(const char *track, + const char *submitter) { + static notify_play_fn *f; + + if(!f) + f = (notify_play_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_play"); + (*f)(track, submitter); +} + +typedef void notify_scratch_fn(const char *track, + const char *submitter, + const char *scratcher, + int seconds); + +void notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds) { + static notify_scratch_fn *f; + + if(!f) + f = (notify_scratch_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_scratch"); + (*f)(track, submitter, scratcher, seconds); +} + +typedef void notify_not_scratched_fn(const char *track, + const char *submitter); + +void notify_not_scratched(const char *track, + const char *submitter) { + static notify_not_scratched_fn *f; + + if(!f) + f = (notify_not_scratched_fn *)get_plugin_function + (open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_not_scratched"); + (*f)(track, submitter); +} + +typedef void notify_queue_fn(const char *track, + const char *submitter); + +void notify_queue(const char *track, + const char *submitter) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue"); + (*f)(track, submitter); +} + +void notify_queue_remove(const char *track, + const char *remover) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue_remove"); + (*f)(track, remover); +} + +void notify_queue_move(const char *track, + const char *mover) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_queue_move"); + (*f)(track, mover); +} + +void notify_pause(const char *track, const char *who) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_pause"); + (*f)(track, who); +} + +void notify_resume(const char *track, const char *who) { + static notify_queue_fn *f; + + if(!f) + f = (notify_queue_fn *)get_plugin_function(open_plugin("notify", + PLUGIN_FATAL), + "disorder_notify_resume"); + (*f)(track, who); +} + +/* player plugin interfaces ***************************************************/ + +/* get type */ + +unsigned long play_get_type(const struct plugin *pl) { + return *(const unsigned long *)get_plugin_object(pl, "disorder_player_type"); +} + +/* prefork */ + +typedef void *prefork_fn(const char *track); + +void *play_prefork(const struct plugin *pl, + const char *track) { + return ((prefork_fn *)get_plugin_function(pl, + "disorder_play_prefork"))(track); +} + +/* play */ + +typedef void play_track_fn(const char *const *parameters, + int nparameters, + const char *path, + const char *track); + +void play_track(const struct plugin *pl, + const char *const *parameters, + int nparameters, + const char *path, + const char *track) { + ((play_track_fn *)get_plugin_function(pl, + "disorder_play_track"))(parameters, + nparameters, + path, + track); +} + +/* cleanup */ + +typedef void cleanup_fn(void *data); + +void play_cleanup(const struct plugin *pl, void *data) { + ((cleanup_fn *)get_plugin_function(pl, "disorder_play_cleanup"))(data); +} + +/* pause */ + +typedef int pause_fn(long *playedp, void *data); + +int play_pause(const struct plugin *pl, long *playedp, void *data) { + return (((pause_fn *)get_plugin_function(pl, "disorder_pause_track")) + (playedp, data)); +} + +/* resume */ + +typedef void resume_fn(void *data); + +void play_resume(const struct plugin *pl, void *data) { + (((resume_fn *)get_plugin_function(pl, "disorder_resume_track")) + (data)); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:069494ccad9bf04cf6ca9505d9528f0a */ diff --git a/lib/plugin.h b/lib/plugin.h new file mode 100644 index 0000000..59cc936 --- /dev/null +++ b/lib/plugin.h @@ -0,0 +1,132 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef PLUGIN_H +#define PLUGIN_H + +/* general ********************************************************************/ + +struct plugin; + +typedef void *plugin_handle; +typedef void function_t(void); + +const struct plugin *open_plugin(const char *name, + unsigned flags); +#define PLUGIN_FATAL 0x0001 /* fatal() on error */ +/* Open a plugin. Returns a null pointer on error or a handle to it + * on success. */ + +function_t *get_plugin_function(const struct plugin *handle, + const char *symbol); +const void *get_plugin_object(const struct plugin *handle, + const char *symbol); +/* Look up a function or an object in a plugin */ + +/* track length computation ***************************************************/ + +long tracklength(const char *track, const char *path); +/* compute the length of the track. @track@ is the UTF-8 name of the + * track, @path@ is the file system name (or 0 for tracks that don't + * exist in the filesystem). The return value should be a positive + * number of seconds, 0 for unknown or -1 if an error occurred. */ + +/* collection interface *******************************************************/ + +void scan(const char *module, const char *root); +/* write a list of path names below @root@ to standard output. */ + +int check(const char *module, const char *root, const char *path); +/* Recheck a track, given its root and path name. Return 1 if it + * exists, 0 if it does not exist and -1 if an error occurred. */ + +/* notification interface *****************************************************/ + +void notify_play(const char *track, + const char *submitter); +/* we're going to play @track@. It was submitted by @submitter@ + * (might be a null pointer) */ + +void notify_scratch(const char *track, + const char *submitter, + const char *scratcher, + int seconds); +/* @scratcher@ scratched @track@ after @seconds@. It was submitted by + * @submitter@ (might be a null pointer) */ + +void notify_not_scratched(const char *track, + const char *submitter); +/* @track@ (submitted by @submitter@, which might be a null pointer) + * was not scratched. */ + +void notify_queue(const char *track, + const char *submitter); +/* @track@ was queued by @submitter@ */ + +void notify_queue_remove(const char *track, + const char *remover); +/* @track@ removed from the queue by @remover@ (never a null pointer) */ + +void notify_queue_move(const char *track, + const char *mover); +/* @track@ moved in the queue by @mover@ (never a null pointer) */ + +void notify_pause(const char *track, + const char *pauser); +/* TRACK was paused by PAUSER (might be a null pointer) */ + +void notify_resume(const char *track, + const char *resumer); +/* TRACK was resumed by PAUSER (might be a null pointer) */ + +/* track playing **************************************************************/ + +unsigned long play_get_type(const struct plugin *pl); +/* Get the type word for this plugin */ + +void *play_prefork(const struct plugin *pl, + const char *track); +/* Call the prefork function for PL and return the user data */ + +void play_track(const struct plugin *pl, + const char *const *parameters, + int nparameters, + const char *path, + const char *track); +/* play a track. Called inside a fork. */ + +void play_cleanup(const struct plugin *pl, void *data); +/* Call the cleanup function for PL if necessary */ + +int play_pause(const struct plugin *pl, long *playedp, void *data); +/* Pause track. */ + +void play_resume(const struct plugin *pl, void *data); +/* Resume track. */ + +#endif /* PLUGIN_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4dbf7d07d493c66a58f6522f253287b6 */ diff --git a/lib/printf.c b/lib/printf.c new file mode 100644 index 0000000..aafe7ce --- /dev/null +++ b/lib/printf.c @@ -0,0 +1,494 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because byte_snprintf used from log.c */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> +#include <stdlib.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +enum flags { + f_thousands = 1, + f_left = 2, + f_sign = 4, + f_space = 8, + f_hash = 16, + f_zero = 32, + f_width = 256, + f_precision = 512 +}; + +enum lengths { + l_char = 1, + l_short, + l_long, + l_longlong, + l_size_t, + l_intmax_t, + l_ptrdiff_t, + l_longdouble +}; + +struct conversion; + +struct state { + struct sink *output; + int bytes; + va_list *ap; +}; + +struct specifier { + int ch; + int (*check)(const struct conversion *c); + int (*output)(struct state *s, struct conversion *c); + int base; + const char *digits; + const char *xform; +}; + +struct conversion { + unsigned flags; + int width; + int precision; + int length; + const struct specifier *specifier; +}; + +static const char flags[] = "'-+ #0"; + +/* write @nbytes@ to the output. Return -1 on error, 0 on success. + * Keeps track of the number of bytes written. */ +static int do_write(struct state *s, + const void *buffer, + int nbytes) { + if(s->bytes > INT_MAX - nbytes) { +#ifdef EOVERFLOW + errno = EOVERFLOW; +#endif + return -1; + } + if(s->output->write(s->output, buffer, nbytes) < 0) return -1; + s->bytes += nbytes; + return 0; +} + +/* write character @ch@ @n@ times, reasonably efficiently */ +static int do_pad(struct state *s, int ch, unsigned n) { + unsigned t; + const char *padding; + + switch(ch) { + case ' ': padding = " "; break; + case '0': padding = "00000000000000000000000000000000"; break; + default: abort(); + } + t = n / 32; + n %= 32; + while(t-- > 0) + if(do_write(s, padding, 32) < 0) return -1; + if(n > 0) + if(do_write(s, padding, n) < 0) return -1; + return 0; +} + +/* pick up the integer at @ptr@, returning it via @intp@. Return the + * number of characters consumed. Return 0 if there is no integer + * there and -1 if an error occurred (e.g. too big) */ +static int get_integer(int *intp, const char *ptr) { + long n; + char *e; + + errno = 0; + n = strtol(ptr, &e, 10); + if(errno || n > INT_MAX || n < INT_MIN || e == ptr) return -1; + *intp = n; + return e - ptr; +} + +/* consistency checks for various conversion specifications */ + +static int check_integer(const struct conversion *c) { + switch(c->length) { + case 0: + case l_char: + case l_short: + case l_long: + case l_longlong: + case l_intmax_t: + case l_size_t: + case l_longdouble: + return 0; + default: + return -1; + } +} + +static int check_string(const struct conversion *c) { + switch(c->length) { + case 0: + /* XXX don't support %ls, %lc */ + return 0; + default: + return -1; + } +} + +static int check_pointer(const struct conversion *c) { + if(c->length) return -1; + return 0; +} + +static int check_percent(const struct conversion *c) { + if(c->flags || c->width || c->precision || c->length) return -1; + return 0; +} + +/* output functions for various conversion specifications */ + +static int output_percent(struct state *s, + struct conversion attribute((unused)) *c) { + return do_write(s, "%", 1); +} + +static int output_integer(struct state *s, struct conversion *c) { + uintmax_t u; + intmax_t l; + char sign; + int base, dp, iszero, ndigits, prec, xform, sign_bytes, pad; + char digits[CHAR_BIT * sizeof (uintmax_t)]; /* overestimate */ + + switch(c->specifier->ch) { + default: + if(c->specifier->base < 0) { + switch(c->length) { + case 0: l = va_arg(*s->ap, int); break; + case l_char: l = (signed char)va_arg(*s->ap, int); break; + case l_short: l = (short)va_arg(*s->ap, int); break; + case l_long: l = va_arg(*s->ap, long); break; + case l_longlong: l = va_arg(*s->ap, long_long); break; + case l_intmax_t: l = va_arg(*s->ap, intmax_t); break; + case l_size_t: l = va_arg(*s->ap, ssize_t); break; + case l_ptrdiff_t: l = va_arg(*s->ap, ptrdiff_t); break; + default: abort(); + } + base = -c->specifier->base; + if(l < 0) { + u = -l; + sign = '-'; + } else { + u = l; + sign = 0; + } + } else { + switch(c->length) { + case 0: u = va_arg(*s->ap, unsigned int); break; + case l_char: u = (unsigned char)va_arg(*s->ap, unsigned int); break; + case l_short: u = (unsigned short)va_arg(*s->ap, unsigned int); break; + case l_long: u = va_arg(*s->ap, unsigned long); break; + case l_longlong: u = va_arg(*s->ap, u_long_long); break; + case l_intmax_t: u = va_arg(*s->ap, uintmax_t); break; + case l_size_t: u = va_arg(*s->ap, size_t); break; + case l_ptrdiff_t: u = va_arg(*s->ap, ptrdiff_t); break; + default: abort(); + } + base = c->specifier->base; + sign = 0; + } + break; + case 'p': + u = (uintptr_t)va_arg(*s->ap, void *); + c->flags |= f_hash; + base = c->specifier->base; + sign = 0; + break; + } + /* default precision */ + if(!(c->flags & f_precision)) + c->precision = 1; + /* enforce sign */ + if((c->flags & f_sign) && !sign) sign = '+'; + /* compute the digits */ + iszero = (u == 0); + dp = sizeof digits; + while(u) { + digits[--dp] = c->specifier->digits[u % base]; + u /= base; + } + ndigits = sizeof digits - dp; + /* alternative form */ + if(c->flags & f_hash) { + switch(base) { + case 8: + if((dp == sizeof digits || digits[dp] != '0') + && c->precision <= ndigits) + c->precision = ndigits + 1; + break; + } + if(!iszero && c->specifier->xform) + xform = strlen(c->specifier->xform); + else + xform = 0; + } else + xform = 0; + /* calculate number of 0s to add for precision */ + if(ndigits < c->precision) + prec = c->precision - ndigits; + else + prec = 0; + /* bytes occupied by the sign */ + if(sign) + sign_bytes = 1; + else + sign_bytes = 0; + /* XXX implement the ' ' flag */ + /* calculate number of bytes of padding */ + if(c->flags & f_width) { + if((pad = c->width - (ndigits + prec + xform + sign_bytes)) < 0) + pad = 0; + } else + pad = 0; + /* now we are ready to output. Possibilities are: + * [space pad][sign][xform][0 prec]digits + * [sign][xform][0 pad][0 prec]digits + * [sign][xform][0 prec]digits[space pad] + * + * '-' beats '0'. + */ + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + } else if(c->flags & f_zero) { + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(pad && do_pad(s, '0', pad) < 0) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + } else { + if(sign && do_write(s, &sign, 1)) return -1; + if(xform && do_write(s, c->specifier->xform, xform)) return -1; + if(prec && do_pad(s, '0', prec) < 0) return -1; + if(ndigits && do_write(s, digits + dp, ndigits)) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; +} + +static int output_string(struct state *s, struct conversion *c) { + const char *str, *n; + int pad, len; + + str = va_arg(*s->ap, const char *); + if(c->flags & f_precision) { + if((n = memchr(str, 0, c->precision))) + len = n - str; + else + len = c->precision; + } else + len = strlen(str); + if(c->flags & f_width) { + if((pad = c->width - len) < 0) + pad = 0; + } else + pad = 0; + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(do_write(s, str, len) < 0) return -1; + } else { + if(do_write(s, str, len) < 0) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; + +} + +static int output_char(struct state *s, struct conversion *c) { + int pad; + char ch; + + ch = va_arg(*s->ap, int); + if(c->flags & f_width) { + if((pad = c->width - 1) < 0) + pad = 0; + } else + pad = 0; + if(c->flags & f_left) { + if(pad && do_pad(s, ' ', pad) < 0) return -1; + if(do_write(s, &ch, 1) < 0) return -1; + } else { + if(do_write(s, &ch, 1) < 0) return -1; + if(pad && do_pad(s, ' ', pad) < 0) return -1; + } + return 0; +} + +static int output_count(struct state *s, struct conversion *c) { + switch(c->length) { + case 0: *va_arg(*s->ap, int *) = s->bytes; break; + case l_char: *va_arg(*s->ap, signed char *) = s->bytes; break; + case l_short: *va_arg(*s->ap, short *) = s->bytes; break; + case l_long: *va_arg(*s->ap, long *) = s->bytes; break; + case l_longlong: *va_arg(*s->ap, long_long *) = s->bytes; break; + case l_intmax_t: *va_arg(*s->ap, intmax_t *) = s->bytes; break; + case l_size_t: *va_arg(*s->ap, ssize_t *) = s->bytes; break; + case l_ptrdiff_t: *va_arg(*s->ap, ptrdiff_t *) = s->bytes; break; + default: abort(); + } + return 0; +} + +/* table of conversion specifiers */ +static const struct specifier specifiers[] = { + /* XXX don't support floating point conversions */ + { '%', check_percent, output_percent, 0, 0, 0 }, + { 'X', check_integer, output_integer, 16, "0123456789ABCDEF", "0X" }, + { 'c', check_string, output_char, 0, 0, 0 }, + { 'd', check_integer, output_integer, -10, "0123456789", 0 }, + { 'i', check_integer, output_integer, -10, "0123456789", 0 }, + { 'n', check_integer, output_count, 0, 0, 0 }, + { 'o', check_integer, output_integer, 8, "01234567", 0 }, + { 'p', check_pointer, output_integer, 16, "0123456789abcdef", "0x" }, + { 's', check_string, output_string, 0, 0, 0 }, + { 'u', check_integer, output_integer, 10, "0123456789", 0 }, + { 'x', check_integer, output_integer, 16, "0123456789abcdef", "0x" }, +}; + +/* collect and check information about a conversion specification */ +static int parse_conversion(struct conversion *c, const char *ptr) { + int n, ch, l, r, m; + const char *q, *start = ptr; + + memset(c, 0, sizeof *c); + /* flags */ + while(*ptr && (q = strchr(flags, *ptr))) { + c->flags |= (1 << (q - flags)); + ++ptr; + } + /* minimum field width */ + if(*ptr >= '0' && *ptr <= '9') { + if((n = get_integer(&c->width, ptr)) < 0) return -1; + ptr += n; + c->flags |= f_width; + } else if(*ptr == '*') { + ++ptr; + c->width = -1; + c->flags |= f_width; + } + /* precision */ + if(*ptr == '.') { + ++ptr; + if(*ptr >= '0' && *ptr <= '9') { + if((n = get_integer(&c->precision, ptr)) < 0) return -1; + ptr += n; + } else if(*ptr == '*') { + ++ptr; + c->precision = -1; + } else + return -1; + c->flags |= f_precision; + } + /* length modifier */ + switch(ch = *ptr++) { + case 'h': + if((ch = *ptr++) == 'h') { c->length = l_char; ch = *ptr++; } + else c->length = l_short; + break; + case 'l': + if((ch = *ptr++) == 'l') { c->length = l_longlong; ch = *ptr++; } + else c->length = l_long; + break; + case 'q': c->length = l_longlong; ch = *ptr++; break; + case 'j': c->length = l_intmax_t; ch = *ptr++; break; + case 'z': c->length = l_size_t; ch = *ptr++; break; + case 't': c->length = l_ptrdiff_t; ch = *ptr++; break; + case 'L': c->length = l_longdouble; ch = *ptr++; break; + } + /* conversion specifier */ + l = 0; + r = sizeof specifiers / sizeof *specifiers; + while(l <= r && (specifiers[m = (l + r) / 2].ch != ch)) + if(ch < specifiers[m].ch) r = m - 1; + else l = m + 1; + if(specifiers[m].ch != ch) return -1; + if(specifiers[m].check(c)) return -1; + c->specifier = &specifiers[m]; + return ptr - start; +} + +/* ISO/IEC 9899:1999 7.19.6.1 */ +/* http://www.opengroup.org/onlinepubs/009695399/functions/fprintf.html */ + +int byte_vsinkprintf(struct sink *output, + const char *fmt, + va_list ap) { + int n; + const char *ptr; + struct state s; + struct conversion c; + + memset(&s, 0, sizeof s); + s.output = output; + s.ap = ≈ + while(*fmt) { + /* output text up to next conversion specification */ + for(ptr = fmt; *fmt && *fmt != '%'; ++fmt) + ; + if((n = fmt - ptr)) + if(do_write(&s, ptr, n) < 0) return -1; + if(!*fmt) + break; + ++fmt; + /* parse conversion */ + if((n = parse_conversion(&c, fmt)) < 0) return -1; + fmt += n; + /* fill in width and precision */ + if((c.flags & f_width) && c.width == -1) + if((c.width = va_arg(*s.ap, int)) < 0) { + c.width = -c.width; + c.flags |= f_left; + } + if((c.flags & f_precision) && c.precision == -1) + if((c.precision = va_arg(*s.ap, int)) < 0) + c.flags ^= f_precision; + /* generate the output */ + if(c.specifier->output(&s, &c) < 0) return -1; + } + return s.bytes; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e6ce806ce060f1d992eed14d9e5f0a6f */ diff --git a/lib/printf.h b/lib/printf.h new file mode 100644 index 0000000..209615d --- /dev/null +++ b/lib/printf.h @@ -0,0 +1,74 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef PRINTF_H +#define PRINTF_H + +struct sink; + +int byte_vsinkprintf(struct sink *output, + const char *fmt, + va_list ap); +/* partial printf implementation that takes ASCII format strings but + * arbitrary byte strings as args to %s and friends. Lots of bits are + * missing! */ + +int byte_vsnprintf(char buffer[], + size_t bufsize, + const char *fmt, + va_list ap); +int byte_snprintf(char buffer[], + size_t bufsize, + const char *fmt, + ...) + attribute((format (printf, 3, 4))); +/* analogues of [v]snprintf */ + +int byte_vasprintf(char **ptrp, + const char *fmt, + va_list ap); +int byte_asprintf(char **ptrp, + const char *fmt, + ...) + attribute((format (printf, 2, 3))); +/* analogues of [v]asprintf (uses xmalloc/xrealloc) */ + +int byte_xvasprintf(char **ptrp, + const char *fmt, + va_list ap); +int byte_xasprintf(char **ptrp, + const char *fmt, + ...) + attribute((format (printf, 2, 3))); +/* same but terminate on error */ + +int byte_vfprintf(FILE *fp, const char *fmt, va_list ap); +int byte_fprintf(FILE *fp, const char *fmt, ...) + attribute((format (printf, 2, 3))); +/* analogues of [v]fprintf */ + +#endif /* PRINTF_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a3e98b59150e46c340b0ce5f87274458 */ diff --git a/lib/queue.c b/lib/queue.c new file mode 100644 index 0000000..c7d32a4 --- /dev/null +++ b/lib/queue.c @@ -0,0 +1,512 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <time.h> +#include <stddef.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "mem.h" +#include "queue.h" +#include "log.h" +#include "configuration.h" +#include "split.h" +#include "syscalls.h" +#include "charset.h" +#include "table.h" +#include "inputline.h" +#include "printf.h" +#include "plugin.h" +#include "basen.h" +#include "eventlog.h" +#include "disorder.h" + +const char *playing_states[] = { + "failed", + "isscratch", + "no_player", + "ok", + "paused", + "quitting", + "random", + "scratched", + "started", + "unplayed" +}; + +/* the head of the queue is played next, so normally we add to the tail */ +struct queue_entry qhead = { &qhead, &qhead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +/* the head of the recent list is the oldest thing, the tail the most recently + * played */ +struct queue_entry phead = { &phead, &phead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +static long pcount; + +/* add new entry @n@ to a doubly linked list just after @b@ */ +static void l_add(struct queue_entry *b, struct queue_entry *n) { + n->prev = b; + n->next = b->next; + n->next->prev = n; + n->prev->next = n; +} + +/* remove an entry from a doubly-linked list */ +static void l_remove(struct queue_entry *node) { + node->next->prev = node->prev; + node->prev->next = node->next; +} + +#define VALUE(q, offset, type) *(type *)((char *)q + offset) + +static int unmarshall_long(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + if(xstrtol(&VALUE(q, offset, long), data, 0, 0)) { + error_handler(strerror(errno), u); + return -1; + } + return 0; +} + +static const char *marshall_long(const struct queue_entry *q, size_t offset) { + char buffer[256]; + int n; + + n = byte_snprintf(buffer, sizeof buffer, "%ld", VALUE(q, offset, long)); + if(n < 0) + fatal(errno, "error converting int"); + else if((size_t)n >= sizeof buffer) + fatal(0, "long converted to decimal is too long"); + return xstrdup(buffer); +} + +static int unmarshall_string(char *data, struct queue_entry *q, + size_t offset, + void attribute((unused)) (*error_handler)(const char *, void *), + void attribute((unused)) *u) { + VALUE(q, offset, char *) = data; + return 0; +} + +static const char *marshall_string(const struct queue_entry *q, size_t offset) { + return VALUE(q, offset, char *); +} + +static int unmarshall_time_t(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + long_long ul; + + if(xstrtoll(&ul, data, 0, 0)) { + error_handler(strerror(errno), u); + return -1; + } + VALUE(q, offset, time_t) = ul; + return 0; +} + +static const char *marshall_time_t(const struct queue_entry *q, size_t offset) { + char buffer[256]; + int n; + + n = byte_snprintf(buffer, sizeof buffer, + "%"PRIdMAX, (intmax_t)VALUE(q, offset, time_t)); + if(n < 0) + fatal(errno, "error converting time"); + else if((size_t)n >= sizeof buffer) + fatal(0, "time converted to decimal is too long"); + return xstrdup(buffer); +} + +static int unmarshall_state(char *data, struct queue_entry *q, + size_t offset, + void (*error_handler)(const char *, void *), + void *u) { + int n; + + if((n = table_find(playing_states, 0, sizeof (char *), + sizeof playing_states / sizeof *playing_states, + data)) < 0) { + D(("state=[%s] n=%d", data, n)); + error_handler("invalid state", u); + return -1; + } + VALUE(q, offset, enum playing_state) = n; + return 0; +} + +static const char *marshall_state(const struct queue_entry *q, size_t offset) { + return playing_states[VALUE(q, offset, enum playing_state)]; +} + +#define F(n, h) { #n, offsetof(struct queue_entry, n), marshall_##h, unmarshall_##h } + +static const struct field { + const char *name; + size_t offset; + const char *(*marshall)(const struct queue_entry *q, size_t offset); + int (*unmarshall)(char *data, struct queue_entry *q, size_t offset, + void (*error_handler)(const char *, void *), + void *u); +} fields[] = { + /* Keep this table sorted. */ + F(expected, time_t), + F(id, string), + F(played, time_t), + F(scratched, string), + F(sofar, long), + F(state, state), + F(submitter, string), + F(track, string), + F(when, time_t), + F(wstat, long) +}; + +int queue_unmarshall(struct queue_entry *q, const char *s, + void (*error_handler)(const char *, void *), + void *u) { + char **vec; + int nvec; + + if(!(vec = split(s, &nvec, SPLIT_QUOTES, error_handler, u))) + return -1; + return queue_unmarshall_vec(q, nvec, vec, error_handler, u); +} + +int queue_unmarshall_vec(struct queue_entry *q, int nvec, char **vec, + void (*error_handler)(const char *, void *), + void *u) { + int n; + + if(nvec % 2 != 0) { + error_handler("invalid marshalled queue format", u); + return -1; + } + while(*vec) { + D(("key %s value %s", vec[0], vec[1])); + if((n = TABLE_FIND(fields, struct field, name, *vec)) < 0) { + error_handler("unknown key in queue data", u); + return -1; + } else { + if(fields[n].unmarshall(vec[1], q, fields[n].offset, error_handler, u)) + return -1; + } + vec += 2; + } + return 0; +} + +void queue_fix_sofar(struct queue_entry *q) { + long sofar; + + /* Fake up SOFAR field for currently-playing tracks that don't have it filled + * in by the speaker process. XXX this horrible bodge should go away when we + * have a more general implementation of pausing as that field will always + * have to be right for the playing track. */ + if((q->state == playing_started + || q->state == playing_paused) + && q->type & DISORDER_PLAYER_PAUSES + && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW) { + if(q->lastpaused) { + if(q->uptopause == -1) /* Don't know how far thru. */ + sofar = -1; + else if(q->lastresumed) /* Has been paused and resumed. */ + sofar = q->uptopause + time(0) - q->lastresumed; + else /* Currently paused. */ + sofar = q->uptopause; + } else /* Never been paused. */ + sofar = time(0) - q->played; + q->sofar = sofar; + } +} + +char *queue_marshall(const struct queue_entry *q) { + unsigned n; + const char *vec[sizeof fields / sizeof *fields], *v; + char *r, *s; + size_t len = 1; + + for(n = 0; n < sizeof fields / sizeof *fields; ++n) + if((v = fields[n].marshall(q, fields[n].offset))) { + vec[n] = quoteutf8(v); + len += strlen(vec[n]) + strlen(fields[n].name) + 2; + } else + vec[n] = 0; + s = r = xmalloc_noptr(len); + for(n = 0; n < sizeof fields / sizeof *fields; ++n) + if(vec[n]) { + *s++ = ' '; + s += strlen(strcpy(s, fields[n].name)); + *s++ = ' '; + s += strlen(strcpy(s, vec[n])); + } + return r; +} + +static void queue_read_error(const char *msg, + void *u) { + fatal(0, "error parsing queue %s: %s", (const char *)u, msg); +} + +static void queue_do_read(struct queue_entry *head, const char *path) { + char *buffer; + FILE *fp; + struct queue_entry *q; + + if(!(fp = fopen(path, "r"))) { + if(errno == ENOENT) + return; /* no queue */ + fatal(errno, "error opening %s", path); + } + head->next = head->prev = head; + while(!inputline(path, fp, &buffer, '\n')) { + q = xmalloc(sizeof *q); + queue_unmarshall(q, buffer, queue_read_error, (void *)path); + if(head == &qhead + && (!q->track + || !q->when)) + fatal(0, "incomplete queue entry in %s", path); + l_add(head->prev, q); + } + if(ferror(fp)) fatal(errno, "error reading %s", path); + fclose(fp); +} + +void queue_read(void) { + queue_do_read(&qhead, config_get_file("queue")); +} + +void recent_read(void) { + struct queue_entry *q; + + queue_do_read(&phead, config_get_file("recent")); + /* reset pcount after loading */ + pcount = 0; + q = phead.next; + while(q != &phead) { + ++pcount; + q = q->next; + } +} + +static void queue_do_write(const struct queue_entry *head, const char *path) { + char *tmp; + FILE *fp; + struct queue_entry *q; + + byte_xasprintf(&tmp, "%s.new", path); + if(!(fp = fopen(tmp, "w"))) fatal(errno, "error opening %s", tmp); + for(q = head->next; q != head; q = q->next) + if(fprintf(fp, "%s\n", queue_marshall(q)) < 0) + fatal(errno, "error writing %s", tmp); + if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp); + if(rename(tmp, path) < 0) fatal(errno, "error replacing %s", path); +} + +void queue_write(void) { + queue_do_write(&qhead, config_get_file("queue")); +} + +void recent_write(void) { + queue_do_write(&phead, config_get_file("recent")); +} + +void queue_id(struct queue_entry *q) { + static unsigned long serial; + unsigned long a[3]; + char buffer[128]; + + a[0] = serial++ & 0xFFFFFFFFUL; + a[1] = time(0) & 0xFFFFFFFFUL; + a[2] = getpid() & 0xFFFFFFFFUL; + basen(a, 3, buffer, sizeof buffer, 62); + q->id = xstrdup(buffer); +} + +struct queue_entry *queue_add(const char *track, const char *submitter, + int where) { + struct queue_entry *q; + + q = xmalloc(sizeof *q); + q->track = xstrdup(track); + q->submitter = submitter ? xstrdup(submitter) : 0; + q->state = playing_unplayed; + queue_id(q); + time(&q->when); + switch(where) { + case WHERE_START: + l_add(&qhead, q); + break; + case WHERE_END: + l_add(qhead.prev, q); + break; + case WHERE_BEFORE_RANDOM: + if(qhead.prev == &qhead /* Empty queue. */ + || qhead.prev->state != playing_random) /* No random track */ + l_add(qhead.prev, q); + else + l_add(qhead.prev->prev, q); /* Before random track. */ + break; + } + /* submitter will be a null pointer for a scratch */ + if(submitter) + notify_queue(track, submitter); + eventlog_raw("queue", queue_marshall(q), (const char *)0); + return q; +} + +int queue_move(struct queue_entry *q, int delta, const char *who) { + int moved = 0; + char buffer[20]; + + /* not the most efficient approach but hopefuly relatively comprehensible: + * the idea is that for each step we determine which nodes are affected, and + * fill in all the links starting at the 'prev' end and moving towards the + * 'next' end. */ + + while(delta > 0 && q->prev != &qhead) { + struct queue_entry *n, *p, *pp; + + n = q->next; + p = q->prev; + pp = p->prev; + pp->next = q; + q->prev = pp; + q->next = p; + p->prev = q; + p->next = n; + n->prev = p; + --delta; + ++moved; + } + + while(delta < 0 && q->next != &qhead) { + struct queue_entry *n, *p, *nn; + + p = q->prev; + n = q->next; + nn = n->next; + p->next = n; + n->prev = p; + n->next = q; + q->prev = n; + q->next = nn; + nn->prev = q; + ++delta; + --moved; + } + + if(moved) { + info("user %s moved %s", who, q->id); + notify_queue_move(q->track, who); + sprintf(buffer, "%d", moved); + eventlog("moved", who, (char *)0); + } + + return delta; +} + +static int find_in_list(struct queue_entry *needle, + int nqs, struct queue_entry **qs) { + int n; + + for(n = 0; n < nqs; ++n) + if(qs[n] == needle) + return 1; + return 0; +} + +void queue_moveafter(struct queue_entry *target, + int nqs, struct queue_entry **qs, + const char *who) { + struct queue_entry *q; + int n; + + /* Normalize */ + if(!target) + target = &qhead; + else + while(find_in_list(target, nqs, qs)) + target = target->prev; + /* Do the move */ + for(n = 0; n < nqs; ++n) { + q = qs[n]; + l_remove(q); + l_add(target, q); + target = q; + /* Log the individual tracks */ + info("user %s moved %s", who, q->id); + notify_queue_move(q->track, who); + } + /* Report that the queue changed to the event log */ + eventlog("moved", who, (char *)0); +} + +void queue_remove(struct queue_entry *which, const char *who) { + if(who) { + info("user %s removed %s", who, which->id); + notify_queue_move(which->track, who); + } + eventlog("removed", which->id, who, (const char *)0); + l_remove(which); +} + +struct queue_entry *queue_find(const char *key) { + struct queue_entry *q; + + for(q = qhead.next; + q != &qhead && strcmp(q->track, key) && strcmp(q->id, key); + q = q->next) + ; + return q != &qhead ? q : 0; +} + +void queue_played(struct queue_entry *q) { + while(pcount && pcount >= config->history) { + eventlog("recent_removed", phead.next->id, (char *)0); + l_remove(phead.next); + pcount--; + } + if(config->history) { + eventlog_raw("recent_added", queue_marshall(q), (char *)0); + l_add(phead.prev, q); + ++pcount; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:62474dd3e75e5483b61b6befe2c523cc */ diff --git a/lib/queue.h b/lib/queue.h new file mode 100644 index 0000000..5bf1587 --- /dev/null +++ b/lib/queue.h @@ -0,0 +1,137 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef QUEUE_H +#define QUEUE_H + +enum playing_state { + playing_failed, /* failed to play */ + playing_isscratch, /* this is a scratch track */ + playing_no_player, /* couldn't find a player */ + playing_ok, /* played OK */ + playing_paused, /* started but paused */ + playing_quitting, /* interrupt because server quit */ + playing_random, /* unplayed randomly chosen track */ + playing_scratched, /* was scratched */ + playing_started, /* started to play */ + playing_unplayed /* haven't played this track yet */ +}; + +extern const char *playing_states[]; + +/* queue entries form a circular doubly-linked list */ +struct queue_entry { + struct queue_entry *next; /* next entry */ + struct queue_entry *prev; /* previous entry */ + const char *track; /* path to track */ + const char *submitter; /* name of submitter */ + time_t when; /* time submitted */ + time_t played; /* when played */ + enum playing_state state; /* state */ + long wstat; /* wait status */ + const char *scratched; /* scratched by */ + const char *id; /* queue entry ID */ + time_t expected; /* expected started time */ + /* for playing or soon-to-be-played tracks only: */ + unsigned long type; /* type word from plugin */ + const struct plugin *pl; /* plugin that's playing this track */ + void *data; /* player data */ + long sofar; /* how much played so far */ + /* For DISORDER_PLAYER_PAUSES only: */ + time_t lastpaused, lastresumed; /* when last paused/resumed, or 0 */ + long uptopause; /* how much played up to last pause */ + /* For Disobedience */ + struct queuelike *ql; /* owning queue */ +}; + +extern struct queue_entry qhead; +/* queue of things yet to be played. the head will be played + * soonest. */ + +extern struct queue_entry phead; +/* things that have been played in the past. the head is the oldest. */ + +void queue_read(void); +/* read the queue in. Calls @fatal@ on error. */ + +void queue_write(void); +/* write the queue out. Calls @fatal@ on error. */ + +void recent_read(void); +/* read the recently played list in. Calls @fatal@ on error. */ + +void recent_write(void); +/* write the recently played list out. Calls @fatal@ on error. */ + +struct queue_entry *queue_add(const char *track, const char *submitter, + int where); +#define WHERE_START 0 /* Add to head of queue */ +#define WHERE_END 1 /* Add to end of queue */ +#define WHERE_BEFORE_RANDOM 2 /* End, or before random track */ +/* add an entry to the queue. Return a pointer to the new entry. */ + +void queue_remove(struct queue_entry *q, const char *who); +/* remove an from the queue */ + +struct queue_entry *queue_find(const char *key); +/* find a track in the queue by name or ID */ + +void queue_played(struct queue_entry *q); +/* add @q@ to the played list */ + +int queue_unmarshall(struct queue_entry *q, const char *s, + void (*error_handler)(const char *, void *), + void *u); +/* unmarshall UTF-8 string @s@ into @q@ */ + +int queue_unmarshall_vec(struct queue_entry *q, int nvec, char **vec, + void (*error_handler)(const char *, void *), + void *u); +/* unmarshall pre-split string @vec@ into @q@ */ + +char *queue_marshall(const struct queue_entry *q); +/* marshall @q@ into a UTF-8 string */ + +void queue_id(struct queue_entry *q); +/* give @q@ an ID */ + +int queue_move(struct queue_entry *q, int delta, const char *who); +/* move element @q@ in the queue towards the front (@delta@ > 0) or towards the + * back (@delta@ < 0). The return value is the leftover delta once we've hit + * the end in whichever direction we were going. */ + +void queue_moveafter(struct queue_entry *target, + int nqs, struct queue_entry **qs, const char *who); +/* Move all the elements QS to just after TARGET, or to the head if + * TARGET=0. */ + +void queue_fix_sofar(struct queue_entry *q); +/* Fix up the sofar field for standalone players */ + +#endif /* QUEUE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:23ec4c111fdd6573a0adc8c366b87e7b */ diff --git a/lib/regsub.c b/lib/regsub.c new file mode 100644 index 0000000..bad6034 --- /dev/null +++ b/lib/regsub.c @@ -0,0 +1,166 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <pcre.h> + +#include "regsub.h" +#include "mem.h" +#include "vector.h" +#include "log.h" + +#define PREMATCH (-1) /* fictitious pre-match substring */ +#define POSTMATCH (-2) /* fictitious post-match substring */ + +static inline int substring_start(const char attribute((unused)) *subject, + const int *ovector, + int n) { + switch(n) { + case PREMATCH: return 0; + case POSTMATCH: return ovector[1]; + default: return ovector[2 * n]; + } +} + +static inline int substring_end(const char *subject, + const int *ovector, + int n) { + switch(n) { + case PREMATCH: return ovector[0]; + case POSTMATCH: return strlen(subject); + default: return ovector[2 * n + 1]; + } +} + +static void transform_append(struct dynstr *d, + const char *subject, + const int *ovector, + int n) { + int start = substring_start(subject, ovector, n); + int end = substring_end(subject, ovector, n); + + if(start != -1) + dynstr_append_bytes(d, subject + start, end - start); +} + +static void replace_core(struct dynstr *d, + const char *subject, + const char *replace, + int rc, + const int *ovector) { + int substr; + + while(*replace) { + if(*replace == '$') + switch(replace[1]) { + case '&': + transform_append(d, subject, ovector, 0); + replace += 2; + break; + case '1': case '2': case '3': + case '4': case '5': case '6': + case '7': case '8': case '9': + substr = replace[1] - '0'; + if(substr < rc) + transform_append(d, subject, ovector, substr); + replace += 2; + break; + case '$': + dynstr_append(d, '$'); + replace += 2; + break; + default: + dynstr_append(d, *replace++); + break; + } + else + dynstr_append(d, *replace++); + } +} + +unsigned regsub_flags(const char *flags) { + unsigned f = 0; + + while(*flags) { + switch(*flags++) { + case 'g': f |= REGSUB_GLOBAL; break; + case 'i': f |= REGSUB_CASE_INDEPENDENT; break; + default: break; + } + } + return f; +} + +int regsub_compile_options(unsigned flags) { + int options = 0; + + if(flags & REGSUB_CASE_INDEPENDENT) + options |= PCRE_CASELESS; + return options; +} + +const char *regsub(const pcre *re, const char *subject, const char *replace, + unsigned flags) { + int rc, ovector[99], matches; + struct dynstr d; + + dynstr_init(&d); + matches = 0; + /* find the next match */ + while((rc = pcre_exec(re, 0, subject, strlen(subject), 0, + 0, ovector, sizeof ovector / sizeof (int))) > 0) { + /* text just before the match */ + if(!(flags & REGSUB_REPLACE)) + transform_append(&d, subject, ovector, PREMATCH); + /* the replacement text */ + replace_core(&d, subject, replace, rc, ovector); + ++matches; + if(!*subject) /* end of subject */ + break; + if(flags & REGSUB_REPLACE) /* replace subject entirely */ + break; + /* step over the matched substring */ + subject += substring_start(subject, ovector, POSTMATCH); + if(!(flags & REGSUB_GLOBAL)) + break; + } + if(rc <= 0 && rc != PCRE_ERROR_NOMATCH) { + error(0, "pcre_exec returned %d, subject '%s'", rc, subject); + return 0; + } + if((flags & REGSUB_MUST_MATCH) && matches == 0) + return 0; + /* append the remainder of the subject */ + if(!(flags & REGSUB_REPLACE)) + dynstr_append_string(&d, subject); + dynstr_terminate(&d); + return d.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:69b3b3dafcbaa25619a77a33c44ad699 */ diff --git a/lib/regsub.h b/lib/regsub.h new file mode 100644 index 0000000..683a85b --- /dev/null +++ b/lib/regsub.h @@ -0,0 +1,46 @@ +/* + * This file is part of DisOrder +* Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef REGSUB_H +#define REGSUB_H + +#define REGSUB_GLOBAL 0x0001 /* global replace */ +#define REGSUB_MUST_MATCH 0x0002 /* return 0 if no match */ +#define REGSUB_CASE_INDEPENDENT 0x0004 /* case independent */ +#define REGSUB_REPLACE 0x0008 /* replace whole, not match */ + +unsigned regsub_flags(const char *flags); +/* parse a flag string */ + +int regsub_compile_options(unsigned flags); +/* convert compile-time options */ + +const char *regsub(const pcre *re, const char *subject, const char *replace, + unsigned flags); + +#endif /* REGSUB_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:cc77c5fd9a947d224b59167ed9eaa360 */ diff --git a/lib/selection.c b/lib/selection.c new file mode 100644 index 0000000..ad46cb3 --- /dev/null +++ b/lib/selection.c @@ -0,0 +1,87 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "mem.h" +#include "hash.h" +#include "selection.h" + +hash *selection_new(void) { + return hash_new(sizeof (int)); +} + +void selection_set(hash *h, const char *key, int selected) { + if(selected) + hash_add(h, key, xmalloc_noptr(sizeof (int)), HASH_INSERT_OR_REPLACE); + else + hash_remove(h, key); +} + +int selection_selected(hash *h, const char *key) { + return hash_find(h, key) != 0; +} + +void selection_flip(hash *h, const char *key) { + selection_set(h, key, !selection_selected(h, key)); +} + +void selection_live(hash *h, const char *key) { + int *ptr = hash_find(h, key); + + if(ptr) + *ptr = 1; +} + +static int selection_cleanup_callback(const char *key, + void *value, + void *v) { + if(*(int *)value) + *(int *)value = 0; + else + hash_remove((hash *)v, key); + return 0; +} + +void selection_cleanup(hash *h) { + hash_foreach(h, selection_cleanup_callback, h); +} + +static int selection_empty_callback(const char *key, + void attribute((unused)) *value, + void *v) { + hash_remove((hash *)v, key); + return 0; +} + +void selection_empty(hash *h) { + hash_foreach(h, selection_empty_callback, h); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:ZUNwKZppAUsZ2KQ9yLedIg */ diff --git a/lib/selection.h b/lib/selection.h new file mode 100644 index 0000000..b358eb3 --- /dev/null +++ b/lib/selection.h @@ -0,0 +1,56 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SELECTION_H +#define SELECTION_H + +/* Represent a selection using a hash */ + +hash *selection_new(void); + +void selection_set(hash *h, const char *key, int selected); +/* Set the selection status of KEY */ + +void selection_flip(hash *h, const char *key); +/* Flip the selection status of KEY */ + +int selection_selected(hash *h, const char *key); +/* Test whether KEY is selected. Always returns 0 or 1. */ + +void selection_live(hash *h, const char *key); +/* Mark KEY as live. Ignored if KEY is not selected. */ + +void selection_cleanup(hash *h); +/* Discard dead items (and mark everything left as dead) */ + +void selection_empty(hash *h); +/* Empty the selection */ + +#endif /* SELECTION_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:es7QFEo6xhzYHZ6+Ejos/A */ diff --git a/lib/signame.c b/lib/signame.c new file mode 100644 index 0000000..19e0662 --- /dev/null +++ b/lib/signame.c @@ -0,0 +1,154 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <signal.h> +#include <stddef.h> + +#include "table.h" +#include "signame.h" + +static const struct sigtable { + int signal; + const char *name; +} signals[] = { +#define S(sig) { sig, #sig } + /* table must be kept in lexical order */ +#ifdef SIGABRT + S(SIGABRT), +#endif +#ifdef SIGALRM + S(SIGALRM), +#endif +#ifdef SIGBUS + S(SIGBUS), +#endif +#ifdef SIGCHLD + S(SIGCHLD), +#endif +#ifdef SIGCONT + S(SIGCONT), +#endif +#ifdef SIGFPE + S(SIGFPE), +#endif +#ifdef SIGHUP + S(SIGHUP), +#endif +#ifdef SIGILL + S(SIGILL), +#endif +#ifdef SIGINT + S(SIGINT), +#endif +#ifdef SIGIO + S(SIGIO), +#endif +#ifdef SIGIOT + S(SIGIOT), +#endif +#ifdef SIGKILL + S(SIGKILL), +#endif +#ifdef SIGPIPE + S(SIGPIPE), +#endif +#ifdef SIGPOLL + S(SIGPOLL), +#endif +#ifdef SIGPROF + S(SIGPROF), +#endif +#ifdef SIGPWR + S(SIGPWR), +#endif +#ifdef SIGQUIT + S(SIGQUIT), +#endif +#ifdef SIGSEGV + S(SIGSEGV), +#endif +#ifdef SIGSTKFLT + S(SIGSTKFLT), +#endif +#ifdef SIGSTOP + S(SIGSTOP), +#endif +#ifdef SIGSYS + S(SIGSYS), +#endif +#ifdef SIGTERM + S(SIGTERM), +#endif +#ifdef SIGTRAP + S(SIGTRAP), +#endif +#ifdef SIGTSTP + S(SIGTSTP), +#endif +#ifdef SIGTTIN + S(SIGTTIN), +#endif +#ifdef SIGTTOU + S(SIGTTOU), +#endif +#ifdef SIGURG + S(SIGURG), +#endif +#ifdef SIGUSR1 + S(SIGUSR1), +#endif +#ifdef SIGUSR2 + S(SIGUSR2), +#endif +#ifdef SIGVTALRM + S(SIGVTALRM), +#endif +#ifdef SIGWINCH + S(SIGWINCH), +#endif +#ifdef SIGXCPU + S(SIGXCPU), +#endif +#ifdef SIGXFSZ + S(SIGXFSZ), +#endif +#undef S +}; + +int find_signal(const char *s) { + int n; + + if((n = TABLE_FIND(signals, struct sigtable, name, s)) < 0) + return -1; + return signals[n].signal; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:6D5fEI6CzWvDXNVeX/1qDg */ diff --git a/lib/signame.h b/lib/signame.h new file mode 100644 index 0000000..a4d74d4 --- /dev/null +++ b/lib/signame.h @@ -0,0 +1,37 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SIGNAME_H +#define SIGNAME_H + +int find_signal(const char *s); +/* Map S to a signal number */ + +#endif /* SIGNAME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:/w7s28ztfuoDjF8iE/rT3A */ diff --git a/lib/sink.c b/lib/sink.c new file mode 100644 index 0000000..e54533a --- /dev/null +++ b/lib/sink.c @@ -0,0 +1,105 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> + +#include "mem.h" +#include "vector.h" +#include "sink.h" +#include "log.h" +#include "printf.h" + +int sink_vprintf(struct sink *s, const char *fmt, va_list ap) { + return byte_vsinkprintf(s, fmt, ap); +} + +int sink_printf(struct sink *s, const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = byte_vsinkprintf(s, fmt, ap); + va_end(ap); + return n; +} + +/* stdio sink *****************************************************************/ + +struct stdio_sink { + struct sink s; + const char *name; + FILE *fp; +}; + +#define S(s) ((struct stdio_sink *)s) + +static int sink_stdio_write(struct sink *s, const void *buffer, int nbytes) { + int n = fwrite(buffer, 1, nbytes, S(s)->fp); + if(n < nbytes) { + if(S(s)->name) + fatal(errno, "error writing to %s", S(s)->name); + else + return -1; + } + return n; +} + +struct sink *sink_stdio(const char *name, FILE *fp) { + struct stdio_sink *s = xmalloc(sizeof *s); + + s->s.write = sink_stdio_write; + s->name = name; + s->fp = fp; + return (struct sink *)s; +} + +/* dynstr sink ****************************************************************/ + +struct dynstr_sink { + struct sink s; + struct dynstr *d; +}; + +static int sink_dynstr_write(struct sink *s, const void *buffer, int nbytes) { + dynstr_append_bytes(((struct dynstr_sink *)s)->d, buffer, nbytes); + return nbytes; +} + +struct sink *sink_dynstr(struct dynstr *output) { + struct dynstr_sink *s = xmalloc(sizeof *s); + + s->s.write = sink_dynstr_write; + s->d = output; + return (struct sink *)s; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:10f08a9ea316137ef7259088403a636e */ diff --git a/lib/sink.h b/lib/sink.h new file mode 100644 index 0000000..f19a0c6 --- /dev/null +++ b/lib/sink.h @@ -0,0 +1,67 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SINK_H +#define SINK_H + +/* a sink is something you write to (the opposite would be a source) */ + +struct dynstr; + +struct sink { + int (*write)(struct sink *s, const void *buffer, int nbytes); + /* return >= 0 on success, -1 on error */ +}; + +struct sink *sink_stdio(const char *name, FILE *fp); +/* return a sink which writes to @fp@. If @name@ is not a null + * pointer, it will be used in (fatal) error messages; if it is a null + * pointer then errors will be signalled by returning -1. */ + +struct sink *sink_dynstr(struct dynstr *output); +/* return a sink which appends to @output@. */ + +int sink_vprintf(struct sink *s, const char *fmt, va_list ap); +int sink_printf(struct sink *s, const char *fmt, ...) + attribute((format (printf, 2, 3))); +/* equivalent of vfprintf/fprintf for sink @s@ */ + +static inline int sink_write(struct sink *s, const void *buffer, int nbytes) { + return s->write(s, buffer, nbytes); +} + +static inline int sink_writes(struct sink *s, const char *str) { + return s->write(s, str, strlen(str)); +} + +static inline int sink_writec(struct sink *s, char c) { + return s->write(s, &c, 1); +} + +#endif /* SINK_H */ + + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2f4f2c2a4e65aef8f7c59b4785121083 */ diff --git a/lib/snprintf.c b/lib/snprintf.c new file mode 100644 index 0000000..f16b047 --- /dev/null +++ b/lib/snprintf.c @@ -0,0 +1,97 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#define NO_MEMORY_ALLOCATION +/* because used from log.c */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdarg.h> +#include <stddef.h> + +#include "printf.h" +#include "sink.h" + +struct fixedstr_sink { + struct sink s; + char *buffer; + int nbytes; + size_t size; +}; + +static int fixedstr_write(struct sink *f, const void *buffer, int nbytes) { + struct fixedstr_sink *s = (struct fixedstr_sink *)f; + int count; + + if((size_t)s->nbytes < s->size) { + if((size_t)nbytes > s->size - s->nbytes) + count = s->size - s->nbytes; + else + count = nbytes; + memcpy(s->buffer + s->nbytes, buffer, count); + } + s->nbytes += nbytes; + return 0; +} + +int byte_vsnprintf(char buffer[], + size_t bufsize, + const char *fmt, + va_list ap) { + struct fixedstr_sink s; + int n, m; + + /* We have to make a sink directly here, since we can't safely do memory + * allocation here (we might be formatting the error message from a failed + * memory allocation) */ + s.s.write = fixedstr_write; + s.buffer = buffer; + s.nbytes = 0; + s.size = bufsize; + n = byte_vsinkprintf(&s.s, fmt, ap); + if(bufsize) { + /* add the null terminator (even if the printf failed) */ + m = s.nbytes; + if((size_t)m >= bufsize) m = bufsize - 1; + buffer[m] = 0; + } + return n; +} + +int byte_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) { + int n; + va_list ap; + + va_start(ap, fmt); + n = byte_vsnprintf(buffer, bufsize, fmt, ap); + va_end(ap); + return n; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4f73357e08ed449f25b5b2c5a6b9ada8 */ diff --git a/lib/speaker.c b/lib/speaker.c new file mode 100644 index 0000000..401bbe5 --- /dev/null +++ b/lib/speaker.c @@ -0,0 +1,109 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/socket.h> +#include <string.h> +#include <errno.h> +#include <sys/uio.h> + +#include "speaker.h" +#include "log.h" + +void speaker_send(int fd, const struct speaker_message *sm, int datafd) { + struct msghdr m; + struct iovec iov; + union { + struct cmsghdr cmsg; + char size[CMSG_SPACE(sizeof (int))]; + } u; + int ret; + + memset(&m, 0, sizeof m); + m.msg_iov = &iov; + m.msg_iovlen = 1; + iov.iov_base = (void *)sm; + iov.iov_len = sizeof *sm; + if(datafd != -1) { + m.msg_control = (void *)&u.cmsg; + m.msg_controllen = sizeof u; + memset(&u, 0, sizeof u); + u.cmsg.cmsg_len = CMSG_LEN(sizeof (int)); + u.cmsg.cmsg_level = SOL_SOCKET; + u.cmsg.cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(&u.cmsg) = datafd; + } + do { + ret = sendmsg(fd, &m, 0); + } while(ret < 0 && errno == EINTR); + if(ret < 0) + fatal(errno, "sendmsg"); +} + +int speaker_recv(int fd, struct speaker_message *sm, int *datafd) { + struct msghdr m; + struct iovec iov; + union { + struct cmsghdr cmsg; + char size[CMSG_SPACE(sizeof (int))]; + } u; + int ret; + + memset(&m, 0, sizeof m); + m.msg_iov = &iov; + m.msg_iovlen = 1; + iov.iov_base = (void *)sm; + iov.iov_len = sizeof *sm; + if(datafd) { + m.msg_control = (void *)&u.cmsg; + m.msg_controllen = sizeof u; + memset(&u, 0, sizeof u); + u.cmsg.cmsg_len = CMSG_LEN(sizeof (int)); + u.cmsg.cmsg_level = SOL_SOCKET; + u.cmsg.cmsg_type = SCM_RIGHTS; + *datafd = -1; + } + do { + ret = recvmsg(fd, &m, MSG_DONTWAIT); + } while(ret < 0 && errno == EINTR); + if(ret < 0) { + if(errno != EAGAIN) fatal(errno, "recvmsg"); + return -1; + } + if((size_t)m.msg_controllen >= CMSG_LEN(sizeof (int))) { + if(!datafd) + fatal(0, "got an unexpected file descriptor from recvmsg"); + else + *datafd = *(int *)CMSG_DATA(&u.cmsg); + } + return ret; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:VMDANBsSj5JSGuDZIL/zUw */ diff --git a/lib/speaker.h b/lib/speaker.h new file mode 100644 index 0000000..ef1e849 --- /dev/null +++ b/lib/speaker.h @@ -0,0 +1,61 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SPEAKER_H +#define SPEAKER_H + +struct speaker_message { + int type; /* message type */ + long data; /* whatever */ + char id[24]; /* ID including terminator */ +}; + +/* messages from the main DisOrder server */ +#define SM_PREPARE 0 /* prepare ID */ +#define SM_PLAY 1 /* play ID */ +#define SM_PAUSE 2 /* pause current track */ +#define SM_RESUME 3 /* resume current track */ +#define SM_CANCEL 4 /* cancel ID */ +#define SM_RELOAD 5 /* reload configuration */ + +/* messages from the speaker */ +#define SM_PAUSED 128 /* paused ID, DATA seconds in */ +#define SM_FINISHED 129 /* finished ID */ +#define SM_PLAYING 131 /* playing ID, DATA seconds in */ + +void speaker_send(int fd, const struct speaker_message *sm, int datafd); +/* Send a message. DATAFD is passed too if not -1. Does not close DATAFD. */ + +int speaker_recv(int fd, struct speaker_message *sm, int *datafd); +/* Receive a message. If DATAFD is not null then can receive an FD. Return 0 + * on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other + * error. */ + +#endif /* SPEAKER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:QpuCTaKRDXEwz+Df/Jt+Wg */ diff --git a/lib/split.c b/lib/split.c new file mode 100644 index 0000000..dbf7a48 --- /dev/null +++ b/lib/split.c @@ -0,0 +1,169 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <ctype.h> +#include <string.h> +#include <errno.h> + +#include "mem.h" +#include "split.h" +#include "log.h" +#include "charset.h" +#include "vector.h" + +static inline int space(int c) { + return (c == ' ' + || c == '\t' + || c == '\n' + || c == '\r'); +} + +static void no_error_handler(const char attribute((unused)) *msg, + void attribute((unused)) *u) { +} + +char **split(const char *p, + int *np, + unsigned flags, + void (*error_handler)(const char *msg, void *u), + void *u) { + char *f, *g; + const char *q; + struct vector v; + size_t l; + int qc; + + if(!error_handler) error_handler = no_error_handler; + vector_init(&v); + while(*p && !(*p == '#' && (flags & SPLIT_COMMENTS))) { + if(space(*p)) { + ++p; + continue; + } + if((flags & SPLIT_QUOTES) && (*p == '"' || *p == '\'')) { + qc = *p++; + l = 0; + for(q = p; *q && *q != qc; ++q) { + if(*q == '\\' && q[1]) + ++q; + ++l; + } + if(!*q) { + error_handler("unterminated quoted string", u); + return 0; + } + f = g = xmalloc_noptr(l + 1); + for(q = p; *q != qc;) { + if(*q == '\\') { + ++q; + switch(*q) { + case '\\': + case '"': + case '\'': + *g++ = *q++; + break; + case 'n': + ++q; + *g++ = '\n'; + break; + default: + error_handler("illegal escape sequence", u); + return 0; + } + } else + *g++ = *q++; + } + *g = 0; + p = q + 1; + } else { + for(q = p; *q && !space(*q); ++q) + ; + l = q - p; + f = xstrndup(p, l); + p = q; + } + vector_append(&v, f); + } + vector_terminate(&v); + if(np) + *np = v.nvec; + return v.vec; +} + +const char *quoteutf8(const char *s) { + size_t len = 3 + strlen(s); + const char *t; + char *r, *q; + + /* see if we need to quote */ + if(*s) { + for(t = s; *t; t++) + if((unsigned char)*t <= ' ' + || *t == '"' + || *t == '\\' + || *t == '\'' + || *t == '#') + break; + if(!*t) + return s; + } + + /* we rely on ASCII characters only ever representing themselves in UTF-8. */ + for(t = s; *t; t++) { + switch(*t) { + case '"': + case '\\': + case '\n': + ++len; + break; + } + } + q = r = xmalloc_noptr(len); + *q++ = '"'; + for(t = s; *t; t++) { + switch(*t) { + case '"': + case '\\': + *q++ = '\\'; + /* fall through */ + default: + *q++ = *t; + break; + case '\n': + *q++ = '\\'; + *q++ = 'n'; + break; + } + } + *q++ = '"'; + *q = 0; + return r; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:768e4e1bc91d9f45d6beecc9b433992f */ diff --git a/lib/split.h b/lib/split.h new file mode 100644 index 0000000..2344f87 --- /dev/null +++ b/lib/split.h @@ -0,0 +1,31 @@ +#ifndef SPLIT_H +#define SPLIT_H + +#define SPLIT_COMMENTS 0001 /* # starts a comment */ +#define SPLIT_QUOTES 0002 /* " and ' quote strings */ + +char **split(const char *s, + int *np, + unsigned flags, + void (*error_handler)(const char *msg, void *u), + void *u); +/* split @s@ up into fields. Return a null-pointer-terminated array + * of pointers to the fields. If @np@ is not a null pointer store the + * number of fields there. Calls @error_handler@ to report any + * errors. + * + * split() operates on UTF-8 strings. + */ + +const char *quoteutf8(const char *s); +/* quote a UTF-8 string. Might return @s@ if no quoting is required. */ + +#endif /* SPLIT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:b1e012fb5925c578672b122a3fc685cb */ diff --git a/lib/syscalls.c b/lib/syscalls.c new file mode 100644 index 0000000..ddfcb46 --- /dev/null +++ b/lib/syscalls.c @@ -0,0 +1,151 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> + +#include "syscalls.h" +#include "log.h" +#include "printf.h" + +int mustnotbeminus1(const char *what, int ret) { + if(ret == -1) + fatal(errno, "error calling %s", what); + return ret; +} + +pid_t xfork(void) { + pid_t pid; + + if((pid = fork()) < 0) fatal(errno, "error calling fork"); + return pid; +} + +void xclose(int fd) { + mustnotbeminus1("close", close(fd)); +} + +void xdup2(int fd1, int fd2) { + mustnotbeminus1("dup2", dup2(fd1, fd2)); +} + +void xpipe(int *fdp) { + mustnotbeminus1("pipe", pipe(fdp)); +} + +void nonblock(int fd) { + mustnotbeminus1("fcntl F_SETFL", + fcntl(fd, F_SETFL, + mustnotbeminus1("fcntl F_GETFL", + fcntl(fd, F_GETFL)) | O_NONBLOCK)); +} + +void cloexec(int fd) { + mustnotbeminus1("fcntl F_SETFD", + fcntl(fd, F_SETFD, + mustnotbeminus1("fcntl F_GETFD", + fcntl(fd, F_GETFD)) | FD_CLOEXEC)); +} + +void xlisten(int fd, int q) { + mustnotbeminus1("listen", listen(fd, q)); +} + +void xshutdown(int fd, int how) { + mustnotbeminus1("shutdown", shutdown(fd, how)); +} + +void xsetsockopt(int fd, int l, int o, const void *v, socklen_t vl) { + mustnotbeminus1("setsockopt", setsockopt(fd, l, o, v, vl)); +} + +int xsocket(int d, int t, int p) { + return mustnotbeminus1("socket", socket(d, t, p)); +} + +void xconnect(int fd, const struct sockaddr *sa, socklen_t sl) { + mustnotbeminus1("connect", connect(fd, sa, sl)); +} + +void xsigprocmask(int how, const sigset_t *set, sigset_t *oldset) { + mustnotbeminus1("sigprocmask", sigprocmask(how, set, oldset)); +} + +void xsigaction(int sig, const struct sigaction *sa, struct sigaction *oldsa) { + mustnotbeminus1("sigaction", sigaction(sig, sa, oldsa)); +} + +int xprintf(const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = mustnotbeminus1("byte_vfprintf", byte_vfprintf(stdout, fmt, ap)); + va_end(ap); + return n; +} + +void xfclose(FILE *fp) { + mustnotbeminus1("fclose", fclose(fp)); +} + +int xstrtol(long *n, const char *s, char **ep, int base) { + errno = 0; + *n = strtol(s, ep, base); + return errno; +} + +int xstrtoll(long_long *n, const char *s, char **ep, int base) { + errno = 0; + *n = strtoll(s, ep, base); + return errno; +} + +int xnice(int inc) { + int ret; + + /* some versions of nice() return the new nice value which in principle could + * be -1 */ + errno = 0; + ret = nice(inc); + if(errno) fatal(errno, "error calling nice"); + return ret; +} + +void xgettimeofday(struct timeval *tv, struct timezone *tz) { + mustnotbeminus1("gettimeofday", gettimeofday(tv, tz)); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0be4384b4081d464d1a2fad746469d3d */ diff --git a/lib/syscalls.h b/lib/syscalls.h new file mode 100644 index 0000000..2ebffd9 --- /dev/null +++ b/lib/syscalls.h @@ -0,0 +1,74 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SYSCALLS_H +#define SYSCALLS_H + +/* various error-handling wrappers. Not actually all system calls! */ + +struct sockaddr; +struct sigaction; +struct timezone; + +#include <sys/socket.h> +#include <signal.h> +#include <stdio.h> + +#include "types.h" + +pid_t xfork(void); +void xclose(int); +void xdup2(int, int); +void xpipe(int *); +int xfcntl(int, int, long); +void xlisten(int, int); +void xshutdown(int, int); +void xsetsockopt(int, int, int, const void *, socklen_t); +int xsocket(int, int, int); +void xconnect(int, const struct sockaddr *, socklen_t); +void xsigprocmask(int how, const sigset_t *set, sigset_t *oldset); +void xsigaction(int sig, const struct sigaction *sa, struct sigaction *oldsa); +int xprintf(const char *, ...) + attribute((format (printf, 1, 2))); +void xfclose(FILE *); +int xnice(int); +void xgettimeofday(struct timeval *, struct timezone *); +/* the above all call @fatal@ if the system call fails */ + +void nonblock(int fd); +void cloexec(int fd); +/* make @fd@ non-blocking/close-on-exec; call @fatal@ on error. */ + +int mustnotbeminus1(const char *what, int value); +/* If @value@ is -1, report an error including @what@. */ + +int xstrtol(long *n, const char *s, char **ep, int base); +int xstrtoll(long_long *n, const char *s, char **ep, int base); +/* like strtol() but returns errno on error, 0 on success */ + +#endif /* SYSCALLS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:87545481469f8a85f73c5216c6788c0e */ diff --git a/lib/table.c b/lib/table.c new file mode 100644 index 0000000..1f30980 --- /dev/null +++ b/lib/table.c @@ -0,0 +1,50 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> + +#include "table.h" + +int table_find(const void *table, size_t offset, size_t eltsize, size_t nelts, + const char *name) { + int l = 0, r = (int)nelts - 1, c, m; + const char *k; + + while(l <= r) { + k = *(const char **)((char *)table + offset + eltsize * (m = (l + r) / 2)); + if(!(c = strcmp(name, k))) + return m; + if(c < 0) + r = m - 1; + else + l = m + 1; + } + return -1; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0cc7ee1d25ae5e35f8dbd2460c9f4afa */ diff --git a/lib/table.h b/lib/table.h new file mode 100644 index 0000000..476399d --- /dev/null +++ b/lib/table.h @@ -0,0 +1,46 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TABLE_H +#define TABLE_H + +#define TABLE_FIND(TABLE, TYPE, FIELD, NAME) \ + table_find((void *)TABLE, \ + offsetof(TYPE, FIELD), \ + sizeof (TYPE), \ + sizeof TABLE / sizeof (TYPE), \ + NAME) +/* Search TYPE TABLE[] for an element where TABLE[N].FIELD matches NAME + * Returns the index N on success or -1 if not found + * The table must be lexically sorted on FIELD + */ + +int table_find(const void *table, size_t offset, size_t eltsize, size_t nelts, + const char *name); + +#endif /* TABLE_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:15b07f98a592f80e4e22dd3f213f2580 */ diff --git a/lib/test.c b/lib/test.c new file mode 100644 index 0000000..6eee1c9 --- /dev/null +++ b/lib/test.c @@ -0,0 +1,418 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <ctype.h> + +#include "utf8.h" +#include "mem.h" +#include "log.h" +#include "vector.h" +#include "charset.h" +#include "mime.h" +#include "hex.h" +#include "words.h" + +static int tests, errors; + +#define insist(expr) do { \ + if(!expr) { \ + ++errors; \ + fprintf(stderr, "%s:%d: error checking %s\n", \ + __FILE__, __LINE__, #expr); \ + } \ + ++tests; \ +} while(0) + +static const char *format(const char *s) { + struct dynstr d; + int c; + char buf[10]; + + dynstr_init(&d); + while((c = (unsigned char)*s++)) { + if(c >= ' ' && c <= '~') + dynstr_append(&d, c); + else { + sprintf(buf, "\\x%02X", (unsigned)c); + dynstr_append_string(&d, buf); + } + } + dynstr_terminate(&d); + return d.vec; +} + +#define check_string(GOT, WANT) do { \ + const char *g = GOT; \ + const char *w = WANT; \ + \ + if(w == 0) { \ + fprintf(stderr, "%s:%d: %s returned 0\n", \ + __FILE__, __LINE__, #GOT); \ + ++errors; \ + } else if(strcmp(w, g)) { \ + fprintf(stderr, "%s:%d: %s returned:\n%s\nexpected:\n%s\n", \ + __FILE__, __LINE__, #GOT, format(g), format(w)); \ + ++errors; \ + } \ + ++tests; \ + } while(0) + +static uint32_t *ucs4parse(const char *s) { + struct dynstr_ucs4 d; + char *e; + + dynstr_ucs4_init(&d); + while(*s) { + errno = 0; + dynstr_ucs4_append(&d, strtoul(s, &e, 0)); + if(errno) fatal(errno, "strtoul (%s)", s); + s = e; + } + dynstr_ucs4_terminate(&d); + return d.vec; +} + +static void test_utf8(void) { + /* Test validutf8, convert to UCS-4, check the answer is right, + * convert back to UTF-8, check we got to where we started */ +#define U8(CHARS, WORDS) do { \ + uint32_t *w = ucs4parse(WORDS); \ + uint32_t *ucs; \ + char *u8; \ + \ + insist(validutf8(CHARS)); \ + ucs = utf82ucs4(CHARS); \ + insist(ucs != 0); \ + insist(!ucs4cmp(w, ucs)); \ + u8 = ucs42utf8(ucs); \ + insist(u8 != 0); \ + insist(!strcmp(u8, CHARS)); \ +} while(0) + + /* empty string */ + + U8("", ""); + + /* ASCII characters */ + + U8(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d " + "0x2e 0x2f 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a " + "0x3b 0x3c 0x3d 0x3e 0x3f 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 " + "0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f 0x50 0x51 0x52 0x53 0x54 " + "0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f 0x60 0x61 " + "0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e " + "0x6f 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b " + "0x7c 0x7d 0x7e"); + U8("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177", + "0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa 0xb 0xc 0xd 0xe 0xf 0x10 " + "0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d " + "0x1e 0x1f 0x7f"); + + /* from RFC3629 */ + + /* UTF8-2 = %xC2-DF UTF8-tail */ + insist(!validutf8("\xC0\x80")); + insist(!validutf8("\xC1\x80")); + insist(!validutf8("\xC2\x7F")); + U8("\xC2\x80", "0x80"); + U8("\xDF\xBF", "0x7FF"); + insist(!validutf8("\xDF\xC0")); + + /* UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / + * %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) + */ + insist(!validutf8("\xE0\x9F\x80")); + U8("\xE0\xA0\x80", "0x800"); + U8("\xE0\xBF\xBF", "0xFFF"); + insist(!validutf8("\xE0\xC0\xBF")); + + insist(!validutf8("\xE1\x80\x7F")); + U8("\xE1\x80\x80", "0x1000"); + U8("\xEC\xBF\xBF", "0xCFFF"); + insist(!validutf8("\xEC\xC0\xBF")); + + U8("\xED\x80\x80", "0xD000"); + U8("\xED\x9F\xBF", "0xD7FF"); + insist(!validutf8("\xED\xA0\xBF")); + + insist(!validutf8("\xEE\x7f\x80")); + U8("\xEE\x80\x80", "0xE000"); + U8("\xEF\xBF\xBF", "0xFFFF"); + insist(!validutf8("\xEF\xC0\xBF")); + + /* UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / + * %xF4 %x80-8F 2( UTF8-tail ) + */ + insist(!validutf8("\xF0\x8F\x80\x80")); + U8("\xF0\x90\x80\x80", "0x10000"); + U8("\xF0\xBF\xBF\xBF", "0x3FFFF"); + insist(!validutf8("\xF0\xC0\x80\x80")); + + insist(!validutf8("\xF1\x80\x80\x7F")); + U8("\xF1\x80\x80\x80", "0x40000"); + U8("\xF3\xBF\xBF\xBF", "0xFFFFF"); + insist(!validutf8("\xF3\xC0\x80\x80")); + + insist(!validutf8("\xF4\x80\x80\x7F")); + U8("\xF4\x80\x80\x80", "0x100000"); + U8("\xF4\x8F\xBF\xBF", "0x10FFFF"); + insist(!validutf8("\xF4\x90\x80\x80")); + + /* miscellaneous non-UTF-8 rubbish */ + insist(!validutf8("\x80")); + insist(!validutf8("\xBF")); + insist(!validutf8("\xC0")); + insist(!validutf8("\xC0\x7F")); + insist(!validutf8("\xC0\xC0")); + insist(!validutf8("\xE0")); + insist(!validutf8("\xE0\x7F")); + insist(!validutf8("\xE0\xC0")); + insist(!validutf8("\xE0\x80")); + insist(!validutf8("\xE0\x80\x7f")); + insist(!validutf8("\xE0\x80\xC0")); + insist(!validutf8("\xF0")); + insist(!validutf8("\xF0\x7F")); + insist(!validutf8("\xF0\xC0")); + insist(!validutf8("\xF0\x80")); + insist(!validutf8("\xF0\x80\x7f")); + insist(!validutf8("\xF0\x80\xC0")); + insist(!validutf8("\xF0\x80\x80\x7f")); + insist(!validutf8("\xF0\x80\x80\xC0")); + insist(!validutf8("\xF5\x80\x80\x80")); + insist(!validutf8("\xF8")); +} + +static void test_mime(void) { + char *t, *n, *v; + + t = n = v = 0; + insist(!mime_content_type("text/plain", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(n == 0); + insist(v == 0); + + t = n = v = 0; + insist(!mime_content_type("TEXT ((nested) comment) /plain", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(n == 0); + insist(v == 0); + + t = n = v = 0; + insist(!mime_content_type(" text/plain ; Charset=utf-8", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(!strcmp(n, "charset")); + insist(!strcmp(v, "utf-8")); + + t = n = v = 0; + insist(!mime_content_type("text/plain;charset = ISO-8859-1 ", &t, &n, &v)); + insist(!strcmp(t, "text/plain")); + insist(!strcmp(n, "charset")); + insist(!strcmp(v, "ISO-8859-1")); + + /* XXX mime_parse */ + /* XXX mime_multipart */ + /* XXX mime_rfc2388_content_disposition */ + + check_string(mime_qp(""), ""); + check_string(mime_qp("foobar"), "foobar"); + check_string(mime_qp("foo=20bar"), "foo bar"); + check_string(mime_qp("x \r\ny"), "x\r\ny"); + check_string(mime_qp("x=\r\ny"), "xy"); + check_string(mime_qp("x= \r\ny"), "xy"); + check_string(mime_qp("x =\r\ny"), "x y"); + check_string(mime_qp("x = \r\ny"), "x y"); + + /* from RFC2045 */ + check_string(mime_qp("Now's the time =\r\n" +"for all folk to come=\r\n" +" to the aid of their country."), + "Now's the time for all folk to come to the aid of their country."); + + check_string(mime_base64(""), ""); + check_string(mime_base64("BBBB"), "\x04\x10\x41"); + check_string(mime_base64("////"), "\xFF\xFF\xFF"); + check_string(mime_base64("//BB"), "\xFF\xF0\x41"); + check_string(mime_base64("BBBB//BB////"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("B B B B / / B B / / / /"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("B\r\nBBB.// B-B//~//"), + "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF"); + check_string(mime_base64("BBBB="), + "\x04\x10\x41"); + check_string(mime_base64("BBBBx="), /* not actually valid base64 */ + "\x04\x10\x41"); + check_string(mime_base64("BBBB BB=="), + "\x04\x10\x41" "\x04"); + check_string(mime_base64("BBBB BBB="), + "\x04\x10\x41" "\x04\x10"); +} + +static void test_hex(void) { + unsigned n; + static const unsigned char h[] = { 0x00, 0xFF, 0x80, 0x7F }; + uint8_t *u; + size_t ul; + + for(n = 0; n <= UCHAR_MAX; ++n) { + if(!isxdigit(n)) + insist(unhexdigitq(n) == -1); + } + insist(unhexdigitq('0') == 0); + insist(unhexdigitq('1') == 1); + insist(unhexdigitq('2') == 2); + insist(unhexdigitq('3') == 3); + insist(unhexdigitq('4') == 4); + insist(unhexdigitq('5') == 5); + insist(unhexdigitq('6') == 6); + insist(unhexdigitq('7') == 7); + insist(unhexdigitq('8') == 8); + insist(unhexdigitq('9') == 9); + insist(unhexdigitq('a') == 10); + insist(unhexdigitq('b') == 11); + insist(unhexdigitq('c') == 12); + insist(unhexdigitq('d') == 13); + insist(unhexdigitq('e') == 14); + insist(unhexdigitq('f') == 15); + insist(unhexdigitq('A') == 10); + insist(unhexdigitq('B') == 11); + insist(unhexdigitq('C') == 12); + insist(unhexdigitq('D') == 13); + insist(unhexdigitq('E') == 14); + insist(unhexdigitq('F') == 15); + check_string(hex(h, sizeof h), "00ff807f"); + check_string(hex(0, 0), ""); + u = unhex("00ff807f", &ul); + insist(ul == 4); + insist(memcmp(u, h, 4) == 0); + u = unhex("00FF807F", &ul); + insist(ul == 4); + insist(memcmp(u, h, 4) == 0); + u = unhex("", &ul); + insist(ul == 0); + fprintf(stderr, "2 ERROR reports expected:\n"); + insist(unhex("F", 0) == 0); + insist(unhex("az", 0) == 0); +} + +static void test_casefold(void) { + uint32_t c, l, u[2]; + const char *s, *ls; + + for(c = 1; c < 256; ++c) { + u[0] = c; + u[1] = 0; + s = ucs42utf8(u); + ls = casefold(s); + switch(c) { + default: + if((c >= 'A' && c <= 'Z') + || (c >= 0xC0 && c <= 0xDE && c != 0xD7)) + l = c ^ 0x20; + else + l = c; + break; + case 0xB5: /* MICRO SIGN */ + l = 0x3BC; /* GREEK SMALL LETTER MU */ + break; + case 0xDF: /* LATIN SMALL LETTER SHARP S */ + insist(!strcmp(ls, "ss")); + l = 0; + break; + } + if(l) { + u[0] = l; + u[1] = 0; + s = ucs42utf8(u); + if(strcmp(s, ls)) { + fprintf(stderr, "%s:%d: casefolding %#lx got '%s', expected '%s'\n", + __FILE__, __LINE__, (unsigned long)c, + format(ls), format(s)); + ++errors; + } + ++tests; + } + } + check_string(casefold(""), ""); +} + +int main(void) { + insist('\n' == 0x0A); + insist('\r' == 0x0D); + insist(' ' == 0x20); + insist('0' == 0x30); + insist('9' == 0x39); + insist('A' == 0x41); + insist('Z' == 0x5A); + insist('a' == 0x61); + insist('z' == 0x7A); + /* addr.c */ + /* asprintf.c */ + /* authhash.c */ + /* basen.c */ + /* charset.c */ + /* client.c */ + /* configuration.c */ + /* event.c */ + /* fprintf.c */ + /* hex.c */ + test_hex(); + /* inputline.c */ + /* kvp.c */ + /* log.c */ + /* mem.c */ + /* mime.c */ + test_mime(); + /* mixer.c */ + /* plugin.c */ + /* printf.c */ + /* queue.c */ + /* sink.c */ + /* snprintf.c */ + /* split.c */ + /* syscalls.c */ + /* table.c */ + /* utf8.c */ + test_utf8(); + /* vector.c */ + /* words.c */ + test_casefold(); + /* XXX words() */ + /* wstat.c */ + fprintf(stderr, "%d errors out of %d tests\n", errors, tests); + return !!errors; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ + +/* arch-tag:pJuXi+p6I8qcGldDkx/yXA */ diff --git a/lib/trackname.c b/lib/trackname.c new file mode 100644 index 0000000..c439fa4 --- /dev/null +++ b/lib/trackname.c @@ -0,0 +1,144 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <pcre.h> +#include <fnmatch.h> +#include <string.h> +#include <assert.h> + +#include "trackname.h" +#include "configuration.h" +#include "regsub.h" +#include "log.h" +#include "filepart.h" +#include "words.h" + +const char *find_track_root(const char *track) { + int n; + size_t l, tl = strlen(track); + + for(n = 0; n < config->collection.n; ++n) { + l = strlen(config->collection.s[n].root); + if(tl > l + && !strncmp(track, config->collection.s[n].root, l) + && track[l] == '/') + break; + } + if(n >= config->collection.n) return 0; + return config->collection.s[n].root; +} + +const char *track_rootless(const char *track) { + const char *root; + + if(!(root = find_track_root(track))) return 0; + return track + strlen(root); +} + +const char *trackname_part(const char *track, + const char *context, + const char *part) { + int n; + const char *replaced, *rootless; + + assert(track != 0); + if(!strcmp(part, "path")) return track; + if(!strcmp(part, "ext")) return extension(track); + if((rootless = track_rootless(track))) track = rootless; + for(n = 0; n < config->namepart.n; ++n) { + if(!strcmp(config->namepart.s[n].part, part) + && fnmatch(config->namepart.s[n].context, context, 0) == 0) { + if((replaced = regsub(config->namepart.s[n].re, + track, + config->namepart.s[n].replace, + config->namepart.s[n].reflags + |REGSUB_MUST_MATCH + |REGSUB_REPLACE))) + return replaced; + } + } + return ""; +} + +const char *trackname_transform(const char *type, + const char *subject, + const char *context) { + const char *replaced; + int n; + const struct transform *k; + + for(n = 0; n < config->transform.n; ++n) { + k = &config->transform.t[n]; + if(strcmp(k->type, type)) + continue; + if(fnmatch(k->context, context, 0) != 0) + continue; + if((replaced = regsub(k->re, subject, k->replace, k->flags))) + subject = replaced; + } + return subject; +} + +int compare_tracks(const char *sa, const char *sb, + const char *da, const char *db, + const char *ta, const char *tb) { + int c; + + if((c = strcmp(casefold(sa), casefold(sb)))) return c; + if((c = strcmp(sa, sb))) return c; + if((c = strcmp(casefold(da), casefold(db)))) return c; + if((c = strcmp(da, db))) return c; + return compare_path(ta, tb); +} + +int compare_path_raw(const unsigned char *ap, size_t an, + const unsigned char *bp, size_t bn) { + while(an > 0 && bn > 0) { + if(*ap == *bp) { + ap++; + bp++; + an--; + bn--; + } else if(*ap == '/') { + return -1; /* /a/b < /aa/ */ + } else if(*bp == '/') { + return 1; /* /aa > /a/b */ + } else + return *ap - *bp; + } + if(an > 0) + return 1; /* /a/b > /a and /ab > /a */ + else if(bn > 0) + return -1; /* /a < /ab and /a < /a/b */ + else + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:xMbRRluU86PaVSSnyIR77A */ diff --git a/lib/trackname.h b/lib/trackname.h new file mode 100644 index 0000000..cb227e0 --- /dev/null +++ b/lib/trackname.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005, 2006 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TRACKNAME_H +#define TRACKNAME_H + +const char *find_track_root(const char *track); +/* find the collection root for @track@ */ + +const char *track_rootless(const char *track); +/* return the rootless part of @track@ (typically starting /) */ + +const char *trackname_part(const char *track, + const char *context, + const char *part); +/* compute PART (artist/album/title) for TRACK in CONTEXT (display/sort) */ + +const char *trackname_transform(const char *type, + const char *subject, + const char *context); +/* convert SUBJECT (usually 'track' or 'dir' according to TYPE) for CONTEXT + * (display/sort) */ + +int compare_tracks(const char *sa, const char *sb, + const char *da, const char *db, + const char *ta, const char *tb); +/* Compare tracks A and B, with sort/display/track names S?, D? and T? */ + +int compare_path_raw(const unsigned char *ap, size_t an, + const unsigned char *bp, size_t bn); +/* Comparison function for path names that groups all entries in a directory + * together */ + +/* Convenient wrapper for compare_path_raw */ +static inline int compare_path(const char *ap, const char *bp) { + return compare_path_raw((const unsigned char *)ap, strlen(ap), + (const unsigned char *)bp, strlen(bp)); +} + +#endif /* TRACKNAME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:JwJ4jz+OgN1Th4UTvpqQ5Q */ diff --git a/lib/types.h b/lib/types.h new file mode 100644 index 0000000..e683268 --- /dev/null +++ b/lib/types.h @@ -0,0 +1,115 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef TYPES_H +#define TYPES_H + +#if HAVE_INTTYPES_H +# include <inttypes.h> +#endif +#include <limits.h> +#include <sys/types.h> + +/* had better be before atol/atoll redefinition */ +#include <stdlib.h> + +#if HAVE_LONG_LONG +typedef long long long_long; +typedef unsigned long long u_long_long; +# if ! DECLARES_STRTOLL +long long strtoll(const char *, char **, int); +# endif +# if ! DECLARES_ATOLL +long long atoll(const char *); +# endif +#else +typedef long long_long; +typedef unsigned long u_long_long; +# define atoll atol +# define strtoll strtol +#endif + +#if __APPLE__ +/* apple define these to j[dxu], which gcc -std=c99 -pedantic then rejects */ +# undef PRIdMAX +# undef PRIxMAX +# undef PRIuMAX +#endif + +#if HAVE_INTMAX_T +# ifndef PRIdMAX +# define PRIdMAX "jd" +# endif +#elif HAVE_LONG_LONG +typedef long long intmax_t; +# define PRIdMAX "lld" +#else +typedef long intmax_t; +# define PRIdMAX "ld" +#endif + +#if HAVE_UINTMAX_T +# ifndef PRIuMAX +# define PRIuMAX "ju" +# endif +# ifndef PRIxMAX +# define PRIxMAX "jx" +# endif +#elif HAVE_LONG_LONG +typedef unsigned long long uintmax_t; +# define PRIuMAX "llu" +# define PRIxMAX "llx" +#else +typedef unsigned long uintmax_t; +# define PRIuMAX "lu" +# define PRIxMAX "lx" +#endif + +#if ! HAVE_UINT8_T +# if CHAR_BIT == 8 +typedef unsigned char uint8_t; +# else +# error cannot determine uint8_t +# endif +#endif + +#if ! HAVE_UINT32_T +# if UINT_MAX == 4294967295 +typedef unsigned int uint32_t; +# elif ULONG_MAX == 4294967295 +typedef unsigned long uint32_t; +# elif USHRT_MAX == 4294967295 +typedef unsigned short uint32_t; +# elif UCHAR_MAX == 4294967295 +typedef unsigned char uint32_t; +# else +# error cannot determine uint32_t +# endif +#endif + +#endif /* TYPES_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:d42f46f53ccab924aabc3116b716a531 */ diff --git a/lib/unicodegc.h b/lib/unicodegc.h new file mode 100644 index 0000000..1f67f5d --- /dev/null +++ b/lib/unicodegc.h @@ -0,0 +1,1614 @@ +enum unicode_gc_cat { + unicode_gc_Cc, + unicode_gc_Cf, + unicode_gc_Co, + unicode_gc_Cs, + unicode_gc_Ll, + unicode_gc_Lm, + unicode_gc_Lo, + unicode_gc_Lt, + unicode_gc_Lu, + unicode_gc_Mc, + unicode_gc_Me, + unicode_gc_Mn, + unicode_gc_Nd, + unicode_gc_Nl, + unicode_gc_No, + unicode_gc_Pc, + unicode_gc_Pd, + unicode_gc_Pe, + unicode_gc_Pf, + unicode_gc_Pi, + unicode_gc_Po, + unicode_gc_Ps, + unicode_gc_Sc, + unicode_gc_Sk, + unicode_gc_Sm, + unicode_gc_So, + unicode_gc_Zl, + unicode_gc_Zp, + unicode_gc_Zs, + unicode_gc_none +}; +static const struct unicode_gc { + uint32_t l, h; + enum unicode_gc_cat cat; +} gcs[] = { + { 0, 31, unicode_gc_Cc }, + { 32, 32, unicode_gc_Zs }, + { 33, 35, unicode_gc_Po }, + { 36, 36, unicode_gc_Sc }, + { 37, 39, unicode_gc_Po }, + { 40, 40, unicode_gc_Ps }, + { 41, 41, unicode_gc_Pe }, + { 42, 42, unicode_gc_Po }, + { 43, 43, unicode_gc_Sm }, + { 44, 44, unicode_gc_Po }, + { 45, 45, unicode_gc_Pd }, + { 46, 47, unicode_gc_Po }, + { 48, 57, unicode_gc_Nd }, + { 58, 59, unicode_gc_Po }, + { 60, 62, unicode_gc_Sm }, + { 63, 64, unicode_gc_Po }, + { 65, 90, unicode_gc_Lu }, + { 91, 91, unicode_gc_Ps }, + { 92, 92, unicode_gc_Po }, + { 93, 93, unicode_gc_Pe }, + { 94, 94, unicode_gc_Sk }, + { 95, 95, unicode_gc_Pc }, + { 96, 96, unicode_gc_Sk }, + { 97, 122, unicode_gc_Ll }, + { 123, 123, unicode_gc_Ps }, + { 124, 124, unicode_gc_Sm }, + { 125, 125, unicode_gc_Pe }, + { 126, 126, unicode_gc_Sm }, + { 127, 159, unicode_gc_Cc }, + { 160, 160, unicode_gc_Zs }, + { 161, 161, unicode_gc_Po }, + { 162, 165, unicode_gc_Sc }, + { 166, 167, unicode_gc_So }, + { 168, 168, unicode_gc_Sk }, + { 169, 169, unicode_gc_So }, + { 170, 170, unicode_gc_Ll }, + { 171, 171, unicode_gc_Pi }, + { 172, 172, unicode_gc_Sm }, + { 173, 173, unicode_gc_Cf }, + { 174, 174, unicode_gc_So }, + { 175, 175, unicode_gc_Sk }, + { 176, 176, unicode_gc_So }, + { 177, 177, unicode_gc_Sm }, + { 178, 179, unicode_gc_No }, + { 180, 180, unicode_gc_Sk }, + { 181, 181, unicode_gc_Ll }, + { 182, 182, unicode_gc_So }, + { 183, 183, unicode_gc_Po }, + { 184, 184, unicode_gc_Sk }, + { 185, 185, unicode_gc_No }, + { 186, 186, unicode_gc_Ll }, + { 187, 187, unicode_gc_Pf }, + { 188, 190, unicode_gc_No }, + { 191, 191, unicode_gc_Po }, + { 192, 214, unicode_gc_Lu }, + { 215, 215, unicode_gc_Sm }, + { 216, 222, unicode_gc_Lu }, + { 223, 246, unicode_gc_Ll }, + { 247, 247, unicode_gc_Sm }, + { 248, 255, unicode_gc_Ll }, + { 256, 256, unicode_gc_Lu }, + { 257, 257, unicode_gc_Ll }, + { 258, 258, unicode_gc_Lu }, + { 259, 259, unicode_gc_Ll }, + { 260, 260, unicode_gc_Lu }, + { 261, 261, unicode_gc_Ll }, + { 262, 262, unicode_gc_Lu }, + { 263, 263, unicode_gc_Ll }, + { 264, 264, unicode_gc_Lu }, + { 265, 265, unicode_gc_Ll }, + { 266, 266, unicode_gc_Lu }, + { 267, 267, unicode_gc_Ll }, + { 268, 268, unicode_gc_Lu }, + { 269, 269, unicode_gc_Ll }, + { 270, 270, unicode_gc_Lu }, + { 271, 271, unicode_gc_Ll }, + { 272, 272, unicode_gc_Lu }, + { 273, 273, unicode_gc_Ll }, + { 274, 274, unicode_gc_Lu }, + { 275, 275, unicode_gc_Ll }, + { 276, 276, unicode_gc_Lu }, + { 277, 277, unicode_gc_Ll }, + { 278, 278, unicode_gc_Lu }, + { 279, 279, unicode_gc_Ll }, + { 280, 280, unicode_gc_Lu }, + { 281, 281, unicode_gc_Ll }, + { 282, 282, unicode_gc_Lu }, + { 283, 283, unicode_gc_Ll }, + { 284, 284, unicode_gc_Lu }, + { 285, 285, unicode_gc_Ll }, + { 286, 286, unicode_gc_Lu }, + { 287, 287, unicode_gc_Ll }, + { 288, 288, unicode_gc_Lu }, + { 289, 289, unicode_gc_Ll }, + { 290, 290, unicode_gc_Lu }, + { 291, 291, unicode_gc_Ll }, + { 292, 292, unicode_gc_Lu }, + { 293, 293, unicode_gc_Ll }, + { 294, 294, unicode_gc_Lu }, + { 295, 295, unicode_gc_Ll }, + { 296, 296, unicode_gc_Lu }, + { 297, 297, unicode_gc_Ll }, + { 298, 298, unicode_gc_Lu }, + { 299, 299, unicode_gc_Ll }, + { 300, 300, unicode_gc_Lu }, + { 301, 301, unicode_gc_Ll }, + { 302, 302, unicode_gc_Lu }, + { 303, 303, unicode_gc_Ll }, + { 304, 304, unicode_gc_Lu }, + { 305, 305, unicode_gc_Ll }, + { 306, 306, unicode_gc_Lu }, + { 307, 307, unicode_gc_Ll }, + { 308, 308, unicode_gc_Lu }, + { 309, 309, unicode_gc_Ll }, + { 310, 310, unicode_gc_Lu }, + { 311, 312, unicode_gc_Ll }, + { 313, 313, unicode_gc_Lu }, + { 314, 314, unicode_gc_Ll }, + { 315, 315, unicode_gc_Lu }, + { 316, 316, unicode_gc_Ll }, + { 317, 317, unicode_gc_Lu }, + { 318, 318, unicode_gc_Ll }, + { 319, 319, unicode_gc_Lu }, + { 320, 320, unicode_gc_Ll }, + { 321, 321, unicode_gc_Lu }, + { 322, 322, unicode_gc_Ll }, + { 323, 323, unicode_gc_Lu }, + { 324, 324, unicode_gc_Ll }, + { 325, 325, unicode_gc_Lu }, + { 326, 326, unicode_gc_Ll }, + { 327, 327, unicode_gc_Lu }, + { 328, 329, unicode_gc_Ll }, + { 330, 330, unicode_gc_Lu }, + { 331, 331, unicode_gc_Ll }, + { 332, 332, unicode_gc_Lu }, + { 333, 333, unicode_gc_Ll }, + { 334, 334, unicode_gc_Lu }, + { 335, 335, unicode_gc_Ll }, + { 336, 336, unicode_gc_Lu }, + { 337, 337, unicode_gc_Ll }, + { 338, 338, unicode_gc_Lu }, + { 339, 339, unicode_gc_Ll }, + { 340, 340, unicode_gc_Lu }, + { 341, 341, unicode_gc_Ll }, + { 342, 342, unicode_gc_Lu }, + { 343, 343, unicode_gc_Ll }, + { 344, 344, unicode_gc_Lu }, + { 345, 345, unicode_gc_Ll }, + { 346, 346, unicode_gc_Lu }, + { 347, 347, unicode_gc_Ll }, + { 348, 348, unicode_gc_Lu }, + { 349, 349, unicode_gc_Ll }, + { 350, 350, unicode_gc_Lu }, + { 351, 351, unicode_gc_Ll }, + { 352, 352, unicode_gc_Lu }, + { 353, 353, unicode_gc_Ll }, + { 354, 354, unicode_gc_Lu }, + { 355, 355, unicode_gc_Ll }, + { 356, 356, unicode_gc_Lu }, + { 357, 357, unicode_gc_Ll }, + { 358, 358, unicode_gc_Lu }, + { 359, 359, unicode_gc_Ll }, + { 360, 360, unicode_gc_Lu }, + { 361, 361, unicode_gc_Ll }, + { 362, 362, unicode_gc_Lu }, + { 363, 363, unicode_gc_Ll }, + { 364, 364, unicode_gc_Lu }, + { 365, 365, unicode_gc_Ll }, + { 366, 366, unicode_gc_Lu }, + { 367, 367, unicode_gc_Ll }, + { 368, 368, unicode_gc_Lu }, + { 369, 369, unicode_gc_Ll }, + { 370, 370, unicode_gc_Lu }, + { 371, 371, unicode_gc_Ll }, + { 372, 372, unicode_gc_Lu }, + { 373, 373, unicode_gc_Ll }, + { 374, 374, unicode_gc_Lu }, + { 375, 375, unicode_gc_Ll }, + { 376, 377, unicode_gc_Lu }, + { 378, 378, unicode_gc_Ll }, + { 379, 379, unicode_gc_Lu }, + { 380, 380, unicode_gc_Ll }, + { 381, 381, unicode_gc_Lu }, + { 382, 384, unicode_gc_Ll }, + { 385, 386, unicode_gc_Lu }, + { 387, 387, unicode_gc_Ll }, + { 388, 388, unicode_gc_Lu }, + { 389, 389, unicode_gc_Ll }, + { 390, 391, unicode_gc_Lu }, + { 392, 392, unicode_gc_Ll }, + { 393, 395, unicode_gc_Lu }, + { 396, 397, unicode_gc_Ll }, + { 398, 401, unicode_gc_Lu }, + { 402, 402, unicode_gc_Ll }, + { 403, 404, unicode_gc_Lu }, + { 405, 405, unicode_gc_Ll }, + { 406, 408, unicode_gc_Lu }, + { 409, 411, unicode_gc_Ll }, + { 412, 413, unicode_gc_Lu }, + { 414, 414, unicode_gc_Ll }, + { 415, 416, unicode_gc_Lu }, + { 417, 417, unicode_gc_Ll }, + { 418, 418, unicode_gc_Lu }, + { 419, 419, unicode_gc_Ll }, + { 420, 420, unicode_gc_Lu }, + { 421, 421, unicode_gc_Ll }, + { 422, 423, unicode_gc_Lu }, + { 424, 424, unicode_gc_Ll }, + { 425, 425, unicode_gc_Lu }, + { 426, 427, unicode_gc_Ll }, + { 428, 428, unicode_gc_Lu }, + { 429, 429, unicode_gc_Ll }, + { 430, 431, unicode_gc_Lu }, + { 432, 432, unicode_gc_Ll }, + { 433, 435, unicode_gc_Lu }, + { 436, 436, unicode_gc_Ll }, + { 437, 437, unicode_gc_Lu }, + { 438, 438, unicode_gc_Ll }, + { 439, 440, unicode_gc_Lu }, + { 441, 442, unicode_gc_Ll }, + { 443, 443, unicode_gc_Lo }, + { 444, 444, unicode_gc_Lu }, + { 445, 447, unicode_gc_Ll }, + { 448, 451, unicode_gc_Lo }, + { 452, 452, unicode_gc_Lu }, + { 453, 453, unicode_gc_Lt }, + { 454, 454, unicode_gc_Ll }, + { 455, 455, unicode_gc_Lu }, + { 456, 456, unicode_gc_Lt }, + { 457, 457, unicode_gc_Ll }, + { 458, 458, unicode_gc_Lu }, + { 459, 459, unicode_gc_Lt }, + { 460, 460, unicode_gc_Ll }, + { 461, 461, unicode_gc_Lu }, + { 462, 462, unicode_gc_Ll }, + { 463, 463, unicode_gc_Lu }, + { 464, 464, unicode_gc_Ll }, + { 465, 465, unicode_gc_Lu }, + { 466, 466, unicode_gc_Ll }, + { 467, 467, unicode_gc_Lu }, + { 468, 468, unicode_gc_Ll }, + { 469, 469, unicode_gc_Lu }, + { 470, 470, unicode_gc_Ll }, + { 471, 471, unicode_gc_Lu }, + { 472, 472, unicode_gc_Ll }, + { 473, 473, unicode_gc_Lu }, + { 474, 474, unicode_gc_Ll }, + { 475, 475, unicode_gc_Lu }, + { 476, 477, unicode_gc_Ll }, + { 478, 478, unicode_gc_Lu }, + { 479, 479, unicode_gc_Ll }, + { 480, 480, unicode_gc_Lu }, + { 481, 481, unicode_gc_Ll }, + { 482, 482, unicode_gc_Lu }, + { 483, 483, unicode_gc_Ll }, + { 484, 484, unicode_gc_Lu }, + { 485, 485, unicode_gc_Ll }, + { 486, 486, unicode_gc_Lu }, + { 487, 487, unicode_gc_Ll }, + { 488, 488, unicode_gc_Lu }, + { 489, 489, unicode_gc_Ll }, + { 490, 490, unicode_gc_Lu }, + { 491, 491, unicode_gc_Ll }, + { 492, 492, unicode_gc_Lu }, + { 493, 493, unicode_gc_Ll }, + { 494, 494, unicode_gc_Lu }, + { 495, 496, unicode_gc_Ll }, + { 497, 497, unicode_gc_Lu }, + { 498, 498, unicode_gc_Lt }, + { 499, 499, unicode_gc_Ll }, + { 500, 500, unicode_gc_Lu }, + { 501, 501, unicode_gc_Ll }, + { 502, 504, unicode_gc_Lu }, + { 505, 505, unicode_gc_Ll }, + { 506, 506, unicode_gc_Lu }, + { 507, 507, unicode_gc_Ll }, + { 508, 508, unicode_gc_Lu }, + { 509, 509, unicode_gc_Ll }, + { 510, 510, unicode_gc_Lu }, + { 511, 511, unicode_gc_Ll }, + { 512, 512, unicode_gc_Lu }, + { 513, 513, unicode_gc_Ll }, + { 514, 514, unicode_gc_Lu }, + { 515, 515, unicode_gc_Ll }, + { 516, 516, unicode_gc_Lu }, + { 517, 517, unicode_gc_Ll }, + { 518, 518, unicode_gc_Lu }, + { 519, 519, unicode_gc_Ll }, + { 520, 520, unicode_gc_Lu }, + { 521, 521, unicode_gc_Ll }, + { 522, 522, unicode_gc_Lu }, + { 523, 523, unicode_gc_Ll }, + { 524, 524, unicode_gc_Lu }, + { 525, 525, unicode_gc_Ll }, + { 526, 526, unicode_gc_Lu }, + { 527, 527, unicode_gc_Ll }, + { 528, 528, unicode_gc_Lu }, + { 529, 529, unicode_gc_Ll }, + { 530, 530, unicode_gc_Lu }, + { 531, 531, unicode_gc_Ll }, + { 532, 532, unicode_gc_Lu }, + { 533, 533, unicode_gc_Ll }, + { 534, 534, unicode_gc_Lu }, + { 535, 535, unicode_gc_Ll }, + { 536, 536, unicode_gc_Lu }, + { 537, 537, unicode_gc_Ll }, + { 538, 538, unicode_gc_Lu }, + { 539, 539, unicode_gc_Ll }, + { 540, 540, unicode_gc_Lu }, + { 541, 541, unicode_gc_Ll }, + { 542, 542, unicode_gc_Lu }, + { 543, 543, unicode_gc_Ll }, + { 544, 544, unicode_gc_Lu }, + { 545, 545, unicode_gc_Ll }, + { 546, 546, unicode_gc_Lu }, + { 547, 547, unicode_gc_Ll }, + { 548, 548, unicode_gc_Lu }, + { 549, 549, unicode_gc_Ll }, + { 550, 550, unicode_gc_Lu }, + { 551, 551, unicode_gc_Ll }, + { 552, 552, unicode_gc_Lu }, + { 553, 553, unicode_gc_Ll }, + { 554, 554, unicode_gc_Lu }, + { 555, 555, unicode_gc_Ll }, + { 556, 556, unicode_gc_Lu }, + { 557, 557, unicode_gc_Ll }, + { 558, 558, unicode_gc_Lu }, + { 559, 559, unicode_gc_Ll }, + { 560, 560, unicode_gc_Lu }, + { 561, 561, unicode_gc_Ll }, + { 562, 562, unicode_gc_Lu }, + { 563, 687, unicode_gc_Ll }, + { 688, 705, unicode_gc_Lm }, + { 706, 709, unicode_gc_Sk }, + { 710, 721, unicode_gc_Lm }, + { 722, 735, unicode_gc_Sk }, + { 736, 740, unicode_gc_Lm }, + { 741, 749, unicode_gc_Sk }, + { 750, 750, unicode_gc_Lm }, + { 751, 767, unicode_gc_Sk }, + { 768, 883, unicode_gc_Mn }, + { 884, 889, unicode_gc_Sk }, + { 890, 893, unicode_gc_Lm }, + { 894, 899, unicode_gc_Po }, + { 900, 901, unicode_gc_Sk }, + { 902, 902, unicode_gc_Lu }, + { 903, 903, unicode_gc_Po }, + { 904, 911, unicode_gc_Lu }, + { 912, 912, unicode_gc_Ll }, + { 913, 939, unicode_gc_Lu }, + { 940, 977, unicode_gc_Ll }, + { 978, 980, unicode_gc_Lu }, + { 981, 983, unicode_gc_Ll }, + { 984, 984, unicode_gc_Lu }, + { 985, 985, unicode_gc_Ll }, + { 986, 986, unicode_gc_Lu }, + { 987, 987, unicode_gc_Ll }, + { 988, 988, unicode_gc_Lu }, + { 989, 989, unicode_gc_Ll }, + { 990, 990, unicode_gc_Lu }, + { 991, 991, unicode_gc_Ll }, + { 992, 992, unicode_gc_Lu }, + { 993, 993, unicode_gc_Ll }, + { 994, 994, unicode_gc_Lu }, + { 995, 995, unicode_gc_Ll }, + { 996, 996, unicode_gc_Lu }, + { 997, 997, unicode_gc_Ll }, + { 998, 998, unicode_gc_Lu }, + { 999, 999, unicode_gc_Ll }, + { 1000, 1000, unicode_gc_Lu }, + { 1001, 1001, unicode_gc_Ll }, + { 1002, 1002, unicode_gc_Lu }, + { 1003, 1003, unicode_gc_Ll }, + { 1004, 1004, unicode_gc_Lu }, + { 1005, 1005, unicode_gc_Ll }, + { 1006, 1006, unicode_gc_Lu }, + { 1007, 1011, unicode_gc_Ll }, + { 1012, 1012, unicode_gc_Lu }, + { 1013, 1013, unicode_gc_Ll }, + { 1014, 1014, unicode_gc_Sm }, + { 1015, 1015, unicode_gc_Lu }, + { 1016, 1016, unicode_gc_Ll }, + { 1017, 1018, unicode_gc_Lu }, + { 1019, 1023, unicode_gc_Ll }, + { 1024, 1071, unicode_gc_Lu }, + { 1072, 1119, unicode_gc_Ll }, + { 1120, 1120, unicode_gc_Lu }, + { 1121, 1121, unicode_gc_Ll }, + { 1122, 1122, unicode_gc_Lu }, + { 1123, 1123, unicode_gc_Ll }, + { 1124, 1124, unicode_gc_Lu }, + { 1125, 1125, unicode_gc_Ll }, + { 1126, 1126, unicode_gc_Lu }, + { 1127, 1127, unicode_gc_Ll }, + { 1128, 1128, unicode_gc_Lu }, + { 1129, 1129, unicode_gc_Ll }, + { 1130, 1130, unicode_gc_Lu }, + { 1131, 1131, unicode_gc_Ll }, + { 1132, 1132, unicode_gc_Lu }, + { 1133, 1133, unicode_gc_Ll }, + { 1134, 1134, unicode_gc_Lu }, + { 1135, 1135, unicode_gc_Ll }, + { 1136, 1136, unicode_gc_Lu }, + { 1137, 1137, unicode_gc_Ll }, + { 1138, 1138, unicode_gc_Lu }, + { 1139, 1139, unicode_gc_Ll }, + { 1140, 1140, unicode_gc_Lu }, + { 1141, 1141, unicode_gc_Ll }, + { 1142, 1142, unicode_gc_Lu }, + { 1143, 1143, unicode_gc_Ll }, + { 1144, 1144, unicode_gc_Lu }, + { 1145, 1145, unicode_gc_Ll }, + { 1146, 1146, unicode_gc_Lu }, + { 1147, 1147, unicode_gc_Ll }, + { 1148, 1148, unicode_gc_Lu }, + { 1149, 1149, unicode_gc_Ll }, + { 1150, 1150, unicode_gc_Lu }, + { 1151, 1151, unicode_gc_Ll }, + { 1152, 1152, unicode_gc_Lu }, + { 1153, 1153, unicode_gc_Ll }, + { 1154, 1154, unicode_gc_So }, + { 1155, 1159, unicode_gc_Mn }, + { 1160, 1161, unicode_gc_Me }, + { 1162, 1162, unicode_gc_Lu }, + { 1163, 1163, unicode_gc_Ll }, + { 1164, 1164, unicode_gc_Lu }, + { 1165, 1165, unicode_gc_Ll }, + { 1166, 1166, unicode_gc_Lu }, + { 1167, 1167, unicode_gc_Ll }, + { 1168, 1168, unicode_gc_Lu }, + { 1169, 1169, unicode_gc_Ll }, + { 1170, 1170, unicode_gc_Lu }, + { 1171, 1171, unicode_gc_Ll }, + { 1172, 1172, unicode_gc_Lu }, + { 1173, 1173, unicode_gc_Ll }, + { 1174, 1174, unicode_gc_Lu }, + { 1175, 1175, unicode_gc_Ll }, + { 1176, 1176, unicode_gc_Lu }, + { 1177, 1177, unicode_gc_Ll }, + { 1178, 1178, unicode_gc_Lu }, + { 1179, 1179, unicode_gc_Ll }, + { 1180, 1180, unicode_gc_Lu }, + { 1181, 1181, unicode_gc_Ll }, + { 1182, 1182, unicode_gc_Lu }, + { 1183, 1183, unicode_gc_Ll }, + { 1184, 1184, unicode_gc_Lu }, + { 1185, 1185, unicode_gc_Ll }, + { 1186, 1186, unicode_gc_Lu }, + { 1187, 1187, unicode_gc_Ll }, + { 1188, 1188, unicode_gc_Lu }, + { 1189, 1189, unicode_gc_Ll }, + { 1190, 1190, unicode_gc_Lu }, + { 1191, 1191, unicode_gc_Ll }, + { 1192, 1192, unicode_gc_Lu }, + { 1193, 1193, unicode_gc_Ll }, + { 1194, 1194, unicode_gc_Lu }, + { 1195, 1195, unicode_gc_Ll }, + { 1196, 1196, unicode_gc_Lu }, + { 1197, 1197, unicode_gc_Ll }, + { 1198, 1198, unicode_gc_Lu }, + { 1199, 1199, unicode_gc_Ll }, + { 1200, 1200, unicode_gc_Lu }, + { 1201, 1201, unicode_gc_Ll }, + { 1202, 1202, unicode_gc_Lu }, + { 1203, 1203, unicode_gc_Ll }, + { 1204, 1204, unicode_gc_Lu }, + { 1205, 1205, unicode_gc_Ll }, + { 1206, 1206, unicode_gc_Lu }, + { 1207, 1207, unicode_gc_Ll }, + { 1208, 1208, unicode_gc_Lu }, + { 1209, 1209, unicode_gc_Ll }, + { 1210, 1210, unicode_gc_Lu }, + { 1211, 1211, unicode_gc_Ll }, + { 1212, 1212, unicode_gc_Lu }, + { 1213, 1213, unicode_gc_Ll }, + { 1214, 1214, unicode_gc_Lu }, + { 1215, 1215, unicode_gc_Ll }, + { 1216, 1217, unicode_gc_Lu }, + { 1218, 1218, unicode_gc_Ll }, + { 1219, 1219, unicode_gc_Lu }, + { 1220, 1220, unicode_gc_Ll }, + { 1221, 1221, unicode_gc_Lu }, + { 1222, 1222, unicode_gc_Ll }, + { 1223, 1223, unicode_gc_Lu }, + { 1224, 1224, unicode_gc_Ll }, + { 1225, 1225, unicode_gc_Lu }, + { 1226, 1226, unicode_gc_Ll }, + { 1227, 1227, unicode_gc_Lu }, + { 1228, 1228, unicode_gc_Ll }, + { 1229, 1229, unicode_gc_Lu }, + { 1230, 1231, unicode_gc_Ll }, + { 1232, 1232, unicode_gc_Lu }, + { 1233, 1233, unicode_gc_Ll }, + { 1234, 1234, unicode_gc_Lu }, + { 1235, 1235, unicode_gc_Ll }, + { 1236, 1236, unicode_gc_Lu }, + { 1237, 1237, unicode_gc_Ll }, + { 1238, 1238, unicode_gc_Lu }, + { 1239, 1239, unicode_gc_Ll }, + { 1240, 1240, unicode_gc_Lu }, + { 1241, 1241, unicode_gc_Ll }, + { 1242, 1242, unicode_gc_Lu }, + { 1243, 1243, unicode_gc_Ll }, + { 1244, 1244, unicode_gc_Lu }, + { 1245, 1245, unicode_gc_Ll }, + { 1246, 1246, unicode_gc_Lu }, + { 1247, 1247, unicode_gc_Ll }, + { 1248, 1248, unicode_gc_Lu }, + { 1249, 1249, unicode_gc_Ll }, + { 1250, 1250, unicode_gc_Lu }, + { 1251, 1251, unicode_gc_Ll }, + { 1252, 1252, unicode_gc_Lu }, + { 1253, 1253, unicode_gc_Ll }, + { 1254, 1254, unicode_gc_Lu }, + { 1255, 1255, unicode_gc_Ll }, + { 1256, 1256, unicode_gc_Lu }, + { 1257, 1257, unicode_gc_Ll }, + { 1258, 1258, unicode_gc_Lu }, + { 1259, 1259, unicode_gc_Ll }, + { 1260, 1260, unicode_gc_Lu }, + { 1261, 1261, unicode_gc_Ll }, + { 1262, 1262, unicode_gc_Lu }, + { 1263, 1263, unicode_gc_Ll }, + { 1264, 1264, unicode_gc_Lu }, + { 1265, 1265, unicode_gc_Ll }, + { 1266, 1266, unicode_gc_Lu }, + { 1267, 1267, unicode_gc_Ll }, + { 1268, 1268, unicode_gc_Lu }, + { 1269, 1271, unicode_gc_Ll }, + { 1272, 1272, unicode_gc_Lu }, + { 1273, 1279, unicode_gc_Ll }, + { 1280, 1280, unicode_gc_Lu }, + { 1281, 1281, unicode_gc_Ll }, + { 1282, 1282, unicode_gc_Lu }, + { 1283, 1283, unicode_gc_Ll }, + { 1284, 1284, unicode_gc_Lu }, + { 1285, 1285, unicode_gc_Ll }, + { 1286, 1286, unicode_gc_Lu }, + { 1287, 1287, unicode_gc_Ll }, + { 1288, 1288, unicode_gc_Lu }, + { 1289, 1289, unicode_gc_Ll }, + { 1290, 1290, unicode_gc_Lu }, + { 1291, 1291, unicode_gc_Ll }, + { 1292, 1292, unicode_gc_Lu }, + { 1293, 1293, unicode_gc_Ll }, + { 1294, 1294, unicode_gc_Lu }, + { 1295, 1328, unicode_gc_Ll }, + { 1329, 1368, unicode_gc_Lu }, + { 1369, 1369, unicode_gc_Lm }, + { 1370, 1376, unicode_gc_Po }, + { 1377, 1416, unicode_gc_Ll }, + { 1417, 1417, unicode_gc_Po }, + { 1418, 1424, unicode_gc_Pd }, + { 1425, 1469, unicode_gc_Mn }, + { 1470, 1470, unicode_gc_Po }, + { 1471, 1471, unicode_gc_Mn }, + { 1472, 1472, unicode_gc_Po }, + { 1473, 1474, unicode_gc_Mn }, + { 1475, 1475, unicode_gc_Po }, + { 1476, 1487, unicode_gc_Mn }, + { 1488, 1522, unicode_gc_Lo }, + { 1523, 1535, unicode_gc_Po }, + { 1536, 1547, unicode_gc_Cf }, + { 1548, 1549, unicode_gc_Po }, + { 1550, 1551, unicode_gc_So }, + { 1552, 1562, unicode_gc_Mn }, + { 1563, 1568, unicode_gc_Po }, + { 1569, 1599, unicode_gc_Lo }, + { 1600, 1600, unicode_gc_Lm }, + { 1601, 1610, unicode_gc_Lo }, + { 1611, 1631, unicode_gc_Mn }, + { 1632, 1641, unicode_gc_Nd }, + { 1642, 1645, unicode_gc_Po }, + { 1646, 1647, unicode_gc_Lo }, + { 1648, 1648, unicode_gc_Mn }, + { 1649, 1747, unicode_gc_Lo }, + { 1748, 1748, unicode_gc_Po }, + { 1749, 1749, unicode_gc_Lo }, + { 1750, 1756, unicode_gc_Mn }, + { 1757, 1757, unicode_gc_Cf }, + { 1758, 1758, unicode_gc_Me }, + { 1759, 1764, unicode_gc_Mn }, + { 1765, 1766, unicode_gc_Lm }, + { 1767, 1768, unicode_gc_Mn }, + { 1769, 1769, unicode_gc_So }, + { 1770, 1773, unicode_gc_Mn }, + { 1774, 1775, unicode_gc_Lo }, + { 1776, 1785, unicode_gc_Nd }, + { 1786, 1788, unicode_gc_Lo }, + { 1789, 1790, unicode_gc_So }, + { 1791, 1791, unicode_gc_Lo }, + { 1792, 1806, unicode_gc_Po }, + { 1807, 1807, unicode_gc_Cf }, + { 1808, 1808, unicode_gc_Lo }, + { 1809, 1809, unicode_gc_Mn }, + { 1810, 1839, unicode_gc_Lo }, + { 1840, 1868, unicode_gc_Mn }, + { 1869, 1957, unicode_gc_Lo }, + { 1958, 1968, unicode_gc_Mn }, + { 1969, 2304, unicode_gc_Lo }, + { 2305, 2306, unicode_gc_Mn }, + { 2307, 2307, unicode_gc_Mc }, + { 2308, 2363, unicode_gc_Lo }, + { 2364, 2364, unicode_gc_Mn }, + { 2365, 2365, unicode_gc_Lo }, + { 2366, 2368, unicode_gc_Mc }, + { 2369, 2376, unicode_gc_Mn }, + { 2377, 2380, unicode_gc_Mc }, + { 2381, 2383, unicode_gc_Mn }, + { 2384, 2384, unicode_gc_Lo }, + { 2385, 2391, unicode_gc_Mn }, + { 2392, 2401, unicode_gc_Lo }, + { 2402, 2403, unicode_gc_Mn }, + { 2404, 2405, unicode_gc_Po }, + { 2406, 2415, unicode_gc_Nd }, + { 2416, 2432, unicode_gc_Po }, + { 2433, 2433, unicode_gc_Mn }, + { 2434, 2436, unicode_gc_Mc }, + { 2437, 2491, unicode_gc_Lo }, + { 2492, 2492, unicode_gc_Mn }, + { 2493, 2493, unicode_gc_Lo }, + { 2494, 2496, unicode_gc_Mc }, + { 2497, 2502, unicode_gc_Mn }, + { 2503, 2508, unicode_gc_Mc }, + { 2509, 2518, unicode_gc_Mn }, + { 2519, 2523, unicode_gc_Mc }, + { 2524, 2529, unicode_gc_Lo }, + { 2530, 2533, unicode_gc_Mn }, + { 2534, 2543, unicode_gc_Nd }, + { 2544, 2545, unicode_gc_Lo }, + { 2546, 2547, unicode_gc_Sc }, + { 2548, 2553, unicode_gc_No }, + { 2554, 2560, unicode_gc_So }, + { 2561, 2562, unicode_gc_Mn }, + { 2563, 2564, unicode_gc_Mc }, + { 2565, 2619, unicode_gc_Lo }, + { 2620, 2621, unicode_gc_Mn }, + { 2622, 2624, unicode_gc_Mc }, + { 2625, 2648, unicode_gc_Mn }, + { 2649, 2661, unicode_gc_Lo }, + { 2662, 2671, unicode_gc_Nd }, + { 2672, 2673, unicode_gc_Mn }, + { 2674, 2688, unicode_gc_Lo }, + { 2689, 2690, unicode_gc_Mn }, + { 2691, 2692, unicode_gc_Mc }, + { 2693, 2747, unicode_gc_Lo }, + { 2748, 2748, unicode_gc_Mn }, + { 2749, 2749, unicode_gc_Lo }, + { 2750, 2752, unicode_gc_Mc }, + { 2753, 2760, unicode_gc_Mn }, + { 2761, 2764, unicode_gc_Mc }, + { 2765, 2767, unicode_gc_Mn }, + { 2768, 2785, unicode_gc_Lo }, + { 2786, 2789, unicode_gc_Mn }, + { 2790, 2800, unicode_gc_Nd }, + { 2801, 2816, unicode_gc_Sc }, + { 2817, 2817, unicode_gc_Mn }, + { 2818, 2820, unicode_gc_Mc }, + { 2821, 2875, unicode_gc_Lo }, + { 2876, 2876, unicode_gc_Mn }, + { 2877, 2877, unicode_gc_Lo }, + { 2878, 2878, unicode_gc_Mc }, + { 2879, 2879, unicode_gc_Mn }, + { 2880, 2880, unicode_gc_Mc }, + { 2881, 2886, unicode_gc_Mn }, + { 2887, 2892, unicode_gc_Mc }, + { 2893, 2902, unicode_gc_Mn }, + { 2903, 2907, unicode_gc_Mc }, + { 2908, 2917, unicode_gc_Lo }, + { 2918, 2927, unicode_gc_Nd }, + { 2928, 2928, unicode_gc_So }, + { 2929, 2945, unicode_gc_Lo }, + { 2946, 2946, unicode_gc_Mn }, + { 2947, 3005, unicode_gc_Lo }, + { 3006, 3007, unicode_gc_Mc }, + { 3008, 3008, unicode_gc_Mn }, + { 3009, 3020, unicode_gc_Mc }, + { 3021, 3030, unicode_gc_Mn }, + { 3031, 3046, unicode_gc_Mc }, + { 3047, 3055, unicode_gc_Nd }, + { 3056, 3058, unicode_gc_No }, + { 3059, 3064, unicode_gc_So }, + { 3065, 3065, unicode_gc_Sc }, + { 3066, 3072, unicode_gc_So }, + { 3073, 3076, unicode_gc_Mc }, + { 3077, 3133, unicode_gc_Lo }, + { 3134, 3136, unicode_gc_Mn }, + { 3137, 3141, unicode_gc_Mc }, + { 3142, 3167, unicode_gc_Mn }, + { 3168, 3173, unicode_gc_Lo }, + { 3174, 3201, unicode_gc_Nd }, + { 3202, 3204, unicode_gc_Mc }, + { 3205, 3259, unicode_gc_Lo }, + { 3260, 3260, unicode_gc_Mn }, + { 3261, 3261, unicode_gc_Lo }, + { 3262, 3262, unicode_gc_Mc }, + { 3263, 3263, unicode_gc_Mn }, + { 3264, 3269, unicode_gc_Mc }, + { 3270, 3270, unicode_gc_Mn }, + { 3271, 3275, unicode_gc_Mc }, + { 3276, 3284, unicode_gc_Mn }, + { 3285, 3293, unicode_gc_Mc }, + { 3294, 3301, unicode_gc_Lo }, + { 3302, 3329, unicode_gc_Nd }, + { 3330, 3332, unicode_gc_Mc }, + { 3333, 3389, unicode_gc_Lo }, + { 3390, 3392, unicode_gc_Mc }, + { 3393, 3397, unicode_gc_Mn }, + { 3398, 3404, unicode_gc_Mc }, + { 3405, 3414, unicode_gc_Mn }, + { 3415, 3423, unicode_gc_Mc }, + { 3424, 3429, unicode_gc_Lo }, + { 3430, 3457, unicode_gc_Nd }, + { 3458, 3460, unicode_gc_Mc }, + { 3461, 3529, unicode_gc_Lo }, + { 3530, 3534, unicode_gc_Mn }, + { 3535, 3537, unicode_gc_Mc }, + { 3538, 3543, unicode_gc_Mn }, + { 3544, 3571, unicode_gc_Mc }, + { 3572, 3584, unicode_gc_Po }, + { 3585, 3632, unicode_gc_Lo }, + { 3633, 3633, unicode_gc_Mn }, + { 3634, 3635, unicode_gc_Lo }, + { 3636, 3646, unicode_gc_Mn }, + { 3647, 3647, unicode_gc_Sc }, + { 3648, 3653, unicode_gc_Lo }, + { 3654, 3654, unicode_gc_Lm }, + { 3655, 3662, unicode_gc_Mn }, + { 3663, 3663, unicode_gc_Po }, + { 3664, 3673, unicode_gc_Nd }, + { 3674, 3712, unicode_gc_Po }, + { 3713, 3760, unicode_gc_Lo }, + { 3761, 3761, unicode_gc_Mn }, + { 3762, 3763, unicode_gc_Lo }, + { 3764, 3772, unicode_gc_Mn }, + { 3773, 3781, unicode_gc_Lo }, + { 3782, 3783, unicode_gc_Lm }, + { 3784, 3791, unicode_gc_Mn }, + { 3792, 3803, unicode_gc_Nd }, + { 3804, 3840, unicode_gc_Lo }, + { 3841, 3843, unicode_gc_So }, + { 3844, 3858, unicode_gc_Po }, + { 3859, 3863, unicode_gc_So }, + { 3864, 3865, unicode_gc_Mn }, + { 3866, 3871, unicode_gc_So }, + { 3872, 3881, unicode_gc_Nd }, + { 3882, 3891, unicode_gc_No }, + { 3892, 3892, unicode_gc_So }, + { 3893, 3893, unicode_gc_Mn }, + { 3894, 3894, unicode_gc_So }, + { 3895, 3895, unicode_gc_Mn }, + { 3896, 3896, unicode_gc_So }, + { 3897, 3897, unicode_gc_Mn }, + { 3898, 3898, unicode_gc_Ps }, + { 3899, 3899, unicode_gc_Pe }, + { 3900, 3900, unicode_gc_Ps }, + { 3901, 3901, unicode_gc_Pe }, + { 3902, 3903, unicode_gc_Mc }, + { 3904, 3952, unicode_gc_Lo }, + { 3953, 3966, unicode_gc_Mn }, + { 3967, 3967, unicode_gc_Mc }, + { 3968, 3972, unicode_gc_Mn }, + { 3973, 3973, unicode_gc_Po }, + { 3974, 3975, unicode_gc_Mn }, + { 3976, 3983, unicode_gc_Lo }, + { 3984, 4029, unicode_gc_Mn }, + { 4030, 4037, unicode_gc_So }, + { 4038, 4038, unicode_gc_Mn }, + { 4039, 4095, unicode_gc_So }, + { 4096, 4139, unicode_gc_Lo }, + { 4140, 4140, unicode_gc_Mc }, + { 4141, 4144, unicode_gc_Mn }, + { 4145, 4145, unicode_gc_Mc }, + { 4146, 4151, unicode_gc_Mn }, + { 4152, 4152, unicode_gc_Mc }, + { 4153, 4159, unicode_gc_Mn }, + { 4160, 4169, unicode_gc_Nd }, + { 4170, 4175, unicode_gc_Po }, + { 4176, 4181, unicode_gc_Lo }, + { 4182, 4183, unicode_gc_Mc }, + { 4184, 4255, unicode_gc_Mn }, + { 4256, 4303, unicode_gc_Lu }, + { 4304, 4346, unicode_gc_Lo }, + { 4347, 4351, unicode_gc_Po }, + { 4352, 4960, unicode_gc_Lo }, + { 4961, 4968, unicode_gc_Po }, + { 4969, 4977, unicode_gc_Nd }, + { 4978, 5023, unicode_gc_No }, + { 5024, 5740, unicode_gc_Lo }, + { 5741, 5742, unicode_gc_Po }, + { 5743, 5759, unicode_gc_Lo }, + { 5760, 5760, unicode_gc_Zs }, + { 5761, 5786, unicode_gc_Lo }, + { 5787, 5787, unicode_gc_Ps }, + { 5788, 5791, unicode_gc_Pe }, + { 5792, 5866, unicode_gc_Lo }, + { 5867, 5869, unicode_gc_Po }, + { 5870, 5887, unicode_gc_Nl }, + { 5888, 5905, unicode_gc_Lo }, + { 5906, 5919, unicode_gc_Mn }, + { 5920, 5937, unicode_gc_Lo }, + { 5938, 5940, unicode_gc_Mn }, + { 5941, 5951, unicode_gc_Po }, + { 5952, 5969, unicode_gc_Lo }, + { 5970, 5983, unicode_gc_Mn }, + { 5984, 6001, unicode_gc_Lo }, + { 6002, 6015, unicode_gc_Mn }, + { 6016, 6067, unicode_gc_Lo }, + { 6068, 6069, unicode_gc_Cf }, + { 6070, 6070, unicode_gc_Mc }, + { 6071, 6077, unicode_gc_Mn }, + { 6078, 6085, unicode_gc_Mc }, + { 6086, 6086, unicode_gc_Mn }, + { 6087, 6088, unicode_gc_Mc }, + { 6089, 6099, unicode_gc_Mn }, + { 6100, 6102, unicode_gc_Po }, + { 6103, 6103, unicode_gc_Lm }, + { 6104, 6106, unicode_gc_Po }, + { 6107, 6107, unicode_gc_Sc }, + { 6108, 6108, unicode_gc_Lo }, + { 6109, 6111, unicode_gc_Mn }, + { 6112, 6127, unicode_gc_Nd }, + { 6128, 6143, unicode_gc_No }, + { 6144, 6149, unicode_gc_Po }, + { 6150, 6150, unicode_gc_Pd }, + { 6151, 6154, unicode_gc_Po }, + { 6155, 6157, unicode_gc_Mn }, + { 6158, 6159, unicode_gc_Zs }, + { 6160, 6175, unicode_gc_Nd }, + { 6176, 6210, unicode_gc_Lo }, + { 6211, 6211, unicode_gc_Lm }, + { 6212, 6312, unicode_gc_Lo }, + { 6313, 6399, unicode_gc_Mn }, + { 6400, 6431, unicode_gc_Lo }, + { 6432, 6434, unicode_gc_Mn }, + { 6435, 6438, unicode_gc_Mc }, + { 6439, 6440, unicode_gc_Mn }, + { 6441, 6449, unicode_gc_Mc }, + { 6450, 6450, unicode_gc_Mn }, + { 6451, 6456, unicode_gc_Mc }, + { 6457, 6463, unicode_gc_Mn }, + { 6464, 6467, unicode_gc_So }, + { 6468, 6469, unicode_gc_Po }, + { 6470, 6479, unicode_gc_Nd }, + { 6480, 6623, unicode_gc_Lo }, + { 6624, 7423, unicode_gc_So }, + { 7424, 7467, unicode_gc_Ll }, + { 7468, 7521, unicode_gc_Lm }, + { 7522, 7679, unicode_gc_Ll }, + { 7680, 7680, unicode_gc_Lu }, + { 7681, 7681, unicode_gc_Ll }, + { 7682, 7682, unicode_gc_Lu }, + { 7683, 7683, unicode_gc_Ll }, + { 7684, 7684, unicode_gc_Lu }, + { 7685, 7685, unicode_gc_Ll }, + { 7686, 7686, unicode_gc_Lu }, + { 7687, 7687, unicode_gc_Ll }, + { 7688, 7688, unicode_gc_Lu }, + { 7689, 7689, unicode_gc_Ll }, + { 7690, 7690, unicode_gc_Lu }, + { 7691, 7691, unicode_gc_Ll }, + { 7692, 7692, unicode_gc_Lu }, + { 7693, 7693, unicode_gc_Ll }, + { 7694, 7694, unicode_gc_Lu }, + { 7695, 7695, unicode_gc_Ll }, + { 7696, 7696, unicode_gc_Lu }, + { 7697, 7697, unicode_gc_Ll }, + { 7698, 7698, unicode_gc_Lu }, + { 7699, 7699, unicode_gc_Ll }, + { 7700, 7700, unicode_gc_Lu }, + { 7701, 7701, unicode_gc_Ll }, + { 7702, 7702, unicode_gc_Lu }, + { 7703, 7703, unicode_gc_Ll }, + { 7704, 7704, unicode_gc_Lu }, + { 7705, 7705, unicode_gc_Ll }, + { 7706, 7706, unicode_gc_Lu }, + { 7707, 7707, unicode_gc_Ll }, + { 7708, 7708, unicode_gc_Lu }, + { 7709, 7709, unicode_gc_Ll }, + { 7710, 7710, unicode_gc_Lu }, + { 7711, 7711, unicode_gc_Ll }, + { 7712, 7712, unicode_gc_Lu }, + { 7713, 7713, unicode_gc_Ll }, + { 7714, 7714, unicode_gc_Lu }, + { 7715, 7715, unicode_gc_Ll }, + { 7716, 7716, unicode_gc_Lu }, + { 7717, 7717, unicode_gc_Ll }, + { 7718, 7718, unicode_gc_Lu }, + { 7719, 7719, unicode_gc_Ll }, + { 7720, 7720, unicode_gc_Lu }, + { 7721, 7721, unicode_gc_Ll }, + { 7722, 7722, unicode_gc_Lu }, + { 7723, 7723, unicode_gc_Ll }, + { 7724, 7724, unicode_gc_Lu }, + { 7725, 7725, unicode_gc_Ll }, + { 7726, 7726, unicode_gc_Lu }, + { 7727, 7727, unicode_gc_Ll }, + { 7728, 7728, unicode_gc_Lu }, + { 7729, 7729, unicode_gc_Ll }, + { 7730, 7730, unicode_gc_Lu }, + { 7731, 7731, unicode_gc_Ll }, + { 7732, 7732, unicode_gc_Lu }, + { 7733, 7733, unicode_gc_Ll }, + { 7734, 7734, unicode_gc_Lu }, + { 7735, 7735, unicode_gc_Ll }, + { 7736, 7736, unicode_gc_Lu }, + { 7737, 7737, unicode_gc_Ll }, + { 7738, 7738, unicode_gc_Lu }, + { 7739, 7739, unicode_gc_Ll }, + { 7740, 7740, unicode_gc_Lu }, + { 7741, 7741, unicode_gc_Ll }, + { 7742, 7742, unicode_gc_Lu }, + { 7743, 7743, unicode_gc_Ll }, + { 7744, 7744, unicode_gc_Lu }, + { 7745, 7745, unicode_gc_Ll }, + { 7746, 7746, unicode_gc_Lu }, + { 7747, 7747, unicode_gc_Ll }, + { 7748, 7748, unicode_gc_Lu }, + { 7749, 7749, unicode_gc_Ll }, + { 7750, 7750, unicode_gc_Lu }, + { 7751, 7751, unicode_gc_Ll }, + { 7752, 7752, unicode_gc_Lu }, + { 7753, 7753, unicode_gc_Ll }, + { 7754, 7754, unicode_gc_Lu }, + { 7755, 7755, unicode_gc_Ll }, + { 7756, 7756, unicode_gc_Lu }, + { 7757, 7757, unicode_gc_Ll }, + { 7758, 7758, unicode_gc_Lu }, + { 7759, 7759, unicode_gc_Ll }, + { 7760, 7760, unicode_gc_Lu }, + { 7761, 7761, unicode_gc_Ll }, + { 7762, 7762, unicode_gc_Lu }, + { 7763, 7763, unicode_gc_Ll }, + { 7764, 7764, unicode_gc_Lu }, + { 7765, 7765, unicode_gc_Ll }, + { 7766, 7766, unicode_gc_Lu }, + { 7767, 7767, unicode_gc_Ll }, + { 7768, 7768, unicode_gc_Lu }, + { 7769, 7769, unicode_gc_Ll }, + { 7770, 7770, unicode_gc_Lu }, + { 7771, 7771, unicode_gc_Ll }, + { 7772, 7772, unicode_gc_Lu }, + { 7773, 7773, unicode_gc_Ll }, + { 7774, 7774, unicode_gc_Lu }, + { 7775, 7775, unicode_gc_Ll }, + { 7776, 7776, unicode_gc_Lu }, + { 7777, 7777, unicode_gc_Ll }, + { 7778, 7778, unicode_gc_Lu }, + { 7779, 7779, unicode_gc_Ll }, + { 7780, 7780, unicode_gc_Lu }, + { 7781, 7781, unicode_gc_Ll }, + { 7782, 7782, unicode_gc_Lu }, + { 7783, 7783, unicode_gc_Ll }, + { 7784, 7784, unicode_gc_Lu }, + { 7785, 7785, unicode_gc_Ll }, + { 7786, 7786, unicode_gc_Lu }, + { 7787, 7787, unicode_gc_Ll }, + { 7788, 7788, unicode_gc_Lu }, + { 7789, 7789, unicode_gc_Ll }, + { 7790, 7790, unicode_gc_Lu }, + { 7791, 7791, unicode_gc_Ll }, + { 7792, 7792, unicode_gc_Lu }, + { 7793, 7793, unicode_gc_Ll }, + { 7794, 7794, unicode_gc_Lu }, + { 7795, 7795, unicode_gc_Ll }, + { 7796, 7796, unicode_gc_Lu }, + { 7797, 7797, unicode_gc_Ll }, + { 7798, 7798, unicode_gc_Lu }, + { 7799, 7799, unicode_gc_Ll }, + { 7800, 7800, unicode_gc_Lu }, + { 7801, 7801, unicode_gc_Ll }, + { 7802, 7802, unicode_gc_Lu }, + { 7803, 7803, unicode_gc_Ll }, + { 7804, 7804, unicode_gc_Lu }, + { 7805, 7805, unicode_gc_Ll }, + { 7806, 7806, unicode_gc_Lu }, + { 7807, 7807, unicode_gc_Ll }, + { 7808, 7808, unicode_gc_Lu }, + { 7809, 7809, unicode_gc_Ll }, + { 7810, 7810, unicode_gc_Lu }, + { 7811, 7811, unicode_gc_Ll }, + { 7812, 7812, unicode_gc_Lu }, + { 7813, 7813, unicode_gc_Ll }, + { 7814, 7814, unicode_gc_Lu }, + { 7815, 7815, unicode_gc_Ll }, + { 7816, 7816, unicode_gc_Lu }, + { 7817, 7817, unicode_gc_Ll }, + { 7818, 7818, unicode_gc_Lu }, + { 7819, 7819, unicode_gc_Ll }, + { 7820, 7820, unicode_gc_Lu }, + { 7821, 7821, unicode_gc_Ll }, + { 7822, 7822, unicode_gc_Lu }, + { 7823, 7823, unicode_gc_Ll }, + { 7824, 7824, unicode_gc_Lu }, + { 7825, 7825, unicode_gc_Ll }, + { 7826, 7826, unicode_gc_Lu }, + { 7827, 7827, unicode_gc_Ll }, + { 7828, 7828, unicode_gc_Lu }, + { 7829, 7839, unicode_gc_Ll }, + { 7840, 7840, unicode_gc_Lu }, + { 7841, 7841, unicode_gc_Ll }, + { 7842, 7842, unicode_gc_Lu }, + { 7843, 7843, unicode_gc_Ll }, + { 7844, 7844, unicode_gc_Lu }, + { 7845, 7845, unicode_gc_Ll }, + { 7846, 7846, unicode_gc_Lu }, + { 7847, 7847, unicode_gc_Ll }, + { 7848, 7848, unicode_gc_Lu }, + { 7849, 7849, unicode_gc_Ll }, + { 7850, 7850, unicode_gc_Lu }, + { 7851, 7851, unicode_gc_Ll }, + { 7852, 7852, unicode_gc_Lu }, + { 7853, 7853, unicode_gc_Ll }, + { 7854, 7854, unicode_gc_Lu }, + { 7855, 7855, unicode_gc_Ll }, + { 7856, 7856, unicode_gc_Lu }, + { 7857, 7857, unicode_gc_Ll }, + { 7858, 7858, unicode_gc_Lu }, + { 7859, 7859, unicode_gc_Ll }, + { 7860, 7860, unicode_gc_Lu }, + { 7861, 7861, unicode_gc_Ll }, + { 7862, 7862, unicode_gc_Lu }, + { 7863, 7863, unicode_gc_Ll }, + { 7864, 7864, unicode_gc_Lu }, + { 7865, 7865, unicode_gc_Ll }, + { 7866, 7866, unicode_gc_Lu }, + { 7867, 7867, unicode_gc_Ll }, + { 7868, 7868, unicode_gc_Lu }, + { 7869, 7869, unicode_gc_Ll }, + { 7870, 7870, unicode_gc_Lu }, + { 7871, 7871, unicode_gc_Ll }, + { 7872, 7872, unicode_gc_Lu }, + { 7873, 7873, unicode_gc_Ll }, + { 7874, 7874, unicode_gc_Lu }, + { 7875, 7875, unicode_gc_Ll }, + { 7876, 7876, unicode_gc_Lu }, + { 7877, 7877, unicode_gc_Ll }, + { 7878, 7878, unicode_gc_Lu }, + { 7879, 7879, unicode_gc_Ll }, + { 7880, 7880, unicode_gc_Lu }, + { 7881, 7881, unicode_gc_Ll }, + { 7882, 7882, unicode_gc_Lu }, + { 7883, 7883, unicode_gc_Ll }, + { 7884, 7884, unicode_gc_Lu }, + { 7885, 7885, unicode_gc_Ll }, + { 7886, 7886, unicode_gc_Lu }, + { 7887, 7887, unicode_gc_Ll }, + { 7888, 7888, unicode_gc_Lu }, + { 7889, 7889, unicode_gc_Ll }, + { 7890, 7890, unicode_gc_Lu }, + { 7891, 7891, unicode_gc_Ll }, + { 7892, 7892, unicode_gc_Lu }, + { 7893, 7893, unicode_gc_Ll }, + { 7894, 7894, unicode_gc_Lu }, + { 7895, 7895, unicode_gc_Ll }, + { 7896, 7896, unicode_gc_Lu }, + { 7897, 7897, unicode_gc_Ll }, + { 7898, 7898, unicode_gc_Lu }, + { 7899, 7899, unicode_gc_Ll }, + { 7900, 7900, unicode_gc_Lu }, + { 7901, 7901, unicode_gc_Ll }, + { 7902, 7902, unicode_gc_Lu }, + { 7903, 7903, unicode_gc_Ll }, + { 7904, 7904, unicode_gc_Lu }, + { 7905, 7905, unicode_gc_Ll }, + { 7906, 7906, unicode_gc_Lu }, + { 7907, 7907, unicode_gc_Ll }, + { 7908, 7908, unicode_gc_Lu }, + { 7909, 7909, unicode_gc_Ll }, + { 7910, 7910, unicode_gc_Lu }, + { 7911, 7911, unicode_gc_Ll }, + { 7912, 7912, unicode_gc_Lu }, + { 7913, 7913, unicode_gc_Ll }, + { 7914, 7914, unicode_gc_Lu }, + { 7915, 7915, unicode_gc_Ll }, + { 7916, 7916, unicode_gc_Lu }, + { 7917, 7917, unicode_gc_Ll }, + { 7918, 7918, unicode_gc_Lu }, + { 7919, 7919, unicode_gc_Ll }, + { 7920, 7920, unicode_gc_Lu }, + { 7921, 7921, unicode_gc_Ll }, + { 7922, 7922, unicode_gc_Lu }, + { 7923, 7923, unicode_gc_Ll }, + { 7924, 7924, unicode_gc_Lu }, + { 7925, 7925, unicode_gc_Ll }, + { 7926, 7926, unicode_gc_Lu }, + { 7927, 7927, unicode_gc_Ll }, + { 7928, 7928, unicode_gc_Lu }, + { 7929, 7943, unicode_gc_Ll }, + { 7944, 7951, unicode_gc_Lu }, + { 7952, 7959, unicode_gc_Ll }, + { 7960, 7967, unicode_gc_Lu }, + { 7968, 7975, unicode_gc_Ll }, + { 7976, 7983, unicode_gc_Lu }, + { 7984, 7991, unicode_gc_Ll }, + { 7992, 7999, unicode_gc_Lu }, + { 8000, 8007, unicode_gc_Ll }, + { 8008, 8015, unicode_gc_Lu }, + { 8016, 8024, unicode_gc_Ll }, + { 8025, 8031, unicode_gc_Lu }, + { 8032, 8039, unicode_gc_Ll }, + { 8040, 8047, unicode_gc_Lu }, + { 8048, 8071, unicode_gc_Ll }, + { 8072, 8079, unicode_gc_Lt }, + { 8080, 8087, unicode_gc_Ll }, + { 8088, 8095, unicode_gc_Lt }, + { 8096, 8103, unicode_gc_Ll }, + { 8104, 8111, unicode_gc_Lt }, + { 8112, 8119, unicode_gc_Ll }, + { 8120, 8123, unicode_gc_Lu }, + { 8124, 8124, unicode_gc_Lt }, + { 8125, 8125, unicode_gc_Sk }, + { 8126, 8126, unicode_gc_Ll }, + { 8127, 8129, unicode_gc_Sk }, + { 8130, 8135, unicode_gc_Ll }, + { 8136, 8139, unicode_gc_Lu }, + { 8140, 8140, unicode_gc_Lt }, + { 8141, 8143, unicode_gc_Sk }, + { 8144, 8151, unicode_gc_Ll }, + { 8152, 8156, unicode_gc_Lu }, + { 8157, 8159, unicode_gc_Sk }, + { 8160, 8167, unicode_gc_Ll }, + { 8168, 8172, unicode_gc_Lu }, + { 8173, 8177, unicode_gc_Sk }, + { 8178, 8183, unicode_gc_Ll }, + { 8184, 8187, unicode_gc_Lu }, + { 8188, 8188, unicode_gc_Lt }, + { 8189, 8191, unicode_gc_Sk }, + { 8192, 8203, unicode_gc_Zs }, + { 8204, 8207, unicode_gc_Cf }, + { 8208, 8213, unicode_gc_Pd }, + { 8214, 8215, unicode_gc_Po }, + { 8216, 8216, unicode_gc_Pi }, + { 8217, 8217, unicode_gc_Pf }, + { 8218, 8218, unicode_gc_Ps }, + { 8219, 8220, unicode_gc_Pi }, + { 8221, 8221, unicode_gc_Pf }, + { 8222, 8222, unicode_gc_Ps }, + { 8223, 8223, unicode_gc_Pi }, + { 8224, 8231, unicode_gc_Po }, + { 8232, 8232, unicode_gc_Zl }, + { 8233, 8233, unicode_gc_Zp }, + { 8234, 8238, unicode_gc_Cf }, + { 8239, 8239, unicode_gc_Zs }, + { 8240, 8248, unicode_gc_Po }, + { 8249, 8249, unicode_gc_Pi }, + { 8250, 8250, unicode_gc_Pf }, + { 8251, 8254, unicode_gc_Po }, + { 8255, 8256, unicode_gc_Pc }, + { 8257, 8259, unicode_gc_Po }, + { 8260, 8260, unicode_gc_Sm }, + { 8261, 8261, unicode_gc_Ps }, + { 8262, 8262, unicode_gc_Pe }, + { 8263, 8273, unicode_gc_Po }, + { 8274, 8274, unicode_gc_Sm }, + { 8275, 8275, unicode_gc_Po }, + { 8276, 8278, unicode_gc_Pc }, + { 8279, 8286, unicode_gc_Po }, + { 8287, 8287, unicode_gc_Zs }, + { 8288, 8303, unicode_gc_Cf }, + { 8304, 8304, unicode_gc_No }, + { 8305, 8307, unicode_gc_Ll }, + { 8308, 8313, unicode_gc_No }, + { 8314, 8316, unicode_gc_Sm }, + { 8317, 8317, unicode_gc_Ps }, + { 8318, 8318, unicode_gc_Pe }, + { 8319, 8319, unicode_gc_Ll }, + { 8320, 8329, unicode_gc_No }, + { 8330, 8332, unicode_gc_Sm }, + { 8333, 8333, unicode_gc_Ps }, + { 8334, 8351, unicode_gc_Pe }, + { 8352, 8399, unicode_gc_Sc }, + { 8400, 8412, unicode_gc_Mn }, + { 8413, 8416, unicode_gc_Me }, + { 8417, 8417, unicode_gc_Mn }, + { 8418, 8420, unicode_gc_Me }, + { 8421, 8447, unicode_gc_Mn }, + { 8448, 8449, unicode_gc_So }, + { 8450, 8450, unicode_gc_Lu }, + { 8451, 8454, unicode_gc_So }, + { 8455, 8455, unicode_gc_Lu }, + { 8456, 8457, unicode_gc_So }, + { 8458, 8458, unicode_gc_Ll }, + { 8459, 8461, unicode_gc_Lu }, + { 8462, 8463, unicode_gc_Ll }, + { 8464, 8466, unicode_gc_Lu }, + { 8467, 8467, unicode_gc_Ll }, + { 8468, 8468, unicode_gc_So }, + { 8469, 8469, unicode_gc_Lu }, + { 8470, 8472, unicode_gc_So }, + { 8473, 8477, unicode_gc_Lu }, + { 8478, 8483, unicode_gc_So }, + { 8484, 8484, unicode_gc_Lu }, + { 8485, 8485, unicode_gc_So }, + { 8486, 8486, unicode_gc_Lu }, + { 8487, 8487, unicode_gc_So }, + { 8488, 8488, unicode_gc_Lu }, + { 8489, 8489, unicode_gc_So }, + { 8490, 8493, unicode_gc_Lu }, + { 8494, 8494, unicode_gc_So }, + { 8495, 8495, unicode_gc_Ll }, + { 8496, 8497, unicode_gc_Lu }, + { 8498, 8498, unicode_gc_So }, + { 8499, 8499, unicode_gc_Lu }, + { 8500, 8500, unicode_gc_Ll }, + { 8501, 8504, unicode_gc_Lo }, + { 8505, 8505, unicode_gc_Ll }, + { 8506, 8508, unicode_gc_So }, + { 8509, 8509, unicode_gc_Ll }, + { 8510, 8511, unicode_gc_Lu }, + { 8512, 8516, unicode_gc_Sm }, + { 8517, 8517, unicode_gc_Lu }, + { 8518, 8521, unicode_gc_Ll }, + { 8522, 8522, unicode_gc_So }, + { 8523, 8530, unicode_gc_Sm }, + { 8531, 8543, unicode_gc_No }, + { 8544, 8591, unicode_gc_Nl }, + { 8592, 8596, unicode_gc_Sm }, + { 8597, 8601, unicode_gc_So }, + { 8602, 8603, unicode_gc_Sm }, + { 8604, 8607, unicode_gc_So }, + { 8608, 8608, unicode_gc_Sm }, + { 8609, 8610, unicode_gc_So }, + { 8611, 8611, unicode_gc_Sm }, + { 8612, 8613, unicode_gc_So }, + { 8614, 8614, unicode_gc_Sm }, + { 8615, 8621, unicode_gc_So }, + { 8622, 8622, unicode_gc_Sm }, + { 8623, 8653, unicode_gc_So }, + { 8654, 8655, unicode_gc_Sm }, + { 8656, 8657, unicode_gc_So }, + { 8658, 8658, unicode_gc_Sm }, + { 8659, 8659, unicode_gc_So }, + { 8660, 8660, unicode_gc_Sm }, + { 8661, 8691, unicode_gc_So }, + { 8692, 8959, unicode_gc_Sm }, + { 8960, 8967, unicode_gc_So }, + { 8968, 8971, unicode_gc_Sm }, + { 8972, 8991, unicode_gc_So }, + { 8992, 8993, unicode_gc_Sm }, + { 8994, 9000, unicode_gc_So }, + { 9001, 9001, unicode_gc_Ps }, + { 9002, 9002, unicode_gc_Pe }, + { 9003, 9083, unicode_gc_So }, + { 9084, 9084, unicode_gc_Sm }, + { 9085, 9114, unicode_gc_So }, + { 9115, 9139, unicode_gc_Sm }, + { 9140, 9140, unicode_gc_Ps }, + { 9141, 9141, unicode_gc_Pe }, + { 9142, 9142, unicode_gc_Po }, + { 9143, 9311, unicode_gc_So }, + { 9312, 9371, unicode_gc_No }, + { 9372, 9449, unicode_gc_So }, + { 9450, 9471, unicode_gc_No }, + { 9472, 9654, unicode_gc_So }, + { 9655, 9655, unicode_gc_Sm }, + { 9656, 9664, unicode_gc_So }, + { 9665, 9665, unicode_gc_Sm }, + { 9666, 9719, unicode_gc_So }, + { 9720, 9727, unicode_gc_Sm }, + { 9728, 9838, unicode_gc_So }, + { 9839, 9839, unicode_gc_Sm }, + { 9840, 10087, unicode_gc_So }, + { 10088, 10088, unicode_gc_Ps }, + { 10089, 10089, unicode_gc_Pe }, + { 10090, 10090, unicode_gc_Ps }, + { 10091, 10091, unicode_gc_Pe }, + { 10092, 10092, unicode_gc_Ps }, + { 10093, 10093, unicode_gc_Pe }, + { 10094, 10094, unicode_gc_Ps }, + { 10095, 10095, unicode_gc_Pe }, + { 10096, 10096, unicode_gc_Ps }, + { 10097, 10097, unicode_gc_Pe }, + { 10098, 10098, unicode_gc_Ps }, + { 10099, 10099, unicode_gc_Pe }, + { 10100, 10100, unicode_gc_Ps }, + { 10101, 10101, unicode_gc_Pe }, + { 10102, 10131, unicode_gc_No }, + { 10132, 10191, unicode_gc_So }, + { 10192, 10213, unicode_gc_Sm }, + { 10214, 10214, unicode_gc_Ps }, + { 10215, 10215, unicode_gc_Pe }, + { 10216, 10216, unicode_gc_Ps }, + { 10217, 10217, unicode_gc_Pe }, + { 10218, 10218, unicode_gc_Ps }, + { 10219, 10223, unicode_gc_Pe }, + { 10224, 10239, unicode_gc_Sm }, + { 10240, 10495, unicode_gc_So }, + { 10496, 10626, unicode_gc_Sm }, + { 10627, 10627, unicode_gc_Ps }, + { 10628, 10628, unicode_gc_Pe }, + { 10629, 10629, unicode_gc_Ps }, + { 10630, 10630, unicode_gc_Pe }, + { 10631, 10631, unicode_gc_Ps }, + { 10632, 10632, unicode_gc_Pe }, + { 10633, 10633, unicode_gc_Ps }, + { 10634, 10634, unicode_gc_Pe }, + { 10635, 10635, unicode_gc_Ps }, + { 10636, 10636, unicode_gc_Pe }, + { 10637, 10637, unicode_gc_Ps }, + { 10638, 10638, unicode_gc_Pe }, + { 10639, 10639, unicode_gc_Ps }, + { 10640, 10640, unicode_gc_Pe }, + { 10641, 10641, unicode_gc_Ps }, + { 10642, 10642, unicode_gc_Pe }, + { 10643, 10643, unicode_gc_Ps }, + { 10644, 10644, unicode_gc_Pe }, + { 10645, 10645, unicode_gc_Ps }, + { 10646, 10646, unicode_gc_Pe }, + { 10647, 10647, unicode_gc_Ps }, + { 10648, 10648, unicode_gc_Pe }, + { 10649, 10711, unicode_gc_Sm }, + { 10712, 10712, unicode_gc_Ps }, + { 10713, 10713, unicode_gc_Pe }, + { 10714, 10714, unicode_gc_Ps }, + { 10715, 10715, unicode_gc_Pe }, + { 10716, 10747, unicode_gc_Sm }, + { 10748, 10748, unicode_gc_Ps }, + { 10749, 10749, unicode_gc_Pe }, + { 10750, 11007, unicode_gc_Sm }, + { 11008, 12287, unicode_gc_So }, + { 12288, 12288, unicode_gc_Zs }, + { 12289, 12291, unicode_gc_Po }, + { 12292, 12292, unicode_gc_So }, + { 12293, 12293, unicode_gc_Lm }, + { 12294, 12294, unicode_gc_Lo }, + { 12295, 12295, unicode_gc_Nl }, + { 12296, 12296, unicode_gc_Ps }, + { 12297, 12297, unicode_gc_Pe }, + { 12298, 12298, unicode_gc_Ps }, + { 12299, 12299, unicode_gc_Pe }, + { 12300, 12300, unicode_gc_Ps }, + { 12301, 12301, unicode_gc_Pe }, + { 12302, 12302, unicode_gc_Ps }, + { 12303, 12303, unicode_gc_Pe }, + { 12304, 12304, unicode_gc_Ps }, + { 12305, 12305, unicode_gc_Pe }, + { 12306, 12307, unicode_gc_So }, + { 12308, 12308, unicode_gc_Ps }, + { 12309, 12309, unicode_gc_Pe }, + { 12310, 12310, unicode_gc_Ps }, + { 12311, 12311, unicode_gc_Pe }, + { 12312, 12312, unicode_gc_Ps }, + { 12313, 12313, unicode_gc_Pe }, + { 12314, 12314, unicode_gc_Ps }, + { 12315, 12315, unicode_gc_Pe }, + { 12316, 12316, unicode_gc_Pd }, + { 12317, 12317, unicode_gc_Ps }, + { 12318, 12319, unicode_gc_Pe }, + { 12320, 12320, unicode_gc_So }, + { 12321, 12329, unicode_gc_Nl }, + { 12330, 12335, unicode_gc_Mn }, + { 12336, 12336, unicode_gc_Pd }, + { 12337, 12341, unicode_gc_Lm }, + { 12342, 12343, unicode_gc_So }, + { 12344, 12346, unicode_gc_Nl }, + { 12347, 12347, unicode_gc_Lm }, + { 12348, 12348, unicode_gc_Lo }, + { 12349, 12349, unicode_gc_Po }, + { 12350, 12352, unicode_gc_So }, + { 12353, 12440, unicode_gc_Lo }, + { 12441, 12442, unicode_gc_Mn }, + { 12443, 12444, unicode_gc_Sk }, + { 12445, 12446, unicode_gc_Lm }, + { 12447, 12447, unicode_gc_Lo }, + { 12448, 12448, unicode_gc_Pd }, + { 12449, 12538, unicode_gc_Lo }, + { 12539, 12539, unicode_gc_Pc }, + { 12540, 12542, unicode_gc_Lm }, + { 12543, 12687, unicode_gc_Lo }, + { 12688, 12689, unicode_gc_So }, + { 12690, 12693, unicode_gc_No }, + { 12694, 12703, unicode_gc_So }, + { 12704, 12799, unicode_gc_Lo }, + { 12800, 12831, unicode_gc_So }, + { 12832, 12841, unicode_gc_No }, + { 12842, 12880, unicode_gc_So }, + { 12881, 12895, unicode_gc_No }, + { 12896, 12927, unicode_gc_So }, + { 12928, 12937, unicode_gc_No }, + { 12938, 12976, unicode_gc_So }, + { 12977, 12991, unicode_gc_No }, + { 12992, 13311, unicode_gc_So }, + { 13312, 19903, unicode_gc_Lo }, + { 19904, 19967, unicode_gc_So }, + { 19968, 42127, unicode_gc_Lo }, + { 42128, 44031, unicode_gc_So }, + { 44032, 55295, unicode_gc_Lo }, + { 55296, 57343, unicode_gc_Cs }, + { 57344, 63743, unicode_gc_Co }, + { 63744, 64255, unicode_gc_Lo }, + { 64256, 64284, unicode_gc_Ll }, + { 64285, 64285, unicode_gc_Lo }, + { 64286, 64286, unicode_gc_Mn }, + { 64287, 64296, unicode_gc_Lo }, + { 64297, 64297, unicode_gc_Sm }, + { 64298, 64829, unicode_gc_Lo }, + { 64830, 64830, unicode_gc_Ps }, + { 64831, 64847, unicode_gc_Pe }, + { 64848, 65019, unicode_gc_Lo }, + { 65020, 65020, unicode_gc_Sc }, + { 65021, 65023, unicode_gc_So }, + { 65024, 65071, unicode_gc_Mn }, + { 65072, 65072, unicode_gc_Po }, + { 65073, 65074, unicode_gc_Pd }, + { 65075, 65076, unicode_gc_Pc }, + { 65077, 65077, unicode_gc_Ps }, + { 65078, 65078, unicode_gc_Pe }, + { 65079, 65079, unicode_gc_Ps }, + { 65080, 65080, unicode_gc_Pe }, + { 65081, 65081, unicode_gc_Ps }, + { 65082, 65082, unicode_gc_Pe }, + { 65083, 65083, unicode_gc_Ps }, + { 65084, 65084, unicode_gc_Pe }, + { 65085, 65085, unicode_gc_Ps }, + { 65086, 65086, unicode_gc_Pe }, + { 65087, 65087, unicode_gc_Ps }, + { 65088, 65088, unicode_gc_Pe }, + { 65089, 65089, unicode_gc_Ps }, + { 65090, 65090, unicode_gc_Pe }, + { 65091, 65091, unicode_gc_Ps }, + { 65092, 65092, unicode_gc_Pe }, + { 65093, 65094, unicode_gc_Po }, + { 65095, 65095, unicode_gc_Ps }, + { 65096, 65096, unicode_gc_Pe }, + { 65097, 65100, unicode_gc_Po }, + { 65101, 65103, unicode_gc_Pc }, + { 65104, 65111, unicode_gc_Po }, + { 65112, 65112, unicode_gc_Pd }, + { 65113, 65113, unicode_gc_Ps }, + { 65114, 65114, unicode_gc_Pe }, + { 65115, 65115, unicode_gc_Ps }, + { 65116, 65116, unicode_gc_Pe }, + { 65117, 65117, unicode_gc_Ps }, + { 65118, 65118, unicode_gc_Pe }, + { 65119, 65121, unicode_gc_Po }, + { 65122, 65122, unicode_gc_Sm }, + { 65123, 65123, unicode_gc_Pd }, + { 65124, 65127, unicode_gc_Sm }, + { 65128, 65128, unicode_gc_Po }, + { 65129, 65129, unicode_gc_Sc }, + { 65130, 65135, unicode_gc_Po }, + { 65136, 65278, unicode_gc_Lo }, + { 65279, 65280, unicode_gc_Cf }, + { 65281, 65283, unicode_gc_Po }, + { 65284, 65284, unicode_gc_Sc }, + { 65285, 65287, unicode_gc_Po }, + { 65288, 65288, unicode_gc_Ps }, + { 65289, 65289, unicode_gc_Pe }, + { 65290, 65290, unicode_gc_Po }, + { 65291, 65291, unicode_gc_Sm }, + { 65292, 65292, unicode_gc_Po }, + { 65293, 65293, unicode_gc_Pd }, + { 65294, 65295, unicode_gc_Po }, + { 65296, 65305, unicode_gc_Nd }, + { 65306, 65307, unicode_gc_Po }, + { 65308, 65310, unicode_gc_Sm }, + { 65311, 65312, unicode_gc_Po }, + { 65313, 65338, unicode_gc_Lu }, + { 65339, 65339, unicode_gc_Ps }, + { 65340, 65340, unicode_gc_Po }, + { 65341, 65341, unicode_gc_Pe }, + { 65342, 65342, unicode_gc_Sk }, + { 65343, 65343, unicode_gc_Pc }, + { 65344, 65344, unicode_gc_Sk }, + { 65345, 65370, unicode_gc_Ll }, + { 65371, 65371, unicode_gc_Ps }, + { 65372, 65372, unicode_gc_Sm }, + { 65373, 65373, unicode_gc_Pe }, + { 65374, 65374, unicode_gc_Sm }, + { 65375, 65375, unicode_gc_Ps }, + { 65376, 65376, unicode_gc_Pe }, + { 65377, 65377, unicode_gc_Po }, + { 65378, 65378, unicode_gc_Ps }, + { 65379, 65379, unicode_gc_Pe }, + { 65380, 65380, unicode_gc_Po }, + { 65381, 65381, unicode_gc_Pc }, + { 65382, 65391, unicode_gc_Lo }, + { 65392, 65392, unicode_gc_Lm }, + { 65393, 65437, unicode_gc_Lo }, + { 65438, 65439, unicode_gc_Lm }, + { 65440, 65503, unicode_gc_Lo }, + { 65504, 65505, unicode_gc_Sc }, + { 65506, 65506, unicode_gc_Sm }, + { 65507, 65507, unicode_gc_Sk }, + { 65508, 65508, unicode_gc_So }, + { 65509, 65511, unicode_gc_Sc }, + { 65512, 65512, unicode_gc_So }, + { 65513, 65516, unicode_gc_Sm }, + { 65517, 65528, unicode_gc_So }, + { 65529, 65531, unicode_gc_Cf }, + { 65532, 65535, unicode_gc_So }, + { 65536, 65791, unicode_gc_Lo }, + { 65792, 65793, unicode_gc_Po }, + { 65794, 65798, unicode_gc_So }, + { 65799, 65846, unicode_gc_No }, + { 65847, 66303, unicode_gc_So }, + { 66304, 66335, unicode_gc_Lo }, + { 66336, 66351, unicode_gc_No }, + { 66352, 66377, unicode_gc_Lo }, + { 66378, 66431, unicode_gc_Nl }, + { 66432, 66462, unicode_gc_Lo }, + { 66463, 66559, unicode_gc_Po }, + { 66560, 66599, unicode_gc_Lu }, + { 66600, 66639, unicode_gc_Ll }, + { 66640, 66719, unicode_gc_Lo }, + { 66720, 67583, unicode_gc_Nd }, + { 67584, 118783, unicode_gc_Lo }, + { 118784, 119140, unicode_gc_So }, + { 119141, 119142, unicode_gc_Mc }, + { 119143, 119145, unicode_gc_Mn }, + { 119146, 119148, unicode_gc_So }, + { 119149, 119154, unicode_gc_Mc }, + { 119155, 119162, unicode_gc_Cf }, + { 119163, 119170, unicode_gc_Mn }, + { 119171, 119172, unicode_gc_So }, + { 119173, 119179, unicode_gc_Mn }, + { 119180, 119209, unicode_gc_So }, + { 119210, 119213, unicode_gc_Mn }, + { 119214, 119807, unicode_gc_So }, + { 119808, 119833, unicode_gc_Lu }, + { 119834, 119859, unicode_gc_Ll }, + { 119860, 119885, unicode_gc_Lu }, + { 119886, 119911, unicode_gc_Ll }, + { 119912, 119937, unicode_gc_Lu }, + { 119938, 119963, unicode_gc_Ll }, + { 119964, 119989, unicode_gc_Lu }, + { 119990, 120015, unicode_gc_Ll }, + { 120016, 120041, unicode_gc_Lu }, + { 120042, 120067, unicode_gc_Ll }, + { 120068, 120093, unicode_gc_Lu }, + { 120094, 120119, unicode_gc_Ll }, + { 120120, 120145, unicode_gc_Lu }, + { 120146, 120171, unicode_gc_Ll }, + { 120172, 120197, unicode_gc_Lu }, + { 120198, 120223, unicode_gc_Ll }, + { 120224, 120249, unicode_gc_Lu }, + { 120250, 120275, unicode_gc_Ll }, + { 120276, 120301, unicode_gc_Lu }, + { 120302, 120327, unicode_gc_Ll }, + { 120328, 120353, unicode_gc_Lu }, + { 120354, 120379, unicode_gc_Ll }, + { 120380, 120405, unicode_gc_Lu }, + { 120406, 120431, unicode_gc_Ll }, + { 120432, 120457, unicode_gc_Lu }, + { 120458, 120487, unicode_gc_Ll }, + { 120488, 120512, unicode_gc_Lu }, + { 120513, 120513, unicode_gc_Sm }, + { 120514, 120538, unicode_gc_Ll }, + { 120539, 120539, unicode_gc_Sm }, + { 120540, 120545, unicode_gc_Ll }, + { 120546, 120570, unicode_gc_Lu }, + { 120571, 120571, unicode_gc_Sm }, + { 120572, 120596, unicode_gc_Ll }, + { 120597, 120597, unicode_gc_Sm }, + { 120598, 120603, unicode_gc_Ll }, + { 120604, 120628, unicode_gc_Lu }, + { 120629, 120629, unicode_gc_Sm }, + { 120630, 120654, unicode_gc_Ll }, + { 120655, 120655, unicode_gc_Sm }, + { 120656, 120661, unicode_gc_Ll }, + { 120662, 120686, unicode_gc_Lu }, + { 120687, 120687, unicode_gc_Sm }, + { 120688, 120712, unicode_gc_Ll }, + { 120713, 120713, unicode_gc_Sm }, + { 120714, 120719, unicode_gc_Ll }, + { 120720, 120744, unicode_gc_Lu }, + { 120745, 120745, unicode_gc_Sm }, + { 120746, 120770, unicode_gc_Ll }, + { 120771, 120771, unicode_gc_Sm }, + { 120772, 120781, unicode_gc_Ll }, + { 120782, 131071, unicode_gc_Nd }, + { 131072, 917504, unicode_gc_Lo }, + { 917505, 917759, unicode_gc_Cf }, + { 917760, 983039, unicode_gc_Mn }, + { 983040, 1114109, unicode_gc_Co }, +}; +/* arch-tag:aba6847fbd64858c183a471a517838a9 */ diff --git a/lib/user.c b/lib/user.c new file mode 100644 index 0000000..7cbca98 --- /dev/null +++ b/lib/user.c @@ -0,0 +1,65 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <sys/types.h> +#include <errno.h> +#include <pwd.h> +#include <grp.h> +#include <unistd.h> + +#include "user.h" +#include "log.h" +#include "configuration.h" + +void become_mortal(void) { + struct passwd *pw; + + if(config->user) { + if(!(pw = getpwnam(config->user))) + fatal(0, "cannot find user %s", config->user); + if(pw->pw_uid != getuid()) { + if(initgroups(config->user, pw->pw_gid)) + fatal(errno, "error calling initgroups"); + if(setgid(pw->pw_gid) < 0) fatal(errno, "error calling setgid"); + if(setuid(pw->pw_uid) < 0) fatal(errno, "error calling setgid"); + info("changed to user %s (uid %lu)", config->user, (unsigned long)getuid()); + } + /* sanity checks */ + if(getuid() != pw->pw_uid) fatal(0, "wrong real uid"); + if(geteuid() != pw->pw_uid) fatal(0, "wrong effective uid"); + if(getgid() != pw->pw_gid) fatal(0, "wrong real gid"); + if(getegid() != pw->pw_gid) fatal(0, "wrong effective gid"); + if(setuid(0) != -1) fatal(0, "setuid(0) unexpectedly succeeded"); + if(seteuid(0) != -1) fatal(0, "seteuid(0) unexpectedly succeeded"); + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:GTEl+AUapne19BB73yRZdw */ diff --git a/lib/user.h b/lib/user.h new file mode 100644 index 0000000..b895c5a --- /dev/null +++ b/lib/user.h @@ -0,0 +1,36 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef USER_H +#define USER_H + +void become_mortal(void); + +#endif /* USER_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:pl++oRgq6iybMI28uVPCsA */ diff --git a/lib/utf8.c b/lib/utf8.c new file mode 100644 index 0000000..d4dc472 --- /dev/null +++ b/lib/utf8.c @@ -0,0 +1,41 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include "utf8.h" + +int validutf8(const char *s) { + unsigned long c; + + while(*s) + PARSE_UTF8(s, c, return 0); + return 1; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:WGilqGFnXhhAeU5ZnbG8oQ */ diff --git a/lib/utf8.h b/lib/utf8.h new file mode 100644 index 0000000..a051019 --- /dev/null +++ b/lib/utf8.h @@ -0,0 +1,66 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#ifndef UTF8_H +#define UTF8_H + +#define PARSE_UTF8(S,C,E) do { \ + if((unsigned char)*S < 0x80) \ + C = *S++; \ + else if((unsigned char)*S <= 0xDF) { \ + C = (*S++ & 0x1F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x80) { E; } \ + } else if((unsigned char)*S <= 0xEF) { \ + C = (*S++ & 0x0F) << 12; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x800 \ + || (C >= 0xD800 && C <= 0xDFFF)) { \ + E; \ + } \ + } else if((unsigned char)*S <= 0xF7) { \ + C = (*S++ & 0x07) << 18; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 12; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F) << 6; \ + if((*S & 0xC0) != 0x80) { E; } \ + C |= (*S++ & 0x3F); \ + if(C < 0x10000 || C > 0x10FFFF) { E; } \ + } else { \ + E; \ + } \ +} while(0) + +int validutf8(const char *s); +/* return nonzero if S is a valid UTF-8 sequence, else false */ + +#endif /* UTF8_h */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:456aedaec99ad19d321ac15b7765da4d */ diff --git a/lib/vacopy.h b/lib/vacopy.h new file mode 100644 index 0000000..844ec31 --- /dev/null +++ b/lib/vacopy.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef VACOPY_H +#define VACOPY_H + +#ifndef va_copy +# ifdef __va_copy +# define va_copy __va_copy +# else +# define va_copy(d, s) ((void)memcpy(&d, &s, sizeof s)) +# endif +#endif + +#endif /* VACOPY_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a57c12f1df1735ec855a1450117e7ccf */ diff --git a/lib/vector.c b/lib/vector.c new file mode 100644 index 0000000..f416104 --- /dev/null +++ b/lib/vector.c @@ -0,0 +1,49 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <string.h> + +#include "mem.h" +#include "log.h" +#include "vector.h" + +void vector_append_many(struct vector *v, char **vec, int nvec) { + while(nvec-- > 0) + vector_append(v, *vec++); +} + +void dynstr_append_bytes(struct dynstr *v, const char *ptr, size_t n) { + while(n > 0) { + dynstr_append(v, *ptr++); + n--; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:4f207f71a87ed2f80b527e1fecddeaf4 */ diff --git a/lib/vector.h b/lib/vector.h new file mode 100644 index 0000000..8fbcc96 --- /dev/null +++ b/lib/vector.h @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef VECTOR_H +#define VECTOR_H + +#define VECTOR_TYPE(NAME,ETYPE,REALLOC) \ + \ +struct NAME { \ + ETYPE *vec; \ + int nvec, nslots; \ +}; \ + \ +static inline void NAME##_init(struct NAME *v) { \ + memset(v, 0, sizeof *v); \ +} \ + \ +static inline void NAME##_append(struct NAME *v, ETYPE val) { \ + if(v->nvec >= v->nslots) { \ + v->nslots = v->nslots ? 2 * v->nslots : 16; \ + v->vec = REALLOC(v->vec, v->nslots * sizeof(ETYPE)); \ + } \ + v->vec[v->nvec++] = val; \ +} \ + \ +static inline void NAME##_terminate(struct NAME *v) { \ + if(v->nvec >= v->nslots) \ + v->vec = REALLOC(v->vec, ++v->nslots * sizeof(ETYPE)); \ + memset(&v->vec[v->nvec], 0, sizeof (ETYPE)); \ +} \ + +VECTOR_TYPE(vector, char *, xrealloc) +VECTOR_TYPE(dynstr, char, xrealloc_noptr) +VECTOR_TYPE(dynstr_ucs4, uint32_t, xrealloc_noptr) + +void vector_append_many(struct vector *v, char **vec, int nvec); + +void dynstr_append_bytes(struct dynstr *v, const char *ptr, size_t n); + +static inline void dynstr_append_string(struct dynstr *v, const char *ptr) { + dynstr_append_bytes(v, ptr, strlen(ptr)); +} + +#endif /* VECTOR_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:a57474a59d7ebd67cda96bf05bd89049 */ diff --git a/lib/words.c b/lib/words.c new file mode 100644 index 0000000..2e4001d --- /dev/null +++ b/lib/words.c @@ -0,0 +1,177 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <string.h> +#include <stddef.h> + +#include "mem.h" +#include "vector.h" +#include "table.h" +#include "words.h" +#include "utf8.h" + +#include "casefold.h" +#include "unicodegc.h" + +const char *casefold(const char *ptr) { + struct dynstr d; + int l, r, m; + uint32_t c; + const struct cm *t; + const char *start, *s = ptr; + + dynstr_init(&d); + while(*s) { + start = s; + PARSE_UTF8(s, c, return ptr); + /* seek the folded equivalent */ + t = cm[c & CM_MASK]; + l = 0; + r = cmn[c & CM_MASK] - 1; + while(l <= r && c != t[m = (l + r) / 2].ch) + if(c < t[m].ch) + r = m - 1; + else + l = m + 1; + if(l <= r) + dynstr_append_string(&d, t[m].tr); + else + dynstr_append_bytes(&d, start, s - start); + } + dynstr_terminate(&d); + return d.vec; +} + +static enum unicode_gc_cat cat(uint32_t c) { + int l, r, m; + + l = 0; + r = sizeof gcs / sizeof *gcs; + while(l <= r) { + m = (l + r) / 2; + if(c < gcs[m].l) + r = m - 1; + else if(c > gcs[m].h) + l = m + 1; + else + return gcs[m].cat; + } + return unicode_gc_none; +} + +/* XXX this is a bit kludgy */ + +char **words(const char *s, int *nvecp) { + struct vector v; + struct dynstr d; + const char *start; + uint32_t c; + int in_word = 0; + + vector_init(&v); + while(*s) { + start = s; + PARSE_UTF8(s, c, return 0); + /* special cases first */ + switch(c) { + case '/': + case '.': + case '+': + case '&': + case ':': + case '_': + case '-': + goto separator; + } + /* do the rest on category */ + switch(cat(c)) { + case unicode_gc_Ll: + case unicode_gc_Lm: + case unicode_gc_Lo: + case unicode_gc_Lt: + case unicode_gc_Lu: + case unicode_gc_Nd: + case unicode_gc_Nl: + case unicode_gc_No: + case unicode_gc_Sc: + case unicode_gc_Sk: + case unicode_gc_Sm: + case unicode_gc_So: + /* letters, digits and symbols are considered to be part of + * words */ + if(!in_word) { + dynstr_init(&d); + in_word = 1; + } + dynstr_append_bytes(&d, start, s - start); + break; + + case unicode_gc_Cc: + case unicode_gc_Cf: + case unicode_gc_Co: + case unicode_gc_Cs: + case unicode_gc_Zl: + case unicode_gc_Zp: + case unicode_gc_Zs: + case unicode_gc_Pe: + case unicode_gc_Ps: + separator: + if(in_word) { + dynstr_terminate(&d); + vector_append(&v, d.vec); + in_word = 0; + } + break; + + case unicode_gc_Mc: + case unicode_gc_Me: + case unicode_gc_Mn: + case unicode_gc_Pc: + case unicode_gc_Pd: + case unicode_gc_Pf: + case unicode_gc_Pi: + case unicode_gc_Po: + case unicode_gc_none: + /* control and punctuation is completely ignored */ + break; + + } + } + if(in_word) { + /* pick up the final word */ + dynstr_terminate(&d); + vector_append(&v, d.vec); + } + vector_terminate(&v); + if(nvecp) + *nvecp = v.nvec; + return v.vec; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:0ea1f1700f14cd031b7f1fbbcca765fa */ diff --git a/lib/words.h b/lib/words.h new file mode 100644 index 0000000..52a6839 --- /dev/null +++ b/lib/words.h @@ -0,0 +1,40 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef WORDS_H +#define WORDS_H + +const char *casefold(const char *s); +/* return a case-folded version of UTF-8 string @s@, or the original + * string if malformed. */ + +char **words(const char *s, int *nvecp); +/* return the words found in UTF-8 string @s@, with punctuation + * stripped out. (Doesn't casefold.) */ + +#endif /* WORDS_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:e575b078ed351ae974d55ed5dfadfaa2 */ diff --git a/lib/wstat.c b/lib/wstat.c new file mode 100644 index 0000000..bd387ff --- /dev/null +++ b/lib/wstat.c @@ -0,0 +1,58 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <sys/wait.h> +#include <stdio.h> +#include <string.h> +#include <signal.h> + +#include "mem.h" +#include "log.h" +#include "wstat.h" +#include "printf.h" + +const char *wstat(int w) { + int n; + char *r; + + if(WIFEXITED(w)) + n = byte_xasprintf(&r, "exited with status %d", WEXITSTATUS(w)); + else if(WIFSIGNALED(w)) + n = byte_xasprintf(&r, "terminated by signal %d (%s)%s", + WTERMSIG(w), strsignal(WTERMSIG(w)), + WCOREDUMP(w) ? " - core dumped" : ""); + else if(WIFSTOPPED(w)) + n = byte_xasprintf(&r, "stopped by signal %d (%s)", + WSTOPSIG(w), strsignal(WSTOPSIG(w))); + else + n = byte_xasprintf(&r, "terminated with unknown wait status %#x", + (unsigned)w); + return n >= 0 ? r : "[could not convert wait status]"; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:266c61bd39bdfb327908ad3dd95412ae */ diff --git a/lib/wstat.h b/lib/wstat.h new file mode 100644 index 0000000..bf80e42 --- /dev/null +++ b/lib/wstat.h @@ -0,0 +1,36 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef WSTAT_H +#define WSTAT_H + +const char *wstat(int w); +/* Format wait status @w@. In extremis the return value might be a + * pointer to a string literal. The result should always be ASCII. */ + +#endif /* WSTAT_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7bf1b99d0880deb7c52839aef20fa88d */ diff --git a/plugins/Makefile.am b/plugins/Makefile.am new file mode 100644 index 0000000..3c87817 --- /dev/null +++ b/plugins/Makefile.am @@ -0,0 +1,43 @@ +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +pkglib_LTLIBRARIES=tracklength.la fs.la notify.la exec.la shell.la \ + execraw.la +AM_CPPFLAGS=-I${top_srcdir}/lib + +notify_la_SOURCES=notify.c +notify_la_LDFLAGS=-module + +tracklength_la_SOURCES=tracklength.c mad.c madshim.h +tracklength_la_LDFLAGS=-module +tracklength_la_LIBADD=@LIBVORBISFILE@ @LIBMAD@ + +fs_la_SOURCES=fs.c +fs_la_LDFLAGS=-module + +exec_la_SOURCES=exec.c +exec_la_LDFLAGS=-module + +execraw_la_SOURCES=execraw.c +execraw_la_LDFLAGS=-module + +shell_la_SOURCES=shell.c +shell_la_LDFLAGS=-module +# arch-tag:45b01b1ebc885c96d6faad2d824a7eae diff --git a/plugins/exec.c b/plugins/exec.c new file mode 100644 index 0000000..283ebd3 --- /dev/null +++ b/plugins/exec.c @@ -0,0 +1,58 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> + +#include <disorder.h> + +#ifndef TYPE +# define TYPE DISORDER_PLAYER_STANDALONE +#endif +const unsigned long disorder_player_type = TYPE; + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char attribute((unused)) *track, + void attribute((unused)) *data) { + int i, j; + const char **vec; + + vec = disorder_malloc((nparameters + 2) * sizeof (char *)); + i = 0; + j = 0; + for(i = 0; i < nparameters; ++i) + vec[j++] = parameters[i]; + vec[j++] = path; + vec[j] = 0; + execvp(vec[0], (char **)vec); + disorder_fatal(errno, "error executing %s", vec[0]); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:39dab35f9dd51a2713a16234ee7b5030 */ diff --git a/plugins/execraw.c b/plugins/execraw.c new file mode 100644 index 0000000..3c3595a --- /dev/null +++ b/plugins/execraw.c @@ -0,0 +1,12 @@ +#define TYPE DISORDER_PLAYER_RAW +#include "exec.c" + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ +/* arch-tag:gNkzdgw34U7sQf/CWqcWrQ */ diff --git a/plugins/fs.c b/plugins/fs.c new file mode 100644 index 0000000..3212a04 --- /dev/null +++ b/plugins/fs.c @@ -0,0 +1,85 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <syslog.h> +#include <errno.h> +#include <dirent.h> +#include <stdio.h> +#include <unistd.h> + +#include <disorder.h> + +void disorder_scan(const char *path) { + struct stat sb; + DIR *dp; + struct dirent *de; + char *np; + + if(stat(path, &sb) < 0) { + disorder_error(errno, "cannot lstat %s", path); + return; + } + /* skip files that aren't world-readable */ + if(!(sb.st_mode & 0004)) + return; + if(S_ISDIR(sb.st_mode)) { + if(!(dp = opendir(path))) { + disorder_error(errno, "cannot open directory %s", path); + return; + } + while((errno = 0), + (de = readdir(dp))) { + if(de->d_name[0] != '.') { + disorder_asprintf(&np, "%s/%s", path, de->d_name); + disorder_scan(np); + } + } + if(errno) + disorder_error(errno, "error reading directory %s", path); + closedir(dp); + } else if(S_ISREG(sb.st_mode)) + if(printf("%s%c", path, 0) < 0) + disorder_fatal(errno, "error writing to scanner output pipe"); +} + +int disorder_check(const char attribute((unused)) *root, const char *path) { + if(access(path, R_OK) == 0) + return 1; + else if(errno == ENOENT) + return 0; + else { + disorder_error(errno, "cannot access %s", path); + return -1; + } +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:070e62d92ebc26329f0a3961d615476f */ diff --git a/plugins/mad.c b/plugins/mad.c new file mode 100644 index 0000000..32cbded --- /dev/null +++ b/plugins/mad.c @@ -0,0 +1,232 @@ +/* This file is a subset of the debian source tarball of mpg321-0.2.10.3/mad.c + - see http://mpg321.sourceforge.net/ */ + +/* + mpg321 - a fully free clone of mpg123. + Copyright (C) 2001 Joe Drew + + Originally based heavily upon: + plaympeg - Sample MPEG player using the SMPEG library + Copyright (C) 1999 Loki Entertainment Software + + Also uses some code from + mad - MPEG audio decoder + Copyright (C) 2000-2001 Robert Leslie + + Original playlist code contributed by Tobias Bengtsson <tobbe@tobbe.nu> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +#include <sys/types.h> +#include <string.h> + +#include <mad.h> + +#include "madshim.h" + +/* XING parsing is from the MAD winamp input plugin */ + +struct xing { + int flags; + unsigned long frames; + unsigned long bytes; + unsigned char toc[100]; + long scale; +}; + +enum { + XING_FRAMES = 0x0001, + XING_BYTES = 0x0002, + XING_TOC = 0x0004, + XING_SCALE = 0x0008 +}; + +# define XING_MAGIC (('X' << 24) | ('i' << 16) | ('n' << 8) | 'g') + +static +int parse_xing(struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen) +{ + if (bitlen < 64 || mad_bit_read(&ptr, 32) != XING_MAGIC) + goto fail; + + xing->flags = mad_bit_read(&ptr, 32); + bitlen -= 64; + + if (xing->flags & XING_FRAMES) { + if (bitlen < 32) + goto fail; + + xing->frames = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_BYTES) { + if (bitlen < 32) + goto fail; + + xing->bytes = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_TOC) { + int i; + + if (bitlen < 800) + goto fail; + + for (i = 0; i < 100; ++i) + xing->toc[i] = mad_bit_read(&ptr, 8); + + bitlen -= 800; + } + + if (xing->flags & XING_SCALE) { + if (bitlen < 32) + goto fail; + + xing->scale = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + return 1; + + fail: + xing->flags = 0; + return 0; +} + +/* Following two functions are adapted from mad_timer, from the + libmad distribution */ +void scan_mp3(void const *ptr, ssize_t len, buffer *buf) +{ + struct mad_stream stream; + struct mad_header header; + struct xing xing; + + unsigned long bitrate = 0; + int has_xing = 0; + int is_vbr = 0; + + memset(&xing, 0, sizeof xing); + + mad_stream_init(&stream); + mad_header_init(&header); + + mad_stream_buffer(&stream, ptr, len); + + buf->num_frames = 0; + + /* There are three ways of calculating the length of an mp3: + 1) Constant bitrate: One frame can provide the information + needed: # of frames and duration. Just see how long it + is and do the division. + 2) Variable bitrate: Xing tag. It provides the number of + frames. Each frame has the same number of samples, so + just use that. + 3) All: Count up the frames and duration of each frames + by decoding each one. We do this if we've no other + choice, i.e. if it's a VBR file with no Xing tag. + */ + + while (1) + { + if (mad_header_decode(&header, &stream) == -1) + { + if (MAD_RECOVERABLE(stream.error)) + continue; + else + break; + } + + /* Limit xing testing to the first frame header */ + if (!buf->num_frames++) + { + if(parse_xing(&xing, stream.anc_ptr, stream.anc_bitlen)) + { + is_vbr = 1; + + if (xing.flags & XING_FRAMES) + { + /* We use the Xing tag only for frames. If it doesn't have that + information, it's useless to us and we have to treat it as a + normal VBR file */ + has_xing = 1; + buf->num_frames = xing.frames; + break; + } + } + } + + /* Test the first n frames to see if this is a VBR file */ + if (!is_vbr && !(buf->num_frames > 20)) + { + if (bitrate && header.bitrate != bitrate) + { + is_vbr = 1; + } + + else + { + bitrate = header.bitrate; + } + } + + /* We have to assume it's not a VBR file if it hasn't already been + marked as one and we've checked n frames for different bitrates */ + else if (!is_vbr) + { + break; + } + + mad_timer_add(&buf->duration, header.duration); + } + + if (!is_vbr) + { + double time = (len * 8.0) / (header.bitrate); /* time in seconds */ + double timefrac = (double)time - ((long)(time)); + long nsamples = 32 * MAD_NSBSAMPLES(&header); /* samples per frame */ + + /* samplerate is a constant */ + buf->num_frames = (long) (time * header.samplerate / nsamples); + + mad_timer_set(&buf->duration, (long)time, (long)(timefrac*100), 100); + } + + else if (has_xing) + { + /* modify header.duration since we don't need it anymore */ + mad_timer_multiply(&header.duration, buf->num_frames); + buf->duration = header.duration; + } + + else + { + /* the durations have been added up, and the number of frames + counted. We do nothing here. */ + } + + mad_header_finish(&header); + mad_stream_finish(&stream); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:2b2d6ec382b3b37f3ba8a0bb1d345fbd */ diff --git a/plugins/madshim.h b/plugins/madshim.h new file mode 100644 index 0000000..20ec3dc --- /dev/null +++ b/plugins/madshim.h @@ -0,0 +1,41 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef MADSHIM_H +#define MADSHIM_H + +/* shim to integrate code from mpg123 */ + +typedef struct { + int num_frames; + mad_timer_t duration; +} buffer; + +void scan_mp3(void const *ptr, ssize_t len, buffer *buf); + +#endif /* MADSHIM_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:7b06901432784a575a0a9fe7df7010ab */ diff --git a/plugins/notify.c b/plugins/notify.c new file mode 100644 index 0000000..eede49f --- /dev/null +++ b/plugins/notify.c @@ -0,0 +1,93 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> +#include "types.h" + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> + +#include <disorder.h> + +static void record(const char *track, const char *what) { + const char *count; + int ncount; + char buf[64]; + + if((count = disorder_track_get_data(track, what))) + ncount = atoi(count); + else + ncount = 0; + disorder_snprintf(buf, sizeof buf, "%d", ncount + 1); + disorder_track_set_data(track, what, buf); +} + +void disorder_notify_play(const char *track, + const char *submitter) { + char buf[64]; + + if(submitter) + record(track, "requested"); + record(track, "played"); + disorder_snprintf(buf, sizeof buf, "%"PRIdMAX, (intmax_t)time(0)); + disorder_track_set_data(track, "played_time", buf); +} + +void disorder_notify_queue(const char attribute((unused)) *track, + const char attribute((unused)) *submitter) { +} + +void disorder_notify_scratch(const char *track, + const char attribute((unused)) *submitter, + const char attribute((unused)) *scratcher, + int attribute((unused)) seconds) { + record(track, "scratched"); +} + +void disorder_notify_not_scratched(const char *track, + const char attribute((unused)) *submitter) { + record(track, "unscratched"); +} + +void disorder_notify_queue_remove(const char attribute((unused)) *track, + const char attribute((unused)) *remover) { +} + +void disorder_notify_queue_move(const char attribute((unused)) *track, + const char attribute((unused)) *mover) { +} + +void disorder_notify_pause(const char attribute((unused)) *track, + const char attribute((unused)) *who) { +} + +void disorder_notify_resume(const char attribute((unused)) *track, + const char attribute((unused)) *who) { +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:09c6471644c1b075a86e1d79093b5542 */ diff --git a/plugins/shell.c b/plugins/shell.c new file mode 100644 index 0000000..0fd3333 --- /dev/null +++ b/plugins/shell.c @@ -0,0 +1,69 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2004, 2005 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <unistd.h> +#include <errno.h> +#include <stdlib.h> + +#include <disorder.h> + +const unsigned long disorder_player_type = DISORDER_PLAYER_STANDALONE; + +void disorder_play_track(const char *const *parameters, + int nparameters, + const char *path, + const char *track, + void attribute((unused)) *data) { + const char *vec[4]; + char *env_track, *env_path; + + vec[1] = "-c"; + vec[3] = 0; + switch(nparameters) { + case 0: + disorder_fatal(0, "missing argument to shell player module"); + case 1: + vec[0] = "sh"; + vec[2] = parameters[0]; + break; + case 2: + vec[0] = parameters[0]; + vec[2] = parameters[1]; + break; + default: + disorder_fatal(0, "extra arguments to shell player module"); + } + disorder_asprintf(&env_path, "TRACK=%s", path); + if(putenv(env_path) < 0) disorder_fatal(errno, "error calling putenv"); + disorder_asprintf(&env_track, "TRACK_UTF8=%s", track); + if(putenv(env_track) < 0) disorder_fatal(errno, "error calling putenv"); + execvp(vec[0], (char **)vec); + disorder_fatal(errno, "error executing %s", vec[0]); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +End: +*/ +/* arch-tag:5e4b82a463c04d36c6cb9142db00ed07 */ diff --git a/plugins/tracklength.c b/plugins/tracklength.c new file mode 100644 index 0000000..8d21ad8 --- /dev/null +++ b/plugins/tracklength.c @@ -0,0 +1,265 @@ +/* + * This file is part of DisOrder. + * Portions copyright (C) 2004, 2005 Richard Kettlewell (see also below) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include <config.h> + +#include <string.h> +#include <stdio.h> +#include <math.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <errno.h> + +#include <vorbis/vorbisfile.h> +#include <mad.h> + +#include <disorder.h> + +#include "madshim.h" + +static void *mmap_file(const char *path, size_t *lengthp) { + int fd; + void *base; + struct stat sb; + + if((fd = open(path, O_RDONLY)) < 0) { + disorder_error(errno, "error opening %s", path); + return 0; + } + if(fstat(fd, &sb) < 0) { + disorder_error(errno, "error calling stat on %s", path); + goto error; + } + if(sb.st_size == 0) /* can't map 0-length files */ + goto error; + if((base = mmap(0, sb.st_size, PROT_READ, + MAP_SHARED, fd, 0)) == (void *)-1) { + disorder_error(errno, "error calling mmap on %s", path); + goto error; + } + *lengthp = sb.st_size; + close(fd); + return base; +error: + close(fd); + return 0; +} + +static long tl_mp3(const char *path) { + size_t length; + void *base; + buffer b; + + if(!(base = mmap_file(path, &length))) return -1; + b.duration = mad_timer_zero; + scan_mp3(base, length, &b); + munmap(base, length); + return b.duration.seconds + !!b.duration.fraction; +} + +static long tl_ogg(const char *path) { + OggVorbis_File vf; + FILE *fp = 0; + double length; + + if(!path) goto error; + if(!(fp = fopen(path, "rb"))) goto error; + if(ov_open(fp, &vf, 0, 0)) goto error; + fp = 0; + length = ov_time_total(&vf, -1); + ov_clear(&vf); + return ceil(length); +error: + if(fp) fclose(fp); + return -1; +} + +static long tl_wav(const char *path) { + size_t length; + void *base; + long duration = -1; + unsigned char *ptr; + unsigned n, m, data_bytes = 0, samples_per_second = 0; + unsigned n_channels = 0, bits_per_sample = 0, sample_point_size; + unsigned sample_frame_size, n_samples; + + /* Sources: + * + * http://www.technology.niagarac.on.ca/courses/comp530/WavFileFormat.html + * http://www.borg.com/~jglatt/tech/wave.htm + * http://www.borg.com/~jglatt/tech/aboutiff.htm + * + * These files consists of a header followed by chunks. + * Multibyte values are little-endian. + * + * 12 byte file header: + * offset size meaning + * 00 4 'RIFF' + * 04 4 length of rest of file + * 08 4 'WAVE' + * + * The length includes 'WAVE' but excludes the 1st 8 bytes. + * + * Chunk header: + * 00 4 chunk ID + * 04 4 length of rest of chunk + * + * The stated length may be odd, if so then there is an implicit padding byte + * appended to the chunk to make it up to an even length (someone wasn't + * think about 32/64-bit worlds). + * + * Also some files seem to have extra stuff at the end of chunks that nobody + * I know of documents. Go figure, but check the length field rather than + * deducing the length from the ID. + * + * Format chunk: + * 00 4 'fmt' + * 04 4 length of rest of chunk + * 08 2 compression (1 = none) + * 0a 2 number of channels + * 0c 4 samples/second + * 10 4 average bytes/second, = (samples/sec) * (bytes/sample) + * 14 2 bytes/sample + * 16 2 bits/sample point + * + * 'sample' means 'sample frame' above, i.e. a sample point for each channel. + * + * Data chunk: + * 00 4 'data' + * 04 4 length of rest of chunk + * 08 ... data + * + * There is only allowed to be one data chunk. Some people violate this; we + * shall encourage people to fix their broken WAV files by not supporting + * this violation and because it's easier. + * + * As to the encoding of the data: + * + * Firstly, samples up to 8 bits in size are unsigned, larger samples are + * signed. Madness. + * + * Secondly sample points are stored rounded up to a multiple of 8 bits in + * size. Marginally saner. + * + * Written as a single word (of 8, 16, 24, whatever bits) the padding to + * implement this happens at the right hand (least significant) end. + * e.g. assuming a 9 bit sample: + * + * | padded sample word | + * | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 | + * | 8 7 6 5 4 3 2 1 0 - - - - - - - | + * + * But this is a little-endian file format so the least significant byte is + * the first, which means that the padding is "between" the bits if you + * imagine them in their usual order: + * + * | first byte | second byte | + * | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | + * | 0 - - - - - - - | 8 7 6 5 4 3 2 1 | + * + * Sample points are grouped into sample frames, consisting of as many + * samples points as their are channels. It seems that there are standard + * orderings of different channels. + * + * Given all of the above all we need to do is pick up some numbers from the + * format chunk, and the length of the data chunk, and do some arithmetic. + */ + if(!(base = mmap_file(path, &length))) return -1; +#define get16(p) ((p)[0] + 256 * (p)[1]) +#define get32(p) ((p)[0] + 256 * ((p)[1] + 256 * ((p)[2] + 256 * (p)[3]))) + ptr = base; + if(length < 12) goto out; + if(strncmp((char *)ptr, "RIFF", 4)) goto out; /* wrong type */ + n = get32(ptr + 4); /* file length */ + if(n > length - 8) goto out; /* truncated */ + ptr += 8; /* skip file header */ + if(n < 4 || strncmp((char *)ptr, "WAVE", 4)) goto out; /* wrong type */ + ptr += 4; /* skip 'WAVE' */ + n -= 4; + while(n >= 8) { + m = get32(ptr + 4); /* chunk length */ + if(m > n - 8) goto out; /* truncated */ + if(!strncmp((char *)ptr, "fmt ", 4)) { + if(samples_per_second) goto out; /* duplicate format chunk! */ + n_channels = get16(ptr + 0x0a); + samples_per_second = get32(ptr + 0x0c); + bits_per_sample = get16(ptr + 0x16); + if(!samples_per_second) goto out; /* bogus! */ + } else if(!strncmp((char *)ptr, "data", 4)) { + if(data_bytes) goto out; /* multiple data chunks! */ + data_bytes = m; /* remember data size */ + } + m += 8; /* include chunk header */ + ptr += m; /* skip chunk */ + n -= m; + } + sample_point_size = (bits_per_sample + 7) / 8; + sample_frame_size = sample_point_size * n_channels; + if(!sample_frame_size) goto out; /* bogus or overflow */ + n_samples = data_bytes / sample_frame_size; + duration = (n_samples + samples_per_second - 1) / samples_per_second; +out: + munmap(base, length); + return duration; +} + +static const struct { + const char *ext; + long (*fn)(const char *path); +} file_formats[] = { + { ".MP3", tl_mp3 }, + { ".OGG", tl_ogg }, + { ".WAV", tl_wav }, + { ".mp3", tl_mp3 }, + { ".ogg", tl_ogg }, + { ".wav", tl_wav } +}; +#define N_FILE_FORMATS (int)(sizeof file_formats / sizeof *file_formats) + +long disorder_tracklength(const char attribute((unused)) *track, + const char *path) { + const char *ext = strrchr(path, '.'); + int l, r, m = 0, c = 0; /* quieten compiler */ + + if(ext) { + l = 0; + r = N_FILE_FORMATS - 1; + while(l <= r && (c = strcmp(ext, file_formats[m = (l + r) / 2].ext))) + if(c < 0) + r = m - 1; + else + l = m + 1; + if(!c) + return file_formats[m].fn(path); + } + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +End: +*/ +/* arch-tag:fb794bf679375f55bf26fbb7a96a39de */ diff --git a/prepare b/prepare new file mode 100755 index 0000000..4387123 --- /dev/null +++ b/prepare @@ -0,0 +1,46 @@ +#! /bin/bash +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e +set -x +srcdir=$(dirname $0) +here=$(pwd) +cd $srcdir +rm -f COPYING +for f in /usr/share/common-licenses/GPL-2 $HOME/doc/GPL-2; do + if test -e "$f"; then + ln -s "$f" COPYING + break + fi +done +if test -d $HOME/share/aclocal; then + aclocal --acdir=$HOME/share/aclocal +else + aclocal +fi +libtoolize +autoconf +autoheader +automake -a || true # for INSTALL +automake --foreign -a +cd "$here" +$srcdir/configure "$@" --sysconfdir=/etc --localstatedir=/var +# arch-tag:100027fc986857bb24d779ecdcf41345 diff --git a/python/Makefile.am b/python/Makefile.am new file mode 100644 index 0000000..5f858f8 --- /dev/null +++ b/python/Makefile.am @@ -0,0 +1,33 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +bin_SCRIPTS=tkdisorder +nodist_python_PYTHON=disorder.py + +SEDFILES=disorder.py + +include ${top_srcdir}/scripts/sedfiles.make + +EXTRA_DIST=disorder.py.in tkdisorder + +BUILT_SOURCES=disorder.py + +CLEANFILES=$(SEDFILES) $(HTMLMAN) *.pyc +# arch-tag:6b3bfd0829ab35f1a30e7d8bf9111b95 diff --git a/python/disorder.py.in b/python/disorder.py.in new file mode 100644 index 0000000..dfaddbf --- /dev/null +++ b/python/disorder.py.in @@ -0,0 +1,1045 @@ +# +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +"""Python support for DisOrder + +Provides disorder.client, a class for accessing a DisOrder server. + +Example 1: + + #! /usr/bin/env python + import disorder + d = disorder.client() + p = d.playing() + if p: + print p['track'] + +Example 2: + + #! /usr/bin/env python + import disorder + import sys + d = disorder.client() + for path in sys.argv[1:]: + d.play(path) + +""" + +import re +import string +import os +import pwd +import socket +import binascii +import sha +import sys +import locale + +_configfile = "pkgconfdir/config" +_dbhome = "pkgstatedir" + +# various regexps we'll use +_ws = re.compile(r"^[ \t\n\r]+") +_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'") +_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"") +_unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*") + +_response = re.compile("([0-9]{3}) ?(.*)") + +version = "_version_" + +######################################################################## +# exception classes + +class Error(Exception): + """Base class for DisOrder exceptions.""" + +class _splitError(Error): + # _split failed + def __init__(self, value): + self.value = value + def __str__(self): + return str(self.value) + +class parseError(Error): + """Error parsing the configuration file.""" + def __init__(self, path, line, details): + self.path = path + self.line = line + self.details = details + def __str__(self): + return "%s:%d: %s" % (self.path, self.line, self.details) + +class protocolError(Error): + """DisOrder control protocol error. + + Indicates a mismatch between the client and server's understanding of + the control protocol. + """ + def __init__(self, who, error): + self.who = who + self.error = error + def __str__(self): + return "%s: %s" % (self.who, str(self.error)) + +class operationError(Error): + """DisOrder control protocol error response. + + Indicates that an operation failed (e.g. an attempt to play a + nonexistent track). The connection should still be usable. + """ + def __init__(self, res, details): + self.res_ = int(res) + self.details_ = details + def __str__(self): + """Return the complete response string from the server. + + Excludes the final newline. + """ + return "%d %s" % (self.res_, self.details_) + def response(self): + """Return the response code from the server.""" + return self.res_ + def details(self): + """Returns the detail string from the server.""" + return self.details_ + +class communicationError(Error): + """DisOrder control protocol communication error. + + Indicates that communication with the server went wrong, perhaps + because the server was restarted. The caller could report an error to + the user and wait for further user instructions, or even automatically + retry the operation. + """ + def __init__(self, who, error): + self.who = who + self.error = error + def __str__(self): + return "%s: %s" % (self.who, str(self.error)) + +######################################################################## +# DisOrder-specific text processing + +def _unescape(s): + # Unescape the contents of a string + # + # Arguments: + # + # s -- string to unescape + # + s = re.sub("\\\\n", "\n", s) + s = re.sub("\\\\(.)", "\\1", s) + return s + +def _split(s, *comments): + # Split a string into fields according to the usual Disorder string splitting + # conventions. + # + # Arguments: + # + # s -- string to parse + # comments -- if present, parse comments + # + # Return values: + # + # On success, a list of fields is returned. + # + # On error, disorder.parseError is thrown. + # + fields = [] + while s != "": + # discard comments + if comments and s[0] == '#': + break + # strip spaces + m = _ws.match(s) + if m: + s = s[m.end():] + continue + # pick of quoted fields of both kinds + m = _squote.match(s) + if not m: + m = _dquote.match(s) + if m: + fields.append(_unescape(m.group(1))) + s = s[m.end():] + continue + # and unquoted fields + m = _unquoted.match(s) + if m: + fields.append(m.group(0)) + s = s[m.end():] + continue + # anything left must be in error + if s[0] == '"' or s[0] == '\'': + raise _splitError("invalid quoted string") + else: + raise _splitError("syntax error") + return fields + +def _escape(s): + # Escape the contents of a string + # + # Arguments: + # + # s -- string to escape + # + if re.search("[\\\\\"'\n \t\r]", s) or s == '': + s = re.sub(r'[\\"]', r'\\\g<0>', s) + s = re.sub("\n", r"\\n", s) + return '"' + s + '"' + else: + return s + +def _quote(list): + # Quote a list of values + return ' '.join(map(_escape, list)) + +def _sanitize(s): + # Return the value of s in a form suitable for writing to stderr + return s.encode(locale.nl_langinfo(locale.CODESET), 'replace') + +def _list2dict(l): + # Convert a list of the form [k1, v1, k2, v2, ..., kN, vN] + # to a dictionary {k1:v1, k2:v2, ..., kN:vN} + d = {} + i = iter(l) + try: + while True: + k = i.next() + v = i.next() + d[k] = v + except StopIteration: + pass + return d + +def _queueEntry(s): + # parse a queue entry + return _list2dict(_split(s)) + +######################################################################## +# The client class + +class client: + """DisOrder client class. + + This class provides access to the DisOrder server either on this + machine or across the internet. + + The server to connect to, and the username and password to use, are + determined from the configuration files as described in 'man + disorder_config'. + + All methods will connect if necessary, as soon as you have a + disorder.client object you can start calling operational methods on + it. + + However if the server is restarted then the next method called on a + connection will throw an exception. This may be considered a bug. + + All methods block until they complete. + + Operation methods raise communicationError if the connection breaks, + protocolError if the response from the server is malformed, or + operationError if the response is valid but indicates that the + operation failed. + """ + + debug_proto = 0x0001 + debug_body = 0x0002 + + def __init__(self): + """Constructor for DisOrder client class. + + The constructor reads the configuration file, but does not connect + to the server. + + If the environment variable DISORDER_PYTHON_DEBUG is set then the + debug flags are initialised to that value. This can be overridden + with the debug() method below. + + The constructor Raises parseError() if the configuration file is not + valid. + """ + pw = pwd.getpwuid(os.getuid()) + self.debugging = int(os.getenv("DISORDER_PYTHON_DEBUG", 0)) + self.config = { 'collections': [], + 'username': pw.pw_name, + 'home': _dbhome } + home = os.getenv("HOME") + if not home: + home = pw.pw_dir + privconf = _configfile + "." + pw.pw_name + passfile = home + os.sep + ".disorder" + os.sep + "passwd" + self._readfile(_configfile) + if os.path.exists(privconf): + self._readfile(privconf) + if os.path.exists(passfile): + self._readfile(passfile) + self.state = 'disconnected' + + def debug(self, bits): + """Enable or disable protocol debugging. Debug messages are written + to sys.stderr. + + Arguments: + bits -- bitmap of operations that should generate debug information + + Bitmap values: + debug_proto -- dump control protocol messages (excluding bodies) + debug_body -- dump control protocol message bodies + """ + self.debugging = bits + + def _debug(self, bit, s): + # debug output + if self.debugging & bit: + sys.stderr.write(_sanitize(s)) + sys.stderr.write("\n") + sys.stderr.flush() + + def connect(self): + """Connect to the DisOrder server and authenticate. + + Raises communicationError if connection fails and operationError if + authentication fails (in which case disconnection is automatic). + + May be called more than once to retry connections (e.g. when the + server is down). If we are already connected and authenticated, + this is a no-op. + + Other operations automatically connect if we're not already + connected, so it is not strictly necessary to call this method. + """ + if self.state == 'disconnected': + try: + self.state = 'connecting' + if 'connect' in self.config and len(self.config['connect']) > 0: + c = self.config['connect'] + self.who = repr(c) # temporarily + if len(c) == 1: + a = socket.getaddrinfo(None, c[0], + socket.AF_INET, + socket.SOCK_STREAM, + 0, + 0) + else: + a = socket.getaddrinfo(c[0], c[1], + socket.AF_INET, + socket.SOCK_STREAM, + 0, + 0) + a = a[0] + s = socket.socket(a[0], a[1], a[2]); + s.connect(a[4]) + self.who = "%s" % a[3] + else: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); + self.who = self.config['home'] + os.sep + "socket" + s.connect(self.who) + self.w = s.makefile("wb") + self.r = s.makefile("rb") + (res, challenge) = self._simple() + h = sha.sha() + h.update(self.config['password']) + h.update(binascii.unhexlify(challenge)) + self._simple("user", self.config['username'], h.hexdigest()) + self.state = 'connected' + except socket.error, e: + self._disconnect() + raise communicationError(self.who, e) + except: + self._disconnect() + raise + + def _disconnect(self): + # disconnect from the server, whatever state we are in + try: + del self.w + del self.r + except: + pass + self.state = 'disconnected' + + ######################################################################## + # Operations + + def become(self, who): + """Become another user. + + Arguments: + who -- the user to become. + + Only trusted users can perform this operation. + """ + self._simple("become", who) + + def play(self, track): + """Play a track. + + Arguments: + track -- the path of the track to play. + """ + self._simple("play", track) + + def remove(self, track): + """Remove a track from the queue. + + Arguments: + track -- the path or ID of the track to remove. + """ + self._simple("remove", track) + + def enable(self): + """Enable playing.""" + self._simple("enable") + + def disable(self, *now): + """Disable playing. + + Arguments: + now -- if present (with any value), the current track is stopped + too. + """ + if now: + self._simple("disable", "now") + else: + self._simple("disable") + + def scratch(self, *id): + """Scratch the currently playing track. + + Arguments: + id -- if present, the ID of the track to scratch. + """ + if id: + self._simple("scratch", id[0]) + else: + self._simple("scratch") + + def shutdown(self): + """Shut down the server. + + Only trusted users can perform this operation. + """ + self._simple("shutdown") + + def reconfigure(self): + """Make the server reload its configuration. + + Only trusted users can perform this operation. + """ + self._simple("reconfigure") + + def rescan(self, pattern): + """Rescan one or more collections. + + Arguments: + pattern -- glob pattern matching collections to rescan. + + Only trusted users can perform this operation. + """ + self._simple("rescan", pattern) + + def version(self): + """Return the server's version number.""" + return self._simple("version")[1] + + def playing(self): + """Return the currently playing track. + + If a track is playing then it is returned as a dictionary. + If no track is playing then None is returned.""" + res, details = self._simple("playing") + if res % 10 != 9: + try: + return _queueEntry(details) + except _splitError, s: + raise protocolError(self.who, s.str()) + else: + return None + + def _somequeue(self, command): + self._simple(command) + try: + return map(lambda s: _queueEntry(s), self._body()) + except _splitError, s: + raise protocolError(self.who, s.str()) + + def recent(self): + """Return a list of recently played tracks. + + The return value is a list of dictionaries corresponding to + recently played tracks. The oldest track comes first.""" + return self._somequeue("recent") + + def queue(self): + """Return the current queue. + + The return value is a list of dictionaries corresponding to + recently played tracks. The next track to be played comes first.""" + return self._somequeue("queue") + + def _somedir(self, command, dir, re): + if re: + self._simple(command, dir, re[0]) + else: + self._simple(command, dir) + return self._body() + + def directories(self, dir, *re): + """List subdirectories of a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of the (nonempty) subdirectories of dir. + If dir is '' then a list of top-level directories is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("dirs", dir, re) + + def files(self, dir, *re): + """List files within a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of playable files in dir. If dir is '' + then a list of top-level files is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("files", dir, re) + + def allfiles(self, dir, *re): + """List subdirectories and files within a directory. + + Arguments: + dir -- directory to list, or '' for the whole root. + re -- regexp that results must match. Optional. + + The return value is a list of all (nonempty) subdirectories and + files within dir. If dir is '' then a list of top-level files and + directories is returned. + + If a regexp is specified then the basename of each result must + match. Matching is case-independent. See pcrepattern(3). + """ + return self._somedir("allfiles", dir, re) + + def set(self, track, key, value): + """Set a preference value. + + Arguments: + track -- the track to modify + key -- the preference name + value -- the new preference value + """ + self._simple("set", track, key, value) + + def unset(self, track, key): + """Unset a preference value. + + Arguments: + track -- the track to modify + key -- the preference to remove + """ + self._simple("set", track, key, value) + + def get(self, track, key): + """Get a preference value. + + Arguments: + track -- the track to query + key -- the preference to remove + + The return value is the preference + """ + ret, details = self._simple("get", track, key) + return details + + def prefs(self, track): + """Get all the preferences for a track. + + Arguments: + track -- the track to query + + The return value is a dictionary of all the track's preferences. + Note that even nominally numeric values remain encoded as strings. + """ + self._simple("prefs", track) + r = {} + for line in self._body(): + try: + kv = _split(line) + except _splitError, s: + raise protocolError(self.who, s.str()) + if len(kv) != 2: + raise protocolError(self.who, "invalid prefs body line") + r[kv[0]] = kv[1] + return r + + def _boolean(self, s): + return s[1] == 'yes' + + def exists(self, track): + """Return true if a track exists + + Arguments: + track -- the track to check for""" + return self._boolean(self._simple("exists", track)) + + def enabled(self): + """Return true if playing is enabled""" + return self._boolean(self._simple("enabled")) + + def random_enabled(self): + """Return true if random play is enabled""" + return self._boolean(self._simple("random-enabled")) + + def random_enable(self): + """Enable random play.""" + self._simple("random-enable") + + def random_disable(self): + """Disable random play.""" + self._simple("random-disable") + + def length(self, track): + """Return the length of a track in seconds. + + Arguments: + track -- the track to query. + """ + ret, details = self._simple("length", track) + return int(details) + + def search(self, words): + """Search for tracks. + + Arguments: + words -- the set of words to search for. + + The return value is a list of track path names, all of which contain + all of the required words (in their path name, trackname + preferences, etc.) + """ + self._simple("search", *words) + return self._body() + + def stats(self): + """Get server statistics. + + The return value is list of statistics. + """ + self._simple("stats") + return self._body() + + def dump(self): + """Get all preferences. + + The return value is an encoded dump of the preferences database. + """ + self._simple("dump") + return self._body() + + def set_volume(self, left, right): + """Set volume. + + Arguments: + left -- volume for the left speaker. + right -- volume for the right speaker. + """ + self._simple("volume", left, right) + + def get_volume(self): + """Get volume. + + The return value a tuple consisting of the left and right volumes. + """ + ret, details = self._simple("volume") + return map(int,string.split(details)) + + def move(self, track, delta): + """Move a track in the queue. + + Arguments: + track -- the name or ID of the track to move + delta -- the number of steps towards the head of the queue to move + """ + ret, details = self._simple("move", track, str(delta)) + return int(details) + + def log(self, callback): + """Read event log entries as they happen. + + Each event log entry is handled by passing it to callback. + + The callback takes two arguments, the first is the client and the + second the line from the event log. + + The callback should return True to continue or False to stop (don't + forget this, or your program will mysteriously misbehave). + + It is suggested that you use the disorder.monitor class instead of + calling this method directly, but this is not mandatory. + + See disorder_protocol(5) for the event log syntax. + + Arguments: + callback -- function to call with log entry + """ + ret, details = self._simple("log") + while True: + l = self._line() + self._debug(client.debug_body, "<<< %s" % l) + if l != '' and l[0] == '.': + if l == '.': + return + l = l[1:] + if not callback(self, l): + break + # tell the server to stop sending, eat the remains of the body, + # eat the response + self._send("version") + self._body() + self._response() + + def pause(self): + """Pause the current track.""" + self._simple("pause") + + def resume(self): + """Resume after a pause.""" + self._simple("resume") + + def part(self, track, context, part): + """Get a track name part + + Arguments: + track -- the track to query + context -- the context ('sort' or 'display') + part -- the desired part (usually 'artist', 'album' or 'title') + + The return value is the preference + """ + ret, details = self._simple("part", track, context, part) + return details + + ######################################################################## + # I/O infrastructure + + def _line(self): + # read one response line and return as some suitable string object + # + # If an I/O error occurs, disconnect from the server. + # + # XXX does readline() DTRT regarding character encodings? + try: + l = self.r.readline() + if not re.search("\n", l): + raise communicationError(self.who, "peer disconnected") + l = l[:-1] + except: + self._disconnect() + raise + return unicode(l, "UTF-8") + + def _response(self): + # read a response as a (code, details) tuple + l = self._line() + self._debug(client.debug_proto, "<== %s" % l) + m = _response.match(l) + if m: + return int(m.group(1)), m.group(2) + else: + raise protocolError(self.who, "invalid response %s") + + def _send(self, *command): + quoted = _quote(command) + self._debug(client.debug_proto, "==> %s" % quoted) + encoded = quoted.encode("UTF-8") + try: + self.w.write(encoded) + self.w.write("\n") + self.w.flush() + except IOError, e: + # e.g. EPIPE + self._disconnect() + raise communicationError(self.who, e) + except: + self._disconnect() + raise + + def _simple(self, *command): + # Issue a simple command, throw an exception on error + # + # If an I/O error occurs, disconnect from the server. + # + # On success returns response as a (code, details) tuple + # + # On error raise operationError + if self.state == 'disconnected': + self.connect() + if command: + self._send(*command) + res, details = self._response() + if res / 100 == 2: + return res, details + raise operationError(res, details) + + def _body(self): + # Fetch a dot-stuffed body + result = [] + while True: + l = self._line() + self._debug(client.debug_body, "<<< %s" % l) + if l != '' and l[0] == '.': + if l == '.': + return result + l = l[1:] + result.append(l) + + ######################################################################## + # Configuration file parsing + + def _readfile(self, path): + # Read a configuration file + # + # Arguments: + # + # path -- path of file to read + + # handlers for various commands + def _collection(self, command, args): + if len(args) != 3: + return "'%s' takes three args" % command + self.config["collections"].append(args) + + def _unary(self, command, args): + if len(args) != 1: + return "'%s' takes only one arg" % command + self.config[command] = args[0] + + def _include(self, command, args): + if len(args) != 1: + return "'%s' takes only one arg" % command + self._readfile(args[0]) + + def _any(self, command, args): + self.config[command] = args + + # mapping of options to handlers + _options = { "collection": _collection, + "username": _unary, + "password": _unary, + "home": _unary, + "connect": _any, + "include": _include } + + # the parser + for lno, line in enumerate(file(path, "r")): + try: + fields = _split(line, 'comments') + except _splitError, s: + raise parseError(path, lno + 1, str(s)) + if fields: + command = fields[0] + # we just ignore options we don't know about, so as to cope gracefully + # with version skew (and nothing to do with implementor laziness) + if command in _options: + e = _options[command](self, command, fields[1:]) + if e: + self._parseError(path, lno + 1, e) + + def _parseError(self, path, lno, s): + raise parseError(path, lno, s) + +######################################################################## +# monitor class + +class monitor: + """DisOrder event log monitor class + + Intended to be subclassed with methods corresponding to event log messages + the implementor cares about over-ridden.""" + + def __init__(self, c=None): + """Constructor for the monitor class + + Can be passed a client to use. If none is specified then one + will be created specially for the purpose. + + Arguments: + c -- client""" + if c == None: + c = client(); + self.c = c + + def run(self): + """Start monitoring logs. Continues monitoring until one of the + message-specific methods returns False. Can be called more than once + (but not recursively!)""" + self.c.log(self._callback) + + def when(self): + """Return the timestamp of the current (or most recent) event log entry""" + return self.timestamp + + def _callback(self, c, line): + try: + bits = _split(line) + except: + return self.invalid(line) + if(len(bits) < 2): + return self.invalid(line) + self.timestamp = int(bits[0], 16) + keyword = bits[1] + bits = bits[2:] + if keyword == 'completed': + if len(bits) == 1: + return self.completed(bits[0]) + elif keyword == 'failed': + if len(bits) == 2: + return self.failed(bits[0], bits[1]) + elif keyword == 'moved': + if len(bits) == 3: + try: + n = int(bits[1]) + except: + return self.invalid(line) + return self.moved(bits[0], n, bits[2]) + elif keyword == 'playing': + if len(bits) == 1: + return self.playing(bits[0], None) + elif len(bits) == 2: + return self.playing(bits[0], bits[1]) + elif keyword == 'queue' or keyword == 'recent-added': + try: + q = _list2dict(bits) + except: + return self.invalid(line) + if keyword == 'queue': + return self.queue(q) + if keyword == 'recent-added': + return self.recent_added(q) + elif keyword == 'recent-removed': + if len(bits) == 1: + return self.recent_removed(bits[0]) + elif keyword == 'removed': + if len(bits) == 1: + return self.removed(bits[0], None) + elif len(bits) == 2: + return self.removed(bits[0], bits[1]) + elif keyword == 'scratched': + if len(bits) == 2: + return self.scratched(bits[0], bits[1]) + return self.invalid(line) + + def completed(self, track): + """Called when a track completes. + + Arguments: + track -- track that completed""" + return True + + def failed(self, track, error): + """Called when a player suffers an error. + + Arguments: + track -- track that failed + error -- error indicator""" + return True + + def moved(self, id, offset, user): + """Called when a track is moved in the queue. + + Arguments: + id -- queue entry ID + offset -- distance moved + user -- user responsible""" + return True + + def playing(self, track, user): + """Called when a track starts playing. + + Arguments: + track -- track that has started + user -- user that submitted track, or None""" + return True + + def queue(self, q): + """Called when a track is added to the queue. + + Arguments: + q -- dictionary of new queue entry""" + return True + + def recent_added(self, q): + """Called when a track is added to the recently played list + + Arguments: + q -- dictionary of new queue entry""" + return True + + def recent_removed(self, id): + """Called when a track is removed from the recently played list + + Arguments: + id -- ID of removed entry (always the oldest)""" + return True + + def removed(self, id, user): + """Called when a track is removed from the queue, either manually + or in order to play it. + + Arguments: + id -- ID of removed entry + user -- user responsible (or None if we're playing this track)""" + return True + + def scratched(self, track, user): + """Called when a track is scratched + + Arguments: + track -- track that was scratched + user -- user responsible""" + return True + + def invalid(self, line): + """Called when an event log line cannot be interpreted + + Arguments: + line -- line that could not be understood""" + return True + +# Local Variables: +# mode:python +# py-indent-offset:2 +# comment-column:40 +# fill-column:72 +# End: +# arch-tag:eea975737c837febf73a000630b5ecc4 diff --git a/python/tkdisorder b/python/tkdisorder new file mode 100755 index 0000000..243b15e --- /dev/null +++ b/python/tkdisorder @@ -0,0 +1,475 @@ +#! /usr/bin/env python +# +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +"""Graphical user interface for DisOrder""" + +from Tkinter import * +import tkFont +import Queue +import threading +import disorder +import time +import string +import re +import getopt +import sys + +######################################################################## + +# Architecture: +# +# The main (initial) thread of the program runs all GUI code. The GUI is only +# directly modified from inside this thread. We sometimes call this the +# master thread. +# +# We have a background thread, MonitorStateThread, which waits for changes to +# the server's state which we care about. Whenever such a change occurs it +# notifies all the widgets which care about it (and possibly other widgets; +# the current implementation is unsophisticated.) +# +# Widget poll() methods usually, but NOT ALWAYS, called in the +# MonitorStateThread. Other widget methods are call in the master thread. +# +# We have a separate disorder.client for each thread rather than locking a +# single client. MonitorStateThread also has a private disorder.client that +# it uses to watch for server state changes. + +######################################################################## + +class Intercom: + # communication queue into thread containing Tk event loop + # + # Sets up a callback on the event loop (in this thread) which periodically + # checks the queue for elements; if any are found they are executed. + def __init__(self, master): + self.q = Queue.Queue(); + self.master = master + self.poll() + + def poll(self): + try: + item = self.q.get_nowait() + item() + self.master.after_idle(self.poll) + except Queue.Empty: + self.master.after(100, self.poll) + + def put(self, item): + self.q.put(item) + +######################################################################## + +class ProgressBar(Canvas): + # progress bar widget + def __init__(self, master=None, **kw): + Canvas.__init__(self, master, highlightthickness=0, **kw) + self.outer = self.create_rectangle(0, 0, 0, 0, + outline="#000000", + width=1, + fill="#ffffff") + self.bar = self.create_rectangle(0, 0, 0, 0, + width=1, + fill="#ff0000", + outline='#ff0000') + self.current = None + self.total = None + self.bind("<Configure>", lambda e: self.redisplay()) + + def update(self, current, total): + self.current = current + if current > total: + current = total + elif current < 0: + current = 0 + self.total = total + self.redisplay() + + def clear(self): + self.current = None + self.total = None + self.redisplay() + + def redisplay(self): + w, h = self.winfo_width(), self.winfo_height() + if w > 0 and h > 0: + self.coords(self.outer, 0, 0, w - 1, h - 1) + if self.total: + bw = int((w - 2) * self.current / self.total) + self.itemconfig(self.bar, + fill="#ff0000", + outline="#ff0000") + self.coords(self.bar, 1, 1, bw, h - 2) + else: + self.itemconfig(self.bar, + fill="#909090", + outline="#909090") + self.coords(self.bar, 1, 1, w - 2, h - 2) + +# look up a track's name part, using client c. Maintains a cache. +part_cache = {} +def part(c, track, context, part): + key = "%s-%s-%s" % (part, context, track) + now = time.time() + if not part_cache.has_key(key) or part_cache[key]['when'] < now - 3600: + part_cache[key] = {'when': now, + 'what': c.part(track, context, part)} + return part_cache[key]['what'] + +class PlayingWidget(Frame): + # widget that always displays information about what's + # playing + def __init__(self, master=None, **kw): + Frame.__init__(self, master, **kw) + # column 0 is descriptions, column 1 is the values + self.columnconfigure(0,weight=0) + self.columnconfigure(1,weight=1) + self.fields = {} + self.field(0, 0, "artist", "Artist") + self.field(1, 0, "album", "Album") + self.field(2, 0, "title", "Title") + # column 1 also has the progress bar in it + self.p = ProgressBar(self, height=20) + self.p.grid(row=3, column=1, sticky=E+W) + # column 2 has operation buttons + b = Button(self, text="Quit", command=self.quit) + b.grid(row=0, column=2, sticky=E+W) + b = Button(self, text="Scratch", command=self.scratch) + b.grid(row=1, column=2, sticky=E+W) + b = Button(self, text="Recent", command=self.recent) + b.grid(row=2, column=2, sticky=E+W) + self.length = 0 + self.update_length() + self.last = None + self.recentw = None + + def field(self, row, column, name, label): + # create a field + Label(self, text=label).grid(row=row, column=column, sticky=E) + self.fields[name] = Text(self, height=1, state=DISABLED) + self.fields[name].grid(row=row, column=column + 1, sticky=W+E); + + def set(self, name, value): + # set a field's value + f = self.fields[name] + f.config(state=NORMAL) + f.delete(1.0, END) + f.insert(END, value) + f.config(state=DISABLED) + + def playing(self, p): + # called with new what's-playing information + values = {} + if p: + for tpart in ['artist', 'album', 'title']: + values[tpart] = part(client, p['track'], 'display', tpart) + try: + self.length = client.length(p['track']) + except disorder.operationError: + self.length = 0 + self.started = int(p['played']) + else: + self.length = 0 + for k in self.fields.keys(): + if k in values: + self.set(k, values[k]) + else: + self.set(k, "") + self.length_bar() + + def length_bar(self): + if self.length and self.length > 0: + self.p.update(time.time() - self.started, self.length) + else: + self.p.clear() + + def update_length(self): + self.length_bar() + self.after(1000, self.update_length) + + def poll(self, c): + p = c.playing() + if p != self.last: + intercom.put(lambda: self.playing(p)) + self.last = p + + def quit(self): + sys.exit(0) + + def scratch(self): + client.scratch() + + def recent_close(self): + self.recentw.destroy() + self.recentw = None + + def recent(self): + if self.recentw: + self.recentw.deiconify() + self.recentw.lift() + else: + w = 80*tracklistFont.measure('A') + h = 40*tracklistFont.metrics("linespace") + self.recentw = Toplevel() + self.recentw.protocol("WM_DELETE_WINDOW", self.recent_close) + self.recentw.title("Recently Played") + # XXX for some reason Toplevel(width=w,height=h) doesn't seem to work + self.recentw.geometry("%dx%d" % (w,h)) + w = RecentWidget(self.recentw) + w.pack(fill=BOTH, expand=1) + mst.add(w); + +class TrackListWidget(Frame): + def __init__(self, master=None, **kw): + Frame.__init__(self, master, **kw) + self.yscrollbar = Scrollbar(self) + self.xscrollbar = Scrollbar(self, orient=HORIZONTAL) + self.canvas = Canvas(self, + xscrollcommand=self.xscrollbar.set, + yscrollcommand=self.yscrollbar.set) + self.xscrollbar.config(command=self.canvas.xview) + self.yscrollbar.config(command=self.canvas.yview) + self.canvas.grid(row=0, column=0, sticky=N+S+E+W) + self.yscrollbar.grid(row=0, column=1, sticky=N+S) + self.xscrollbar.grid(row=1, column=0, sticky=E+W) + self.columnconfigure(0,weight=1) + self.rowconfigure(0,weight=1) + self.last = None + self.default_cursor = self['cursor'] + self.configure(cursor="watch") + + def queue(self, q, w_artists, w_albums, w_titles, artists, albums, titles): + # called with new queue state + # delete old contents + try: + for i in self.canvas.find_all(): + self.canvas.delete(i) + except TclError: + # if the call was queued but not received before the window was deleted + # we might get an error from Tcl/Tk, which no longer knows the window, + # here + return + w = tracklistHFont.measure("Artist") + if w > w_artists: + w_artists = w + w = tracklistHFont.measure("Album") + if w > w_albums: + w_albums = w + w = tracklistHFont.measure("Title") + if w > w_titles: + w_titles = w + hheading = tracklistHFont.metrics("linespace") + h = tracklistFont.metrics('linespace') + x_artist = 8 + x_album = x_artist + w_artists + 16 + x_title = x_album + w_albums + 16 + w = x_title + w_titles + 8 + self.canvas['scrollregion'] = (0, 0, w, h * len(artists) + hheading) + self.canvas.create_text(x_artist, 0, text="Artist", + font=tracklistHFont, + anchor='nw') + self.canvas.create_text(x_album, 0, text="Album", + font=tracklistHFont, + anchor='nw') + self.canvas.create_text(x_title, 0, text="Title", + font=tracklistHFont, + anchor='nw') + y = hheading + for n in range(0,len(artists)): + artist = artists[n] + album = albums[n] + title = titles[n] + if artist != "": + self.canvas.create_text(x_artist, y, text=artist, + font=tracklistFont, + anchor='nw') + if album != "": + self.canvas.create_text(x_album, y, text=album, + font=tracklistFont, + anchor='nw') + if title != "": + self.canvas.create_text(x_title, y, text=title, + font=tracklistFont, + anchor='nw') + y += h + self.last = q + self.configure(cursor=self.default_cursor) + + def poll(self, c): + q = self.getqueue(c) + if q != self.last: + # we do the track name calculation in the background thread so that + # the gui can still be responsive + artists = [] + albums = [] + titles = [] + w_artists = w_albums = w_titles = 16 + for t in q: + artist = part(c, t['track'], 'display', 'artist') + album = part(c, t['track'], 'display', 'album') + title = part(c, t['track'], 'display', 'title') + w = tracklistFont.measure(artist) + if w > w_artists: + w_artists = w + w = tracklistFont.measure(album) + if w > w_albums: + w_albums = w + w = tracklistFont.measure(title) + if w > w_titles: + w_titles = w + artists.append(artist) + albums.append(album) + titles.append(title) + intercom.put(lambda: self.queue(q, w_artists, w_albums, w_titles, + artists, albums, titles)) + self.last = q + +class QueueWidget(TrackListWidget): + def __init__(self, master=None, **kw): + TrackListWidget.__init__(self, master, **kw) + + def getqueue(self, c): + return c.queue() + +class RecentWidget(TrackListWidget): + def __init__(self, master=None, **kw): + TrackListWidget.__init__(self, master, **kw) + + def getqueue(self, c): + l = c.recent() + l.reverse() + return l + +class MonitorStateThread: + # thread to pick up current server state and publish it + # + # Creates a client and monitors it in a daemon thread for state changes. + # Whenever one occurs, call w.poll(c) for every member w of widgets with + # a client owned by the thread in which the call occurs. + def __init__(self, widgets, masterclient=None): + self.logclient = disorder.client() + self.client = disorder.client() + self.clientlock = threading.Lock() + if not masterclient: + masterclient = disorder.client() + self.masterclient = masterclient + self.widgets = widgets + self.lock = threading.Lock() + # the main thread + self.thread = threading.Thread(target=self.run) + self.thread.setDaemon(True) + self.thread.start() + # spare thread for processing additions + self.adderq = Queue.Queue() + self.adder = threading.Thread(target=self.runadder) + self.adder.setDaemon(True) + self.adder.start() + + def notify(self, line): + self.lock.acquire() + widgets = self.widgets + self.lock.release() + for w in widgets: + self.clientlock.acquire() + w.poll(self.client) + self.clientlock.release() + return 1 + + def add(self, w): + self.lock.acquire() + self.widgets.append(w) + self.lock.release() + self.adderq.put(lambda client: w.poll(client)) + + def remove(self, what): + self.lock.acquire() + self.widgets.remove(what) + self.lock.release() + + def run(self): + self.notify("") + self.logclient.log(lambda client, line: self.notify(line)) + + def runadder(self): + while True: + item = self.adderq.get() + self.clientlock.acquire() + item(self.client) + self.clientlock.release() + +######################################################################## + +def usage(s): + # display usage on S + s.write( + """Usage: + + tkdisorder [OPTIONS] + +Options: + + -h, --help Display this message + -V, --version Display version number + +tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell. +""") + +######################################################################## + +try: + opts, rest = getopt.getopt(sys.argv[1:], "Vh", ["version", "help"]) +except getopt.GetoptError, e: + sys.stderr.write("ERROR: %s, try --help for help\n" % e.msg) + sys.exit(1) +for o, v in opts: + if o in ('-V', '--version'): + print "%s" % disorder.version + sys.stdout.close() + sys.exit(0) + if o in ('h', '--help'): + usage(sys.stdout) + sys.stdout.close() + sys.exit(0) + +client = disorder.client() # master thread's client + +root = Tk() +root.title("DisOrder") + +tracklistFont = tkFont.Font(family='Helvetica', size=10) +tracklistHFont = tracklistFont.copy() +tracklistHFont.config(weight="bold") + +p = PlayingWidget(root) +p.pack(fill=BOTH, expand=1) + +q = QueueWidget(root) +q.pack(fill=BOTH, expand=1) + +intercom = Intercom(root) # only need a single intercom +mst = MonitorStateThread([p, q], client) + +root.mainloop() + +# Local Variables: +# py-indent-offset:2 +# comment-column:40 +# fill-column:79 +# End: +# arch-tag:d2c64241856ce5b19824e848f26c674b diff --git a/scripts/Makefile.am b/scripts/Makefile.am new file mode 100644 index 0000000..84ad902 --- /dev/null +++ b/scripts/Makefile.am @@ -0,0 +1,24 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2004, 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +dist_pkgdata_DATA=completion.bash + +EXTRA_DIST=htmlman sedfiles.make text2c oggrename +# arch-tag:49137b3a04a5bc92cd8c17d4372db87b diff --git a/scripts/check b/scripts/check new file mode 100755 index 0000000..b105277 --- /dev/null +++ b/scripts/check @@ -0,0 +1,78 @@ +#! /usr/bin/perl -w +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +use strict; + +my $year; +my %checked = (); +my %removed = (); +my $prefix = -e ".src" ? ".src/" : ""; +my $what = ""; +my $original; +my @exceptions; +my %exceptions; +my %missing = (); + +open(F, "<${prefix}scripts/copyright.exceptions") or die "$0: scripts/copyright.exceptions: $!"; +chomp(@exceptions = <F>); +%exceptions = map(($_, 1), grep !/^\#/, @exceptions); + +opendir(D, "${prefix}ChangeLog.d") or die "$0: ChangeLog.d: $!\n"; +for my $dir (readdir D) { + next if $dir =~ /cvs/; + open(C, "<${prefix}ChangeLog.d/$dir") or die "$0: ChangeLog.d/$dir: $!\n"; + while(defined($_ = <C>)) { + if(/^(\d{4})-\d{2}-\d{2}/) { + $year = $1; + } + if(/^\s+(modified|removed|renamed) files:$/) { + $what = $1; + next; + } + if(/^\s*$/) { + $what = ""; + } + if($what eq 'modified') { + my @files = split(/\s+/, $_); + for my $file (@files) { + next if exists $checked{"$file-$year"}; + next if $file =~ /^ChangeLog\.d/; + next if ! -e "$prefix$file"; + open(INPUT, "<$prefix$file") or die "$0: $prefix$file: $!\n"; + my $good = 0; + while(defined(my $line = <INPUT>)) { + if($line =~ /Copyright.*$year/i) { + $good = 1; + last; + } + } + close INPUT; + $checked{"$file-$year"} = $good; + $missing{"$file: missing $year"} = 1 + if !$good && !exists $exceptions{$file}; + } + } + } +} + +print map("$_\n", sort keys %missing); + +# arch-tag:PRdTcEnpHD8PVsb6Bp+QTQ diff --git a/scripts/completion.bash b/scripts/completion.bash new file mode 100644 index 0000000..cf237c3 --- /dev/null +++ b/scripts/completion.bash @@ -0,0 +1,63 @@ +# +# This file is part of DisOrder. +# Copyright (C) 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +complete -r disorder 2>/dev/null || true +complete -r disorderd 2>/dev/null || true +complete -r disorder-dump 2>/dev/null || true +complete -r disobedience 2>/dev/null || true + +complete -o default \ + -A file \ + -W "allfiles authorize become dirs disable disable-random + enable enable-random files get get-volume length log move + play playing prefs quack queue random-disable + random-enable recent reconfigure remove rescan scratch + search set set-volume shutdown stats unset version resolve + part pause resume scratch-id get-global set-global unset-global + tags + -h --help -H --help-commands --version -V --config -c + --length --debug -d" \ + disorder + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c + --debug --dump -d --undump -u --recover -r --recompute-aliases + -a" \ + disorder-dump + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c --debug -d --foreground -f + --pidfile -P" \ + disorderd + +complete -o default \ + -A file \ + -W "-h --help --version -V --config -c --debug -d --tufnel -t + --gtk-module --g-fatal-warnings --gtk-debug --gtk-no-debug + --class --name --gdk-debug --gdk-no-debug + --display --screen --sync --gxid-host --gxid-port" \ + disobedience + +# Local Variables: +# mode:sh +# End: +# arch-tag:2XNe1BR5K6TxF+njsXAByw diff --git a/scripts/copyright.exceptions b/scripts/copyright.exceptions new file mode 100644 index 0000000..4d64d50 --- /dev/null +++ b/scripts/copyright.exceptions @@ -0,0 +1,25 @@ +BUGS +CHANGES +README.raw +README.upgrades +README.client +debian/README.Debian +debian/changelog +debian/changelog +debian/conffiles +debian/control +debian/disorder.config +debian/options.debian +debian/templates +doc/checklist.txt +examples/config.sample.in +images/edit.png +sounds/slap.ogg +templates/options +templates/options.labels +templates/options.labels +{arch}/=tagging-method +{arch}/=tagging-method +disobedience/TODO +scripts/copyright.exceptions +# arch-tag:ez2jSX+4DAfwt7/ucp/JYQ diff --git a/scripts/dist b/scripts/dist new file mode 100755 index 0000000..5583849 --- /dev/null +++ b/scripts/dist @@ -0,0 +1,38 @@ +#! /bin/bash +# +# This file is part of DisOrder +# Copyright (C) 2005, 2006 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e +[ -d =build ] && cd =build +make +make check +make dist-bzip2 +d=$(make echo-distdir) +cp $d.tar.bz2 $HOME/work/web/disorder +cp .src/CHANGES $HOME/work/web/disorder/CHANGES.txt +cp .src/README $HOME/work/web/disorder/README.txt +cp .src/ChangeLog.d/*--* $HOME/work/web/disorder/ChangeLog.d +cd doc +for f in *.[1-9].html; do + echo $f + rm -f $HOME/work/web/disorder/$f + sed < $f > $HOME/work/web/disorder/$f 's/^@.*//' +done +# arch-tag:jpK3z3qFK+Sv/8QVvpMUIg diff --git a/scripts/htmlman b/scripts/htmlman new file mode 100755 index 0000000..54b1402 --- /dev/null +++ b/scripts/htmlman @@ -0,0 +1,52 @@ +#! /bin/sh +# +# This file is part of DisOrder +# Copyright (C) 2004, 2005 Richard Kettlewell +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +set -e + +title=$(basename $1) + +cat <<EOF +<html> + <head> +@include{stdhead}@ + <title>$title + + +@include{@label{menu}@}@ +EOF +printf "
"
+# this is kind of painful using only BREs
+nroff -man "$1" | sed 's/&/\&/g;
+                       s//\>/g;
+                       s/@/\@/g;
+                       s!\(.\)\1!\1!g;
+                       s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
+                       s!_\(.\)!\1!g;
+                       s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
+                       s!<\1>!!g'
+cat <
+@include{@label{menu}@end}@
+ 
+
+EOF
+# arch-tag:c0096f33b8a8f7d88236043ed970ae83
diff --git a/scripts/inst b/scripts/inst
new file mode 100755
index 0000000..e0a51f0
--- /dev/null
+++ b/scripts/inst
@@ -0,0 +1,29 @@
+#! /bin/bash
+#
+# Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+set -e
+set -x
+[ -d =build ] && cd =build
+make "$@"
+make check
+really make "$@" install
+really ./libtool --mode=install install -m 755 server/disorder.cgi /home/jukebox/public_html/index.cgi
+really ldconfig
+# arch-tag:cbb189b7f23939cc806cf6dc0c2cb0d5
diff --git a/scripts/makedeb b/scripts/makedeb
new file mode 100755
index 0000000..0531460
--- /dev/null
+++ b/scripts/makedeb
@@ -0,0 +1,37 @@
+#! /bin/bash
+#
+# Copyright (C) 2004, 2005 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+set -e
+buildhost=${buildhost:-lyonesse}
+builddir=${builddir:-.}
+make dist-bzip2
+distdir=`make echo-distdir`
+scp $distdir.tar.bz2 $buildhost:$builddir/$distdir.tar.bz2
+ssh $buildhost "
+  set -e
+  set -x
+  cd $builddir
+  rm -rf $distdir
+  tar xfj $distdir.tar.bz2
+  cd $distdir
+  CC='ccache cc' debian/rules build
+  fakeroot debian/rules binary
+"
+# arch-tag:3ebf6ef38076b7cb313b86843e741d97
diff --git a/scripts/oggrename b/scripts/oggrename
new file mode 100644
index 0000000..458a32b
--- /dev/null
+++ b/scripts/oggrename
@@ -0,0 +1,58 @@
+#! /usr/bin/perl -w
+#
+# This file is part of DisOrder
+# Copyright (C) 2006 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+use strict;
+
+my %rename = ();
+my $bad = 0;
+
+for my $path (@ARGV) {
+    local $_ = `ogginfo \Q$path\E`;
+    my ($title, $number);
+    if(/title=(.*)/) { $title = $1; }
+    if(/tracknumber=(\d+)/) { $number = $1; }
+    if(!defined $title || !defined $number) {
+	print STDERR "ERROR: cannot find details for $path\n";
+	++$bad;
+	next;
+    }
+    $rename{$path} = sprintf("%02d:%s.ogg", $number, $title);
+}
+exit 1 if $bad;
+
+while(scalar %rename) {
+    my $worked = 0;
+    while(my ($f, $t) = each %rename) {
+	if($f eq $t) {
+	    delete $rename{$f};
+	    $worked = 1;
+	} elsif(link($f, $t)) {
+	    unlink($f);
+	    delete $rename{$f};
+	    print "$f -> $t\n";
+	    $worked = 1;
+	} else {
+	    print "deferring $f -> $t\n";
+	}
+    }
+    die "stuck in a loop!\n" if !$worked;
+}
+# arch-tag:AU/Rh0oscz3GBqwzJYRy1w
diff --git a/scripts/sedfiles.make b/scripts/sedfiles.make
new file mode 100644
index 0000000..534c5de
--- /dev/null
+++ b/scripts/sedfiles.make
@@ -0,0 +1,36 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+$(SEDFILES) : % : %.in Makefile
+	rm -f $@.new
+	sed 's!sbindir!${sbindir}!g;\
+	     s!bindir!${bindir}!g;\
+	     s!pkgconfdir!${sysconfdir}/disorder!g;\
+	     s!pkgstatedir!${localstatedir}/disorder!g;\
+	     s!pkgdatadir!${pkgdatadir}!g;\
+	     s!_version_!${VERSION}!g;\
+	        ' < $< > $@.new
+	chmod 444 $@.new
+	mv -f $@.new $@
+
+# Local Variables:
+# mode:makefile
+# End:
+# arch-tag:97f84bf68cbbc5a0d72871aa0e704447
diff --git a/scripts/text2c b/scripts/text2c
new file mode 100755
index 0000000..23079f3
--- /dev/null
+++ b/scripts/text2c
@@ -0,0 +1,15 @@
+#! /usr/bin/perl -w
+my $name = shift;
+push(@out, "/* autogenerated file, do not edit */\n\n");
+push(@out, "static const char $name\[] = \n");
+while(<>) {
+    next if /arch-tag/;
+    s/[\\\"\?]/\\$&/g;
+    s/\n/\\n/g;
+    push(@out, "  \"$_\"\n");
+}
+push(@out, ";\n");
+((print @out)
+ and (close STDOUT))
+    or die "$0: stdout: $!\n";
+# arch-tag:2eRG7Dpm6lpL7BR3hPvriQ
diff --git a/server/Makefile.am b/server/Makefile.am
new file mode 100644
index 0000000..d0e8087
--- /dev/null
+++ b/server/Makefile.am
@@ -0,0 +1,84 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
+	      disorder-speaker
+noinst_PROGRAMS=disorder.cgi trackname
+
+AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
+
+disorderd_SOURCES=disorderd.c				\
+	api.c api-server.c				\
+	daemonize.c daemonize.h				\
+	play.c play.h 					\
+	server.c server.h     				\
+	state.c state.h					\
+	trackdb.c trackdb.h trackdb-int.h
+disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBPCRE) $(LIBDB) $(LIBAO)
+disorderd_LDFLAGS=-export-dynamic
+disorderd_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_deadlock_SOURCES=deadlock.c                    \
+	trackdb.c trackdb.h
+disorder_deadlock_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBDB)
+disorder_deadlock_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_speaker_SOURCES=speaker.c
+disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBASOUND)
+disorder_speaker_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_rescan_SOURCES=rescan.c                        \
+	api.c api-server.c				\
+	trackdb.c trackdb.h
+disorder_rescan_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBDB)
+disorder_rescan_LDFLAGS=-export-dynamic
+disorder_rescan_DEPENDENCIES=../lib/libdisorder.la
+
+disorder_dump_SOURCES=dump.c                           \
+        trackdb.c trackdb.h
+disorder_dump_LDADD=$(LIBOBJS) ../lib/libdisorder.la $(LIBPCRE) $(LIBDB)
+disorder_dump_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.la
+
+disorder_cgi_SOURCES=dcgi.c dcgi.h			\
+	api.c api-client.c api-client.h			\
+	cgi.c cgi.h cgimain.c
+disorder_cgi_LDADD=../lib/libdisorder.la $(LIBPCRE)
+disorder_cgi_LDFLAGS=-export-dynamic
+disorder_cgi_DEPENDENCIES=../lib/libdisorder.la
+
+trackname_SOURCES=trackname.c
+trackname_LDADD=../lib/libdisorder.la
+trackname_DEPENDENCIES=../lib/libdisorder.la
+
+install-exec-hook:
+	$(LIBTOOL) --mode=finish $(DESTDIR)$(libdir)
+
+check: check-help
+
+# check everything has working --help
+check-help: all
+	./disorderd --help > /dev/null
+	./disorder-dump --help > /dev/null
+	./disorder-deadlock --help > /dev/null
+	./trackname --help > /dev/null
+	./disorder-speaker --help > /dev/null
+
+cgi.o: ../lib/definitions.h
+# arch-tag:f36fc0fa65dd5143a80874c1c83f595f
diff --git a/server/api-client.c b/server/api-client.c
new file mode 100644
index 0000000..a69ec7e
--- /dev/null
+++ b/server/api-client.c
@@ -0,0 +1,73 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+
+static disorder_client *c;
+
+disorder_client *disorder_get_client(void) {
+  if(!c)
+    if(!(c = disorder_new(0))) exit(EXIT_FAILURE);
+  return c;
+}
+
+int disorder_track_exists(const char *track) {
+  int result;
+
+  return disorder_exists(c, track, &result) ? 0 : result;
+}
+
+const char *disorder_track_get_data(const char *track, const char *key) {
+  char *value;
+
+  if(disorder_get(c, track, key, &value)) return 0;
+  return value;
+}
+
+int disorder_track_set_data(const char *track,
+			    const char *key,
+			    const char *value) {
+  if(value)
+    return disorder_set(c, track, key, value);
+  else
+    return disorder_unset(c, track, key);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:3c00ffad971b689e5af1d9a9ff45a120 */
diff --git a/server/api-client.h b/server/api-client.h
new file mode 100644
index 0000000..174405d
--- /dev/null
+++ b/server/api-client.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#ifndef API_CLIENT_H
+#define API_CLIENT_H
+
+disorder_client *disorder_get_client(void);
+
+#endif /* API_CLIENT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:fe8ca21b625a51949cea0f5e2d319215 */
diff --git a/server/api-server.c b/server/api-server.c
new file mode 100644
index 0000000..18056f2
--- /dev/null
+++ b/server/api-server.c
@@ -0,0 +1,60 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "event.h"
+#include "trackdb.h"
+
+int disorder_track_exists(const char *track)  {
+  return trackdb_exists(track);
+}
+
+const char *disorder_track_get_data(const char *track, const char *key)  {
+  return trackdb_get(track, key);
+}
+
+int disorder_track_set_data(const char *track,
+			    const char *key, const char *value)  {
+  return trackdb_set(track, key, value);
+}
+
+const char *disorder_track_random(void)  {
+  return trackdb_random(16);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:b39fbeb5a45190fa30e7f3ac20cd6c34 */
diff --git a/server/api.c b/server/api.c
new file mode 100644
index 0000000..8d631ca
--- /dev/null
+++ b/server/api.c
@@ -0,0 +1,78 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "printf.h"
+
+/* shared implementation of vararg functions */
+#include "log-impl.h"
+#include "mem-impl.h"
+
+void *disorder_malloc(size_t n) {
+  return xmalloc(n);
+}
+
+void *disorder_realloc(void *p, size_t n) {
+  return xrealloc(p, n);
+}
+
+void *disorder_malloc_noptr(size_t n) {
+  return xmalloc_noptr(n);
+}
+
+void *disorder_realloc_noptr(void *p, size_t n) {
+  return xrealloc_noptr(p, n);
+}
+
+char *disorder_strdup(const char *p) {
+  return xstrdup(p);
+}
+
+char *disorder_strndup(const char *p, size_t n) {
+  return xstrndup(p, n);
+}
+
+int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) {
+  int n;
+  va_list ap;
+
+  va_start(ap, fmt);
+  n = byte_vsnprintf(buffer, bufsize, fmt, ap);
+  va_end(ap);
+  return n;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a033645978e202c94260fc21959e59a7 */
diff --git a/server/cgi.c b/server/cgi.c
new file mode 100644
index 0000000..02bad31
--- /dev/null
+++ b/server/cgi.c
@@ -0,0 +1,619 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "mem.h"
+#include "log.h"
+#include "hex.h"
+#include "charset.h"
+#include "configuration.h"
+#include "table.h"
+#include "syscalls.h"
+#include "kvp.h"
+#include "vector.h"
+#include "split.h"
+#include "inputline.h"
+#include "regsub.h"
+#include "defs.h"
+#include "sink.h"
+#include "cgi.h"
+#include "printf.h"
+#include "mime.h"
+#include "utf8.h"
+
+struct kvp *cgi_args;
+
+/* options */
+struct column {
+  struct column *next;
+  char *name;
+  int ncolumns;
+  char **columns;
+};
+
+#define RELIST(x) struct re *x, **x##_tail = &x
+
+static int have_read_options;
+static struct kvp *labels;
+static struct column *columns;
+
+static void include_options(const char *name);
+
+static void cgi_parse_get(void) {
+  const char *q;
+
+  if(!(q = getenv("QUERY_STRING"))) fatal(0, "QUERY_STRING not set");
+  cgi_args = kvp_urldecode(q, strlen(q));
+}
+
+static void cgi_input(char **ptrp, size_t *np) {
+  const char *cl;
+  char *q;
+  size_t n, m = 0;
+  int r;
+
+  if(!(cl = getenv("CONTENT_LENGTH"))) fatal(0, "CONTENT_LENGTH not set");
+  n = atol(cl);
+  q = xmalloc_noptr(n + 1);
+  while(m < n) {
+    r = read(0, q + m, n - m);
+    if(r > 0)
+      m += r;
+    else if(r == 0)
+      fatal(0, "unexpected end of file reading request body");
+    else switch(errno) {
+    case EINTR: break;
+    default: fatal(errno, "error reading request body");
+    }
+  }
+  if(memchr(q, 0, n)) fatal(0, "null character in request body");
+  q[n + 1] = 0;
+  *ptrp = q;
+  if(np) *np = n;
+}
+
+static int cgi_field_callback(const char *name, const char *value,
+			      void *u) {
+  char *disposition, *pname, *pvalue;
+  char **namep = u;
+
+  if(!strcmp(name, "content-disposition")) {
+    if(mime_rfc2388_content_disposition(value,
+					&disposition,
+					&pname,
+					&pvalue))
+      fatal(0, "error parsing Content-Disposition field");
+    if(!strcmp(disposition, "form-data")
+       && pname
+       && !strcmp(pname, "name")) {
+      if(*namep)
+	fatal(0, "duplicate Content-Disposition field");
+      *namep = pvalue;
+    }
+  }
+  return 0;
+}
+
+static int cgi_part_callback(const char *s,
+			     void attribute((unused)) *u) {
+  char *name = 0;
+  struct kvp *k;
+  
+  if(!(s = mime_parse(s, cgi_field_callback, &name)))
+    fatal(0, "error parsing part header");
+  if(!name) fatal(0, "no name found");
+  k = xmalloc(sizeof *k);
+  k->next = cgi_args;
+  k->name = name;
+  k->value = s;
+  cgi_args = k;
+  return 0;
+}
+
+static void cgi_parse_multipart(const char *boundary) {
+  char *q;
+  
+  cgi_input(&q, 0);
+  if(mime_multipart(q, cgi_part_callback, boundary, 0))
+    fatal(0, "invalid multipart object");
+}
+
+static void cgi_parse_post(void) {
+  const char *ct;
+  char *q, *type, *pname, *pvalue;
+  size_t n;
+
+  if(!(ct = getenv("CONTENT_TYPE")))
+    ct = "application/x-www-form-urlencoded";
+  if(mime_content_type(ct, &type, &pname, &pvalue))
+    fatal(0, "invalid content type '%s'", ct);
+  if(!strcmp(type, "application/x-www-form-urlencoded")) {
+    cgi_input(&q, &n);
+    cgi_args = kvp_urldecode(q, n);
+    return;
+  }
+  if(!strcmp(type, "multipart/form-data")) {
+    if(!pname || strcmp(pname, "boundary"))
+      fatal(0, "expected a boundary parameter, found %s",
+	    pname ? pname : "nothing");
+    cgi_parse_multipart(pvalue);
+    return;
+  }
+  fatal(0, "unrecognized content type '%s'", type);
+}
+
+void cgi_parse(void) {
+  const char *p;
+  struct kvp *k;
+
+  if(!(p = getenv("REQUEST_METHOD"))) fatal(0, "REQUEST_METHOD not set");
+  if(!strcmp(p, "GET"))
+    cgi_parse_get();
+  else if(!strcmp(p, "POST"))
+    cgi_parse_post();
+  else
+    fatal(0, "unknown request method %s", p);
+  for(k = cgi_args; k; k = k->next)
+    if(!validutf8(k->name)
+       || !validutf8(k->value))
+      fatal(0, "invalid UTF-8 sequence in cgi argument");
+}
+
+const char *cgi_get(const char *name) {
+  return kvp_get(cgi_args, name);
+}
+
+void cgi_output(cgi_sink *output, const char *fmt, ...) {
+  va_list ap;
+  int n;
+  char *r;
+
+  va_start(ap, fmt);
+  n = byte_vasprintf(&r, fmt, ap);
+  if(n < 0)
+    fatal(errno, "error calling byte_vasprintf");
+  if(output->quote)
+    r = cgi_sgmlquote(r, 0);
+  output->sink->write(output->sink, r, strlen(r));
+  va_end(ap);
+}
+
+void cgi_header(struct sink *output, const char *name, const char *value) {
+  sink_printf(output, "%s: %s\r\n", name, value);
+}
+
+void cgi_body(struct sink *output) {
+  sink_printf(output, "\r\n");
+}
+
+char *cgi_sgmlquote(const char *s, int raw) {
+  uint32_t *ucs, *p, c;
+  char *b, *bp;
+  int n;
+
+  if(!raw) {
+    if(!(ucs = utf82ucs4(s))) exit(EXIT_FAILURE);
+  } else {
+    ucs = xmalloc_noptr((strlen(s) + 1) * sizeof(uint32_t));
+    for(n = 0; s[n]; ++n)
+      ucs[n] = (unsigned char)s[n];
+    ucs[n] = 0;
+  }
+
+  n = 1;
+  /* estimate the length we'll need */
+  for(p = ucs; (c = *p); ++p) {
+    switch(c) {
+    default:
+      if(c > 127 || c < 32) {
+      case '"':
+      case '&':
+      case '<':
+      case '>':
+	n += 12;
+	break;
+      } else
+	n++;
+    }
+  }
+  /* format the string */
+  b = bp = xmalloc_noptr(n);
+  for(p = ucs; (c = *p); ++p) {
+    switch(c) {
+    default:
+      if(*p > 127 || *p < 32) {
+      case '"':
+      case '&':
+      case '<':
+      case '>':
+	bp += sprintf(bp, "&#%lu;", (unsigned long)c);
+	break;
+      } else
+	*bp++ = c;
+    }
+  }
+  *bp = 0;
+  return b;
+}
+
+void cgi_attr(struct sink *output, const char *name, const char *value) {
+  if(!value[strspn(value, "abcdefghijklmnopqrstuvwxyz"
+		   "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+		   "0123456789")])
+    sink_printf(output, "%s=%s", name, value);
+  else
+    sink_printf(output, "%s=\"%s\"", name, cgi_sgmlquote(value, 0));
+}
+
+void cgi_opentag(struct sink *output, const char *name, ...) {
+  va_list ap;
+  const char *n, *v;
+   
+  sink_printf(output, "<%s", name);
+  va_start(ap, name);
+  while((n = va_arg(ap, const char *))) {
+    sink_printf(output, " ");
+    v = va_arg(ap, const char *);
+    if(v)
+      cgi_attr(output, n, v);
+    else
+      sink_printf(output, n);
+  }
+  sink_printf(output, ">");
+}
+
+void cgi_closetag(struct sink *output, const char *name) {
+  sink_printf(output, "", name);
+}
+
+static int template_open(const char *name,
+			 const char *ext,
+			 const char **filenamep) {
+  const char *dirs[2];
+  int fd = -1, n;
+  char *fullpath;
+
+  dirs[0] = pkgconfdir;
+  dirs[1] = pkgdatadir;
+  if(name[0] == '/') {
+    if((fd = open(name, O_RDONLY)) < 0) fatal(0, "cannot open %s", name);
+    *filenamep = name;
+  } else {
+    for(n = 0; n < config->templates.n + (int)(sizeof dirs / sizeof *dirs); ++n) {
+      byte_xasprintf(&fullpath, "%s/%s%s",
+		     n < config->templates.n ? config->templates.s[n]
+		                             : dirs[n - config->templates.n],
+		     name, ext);
+      if((fd = open(fullpath, O_RDONLY)) >= 0) break;
+    }
+    if(fd < 0) error(0, "cannot find %s%s in template path", name, ext);
+    *filenamep = fullpath;
+  }
+  return fd;
+}
+
+static int valid_template_name(const char *name) {
+  if(strchr(name, '/') || name[0] == '.')
+    return 0;
+  return 1;
+}
+
+void cgi_expand(const char *template,
+		const struct cgi_expansion *expansions,
+		size_t nexpansions,
+		cgi_sink *output,
+		void *u) {
+  int fd = -1;
+  int n;
+  off_t m;
+  char *b;
+  struct stat sb;
+
+  if(!valid_template_name(template))
+    fatal(0, "invalid template name '%s'", template);
+  if((fd = template_open(template, ".html", &template)) < 0)
+    exitfn(EXIT_FAILURE);
+  if(fstat(fd, &sb) < 0) fatal(errno, "cannot stat %s", template);
+  m = 0;
+  b = xmalloc_noptr(sb.st_size + 1);
+  while(m < sb.st_size) {
+    n = read(fd, b + m, sb.st_size - m);
+    if(n > 0) m += n;
+    else if(n == 0) fatal(0, "unexpected EOF reading %s", template);
+    else if(errno != EINTR) fatal(errno, "error reading %s", template);
+  }
+  b[sb.st_size] = 0;
+  xclose(fd);
+  cgi_expand_string(template, b, expansions, nexpansions, output, u);
+}
+
+void cgi_expand_string(const char *name,
+		       const char *template,
+		       const struct cgi_expansion *expansions,
+		       size_t nexpansions,
+		       cgi_sink *output,
+		       void *u) {
+  int braces, n, m, line = 1, sline;
+  char *argname;
+  const char *p;
+  struct vector v;
+  struct dynstr d;
+  cgi_sink parameter_output;
+  
+  while(*template) {
+    if(*template != '@') {
+      p = template;
+      while(*p && *p != '@') {
+	if(*p == '\n') ++line;
+	++p;
+      }
+      output->sink->write(output->sink, template, p - template);
+      template = p;
+      continue;
+    }
+    vector_init(&v);
+    braces = 0;
+    ++template;
+    sline = line;
+    while(*template != '@') {
+      dynstr_init(&d);
+      if(*template == '{') {
+	/* bracketed arg */
+	++template;
+	while(*template && (*template != '}' || braces > 0)) {
+	  switch(*template) {
+	  case '{': ++braces; break;
+	  case '}': --braces; break;
+	  case '\n': ++line; break;
+	  }
+	  dynstr_append(&d, *template++);
+	}
+	if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+	++template;
+	/* skip whitespace after closing bracket */
+	while(isspace((unsigned char)*template))
+	  ++template;
+      } else {
+	/* unbracketed arg */
+	/* leading whitespace is not significant in unquoted args */
+	while(isspace((unsigned char)*template))
+	  ++template;
+	while(*template
+	      && *template != '@' && *template != '{' && *template != ':') {
+	  if(*template == '\n') ++line;
+	  dynstr_append(&d, *template++);
+	}
+	if(*template == ':')
+	  ++template;
+	if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+	/* trailing whitespace is not significant in unquoted args */
+	while(d.nvec && (isspace((unsigned char)d.vec[d.nvec - 1])))
+	  --d.nvec;
+      }
+      dynstr_terminate(&d);
+      vector_append(&v, d.vec);
+    }
+    ++template;
+    vector_terminate(&v);
+    /* @@ terminates this file */
+    if(v.nvec == 0)
+      break;
+    if((n = table_find(expansions,
+		       offsetof(struct cgi_expansion, name),
+		       sizeof (struct cgi_expansion),
+		       nexpansions,
+		       v.vec[0])) < 0)
+      fatal(0, "%s:%d: unknown expansion '%s'", name, line, v.vec[0]);
+    if(v.nvec - 1 < expansions[n].minargs)
+      fatal(0, "%s:%d: insufficient arguments to @%s@ (min %d, got %d)",
+	    name, line, v.vec[0], expansions[n].minargs, v.nvec - 1);
+    if(v.nvec - 1 > expansions[n].maxargs)
+      fatal(0, "%s:%d: too many arguments to @%s@ (max %d, got %d)",
+	    name, line, v.vec[0], expansions[n].maxargs, v.nvec - 1);
+    /* for ordinary expansions, recursively expand the arguments */
+    if(!(expansions[n].flags & EXP_MAGIC)) {
+      for(m = 1; m < v.nvec; ++m) {
+	dynstr_init(&d);
+	byte_xasprintf(&argname, "<%s:%d arg #%d>", name, sline, m);
+	parameter_output.quote = 0;
+	parameter_output.sink = sink_dynstr(&d);
+	cgi_expand_string(argname, v.vec[m],
+			  expansions, nexpansions,
+			  ¶meter_output, u);
+	dynstr_terminate(&d);
+	v.vec[m] = d.vec;
+      }
+    }
+    expansions[n].handler(v.nvec - 1, v.vec + 1, output, u);
+  }
+}
+
+char *cgi_makeurl(const char *url, ...) {
+  va_list ap;
+  struct kvp *kvp, *k, **kk = &kvp;
+  struct dynstr d;
+  const char *n, *v;
+  
+  dynstr_init(&d);
+  dynstr_append_string(&d, url);
+  va_start(ap, url);
+  while((n = va_arg(ap, const char *))) {
+    v = va_arg(ap, const char *);
+    *kk = k = xmalloc(sizeof *k);
+    kk = &k->next;
+    k->name = n;
+    k->value = v;
+  }
+  *kk = 0;
+  if(kvp) {
+    dynstr_append(&d, '?');
+    dynstr_append_string(&d, kvp_urlencode(kvp, 0));
+  }
+  dynstr_terminate(&d);
+  return d.vec;
+}
+
+void cgi_set_option(const char *name, const char *value) {
+  struct kvp *k = xmalloc(sizeof *k);
+
+  k->next = labels;
+  k->name = name;
+  k->value = value;
+  labels = k;
+}
+
+static void option_label(int attribute((unused)) nvec,
+			 char **vec) {
+  cgi_set_option(vec[0], vec[1]);
+}
+
+static void option_include(int attribute((unused)) nvec,
+			   char **vec) {
+  include_options(vec[0]);
+}
+
+static void option_columns(int nvec,
+			    char **vec) {
+  struct column *c = xmalloc(sizeof *c);
+  
+  c->next = columns;
+  c->name = vec[0];
+  c->ncolumns = nvec - 1;
+  c->columns = &vec[1];
+  columns = c;
+}
+
+static struct option {
+  const char *name;
+  int minargs, maxargs;
+  void (*handler)(int nvec, char **vec);
+} options[] = {
+  { "columns", 1, INT_MAX, option_columns },
+  { "include", 1, 1, option_include },
+  { "label", 2, 2, option_label },
+};
+
+struct read_options_state {
+  const char *name;
+  int line;
+};
+
+static void read_options_error(const char *msg,
+			       void *u) {
+  struct read_options_state *cs = u;
+  
+  error(0, "%s:%d: %s", cs->name, cs->line, msg);
+}
+
+static void include_options(const char *name) {
+  int n, i;
+  int fd;
+  FILE *fp;
+  char **vec, *buffer;
+  struct read_options_state cs;
+
+  if((fd = template_open(name, "", &cs.name)) < 0) return;
+  if(!(fp = fdopen(fd, "r"))) fatal(errno, "error calling fdopen");
+  cs.line = 0;
+  while(!inputline(cs.name, fp, &buffer, '\n')) {
+    ++cs.line;
+    if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
+		     read_options_error, &cs)))
+      continue;
+    if(!n) continue;
+    if((i = TABLE_FIND(options, struct option, name, vec[0])) == -1) {
+      error(0, "%s:%d: unknown option '%s'", cs.name, cs.line, vec[0]);
+      continue;
+    }
+    ++vec;
+    --n;
+    if(n < options[i].minargs) {
+      error(0, "%s:%d: too few arguments to '%s'", cs.name, cs.line, vec[-1]);
+      continue;
+    }
+    if(n > options[i].maxargs) {
+      error(0, "%s:%d: too many arguments to '%s'", cs.name, cs.line, vec[-1]);
+      continue;
+    }
+    options[i].handler(n, vec);
+  }
+  fclose(fp);
+}
+
+static void read_options(void) {
+  if(!have_read_options) {
+    have_read_options = 1;
+    include_options("options");
+  }
+}
+
+const char *cgi_label(const char *key) {
+  const char *label;
+
+  read_options();
+  if(!(label = kvp_get(labels, key))) {
+    if((label = strchr(key, '.')))
+      ++label;
+    else
+      label = key;
+  }
+  return label;
+}
+
+char **cgi_columns(const char *name, int *ncolumns) {
+  struct column *c;
+
+  read_options();
+  for(c = columns; c && strcmp(name, c->name); c = c->next)
+    ;
+  if(c) {
+    if(ncolumns)
+      *ncolumns = c->ncolumns;
+    return c->columns;
+  } else {
+    if(ncolumns)
+      *ncolumns = 0;
+    return 0;
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a7a5220f29b8bb8d64c0f836f7f41f1f */
diff --git a/server/cgi.h b/server/cgi.h
new file mode 100644
index 0000000..e871052
--- /dev/null
+++ b/server/cgi.h
@@ -0,0 +1,108 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef CGI_H
+#define CGI_H
+
+extern struct kvp *cgi_args;
+
+typedef struct {
+  int quote;
+  struct sink *sink;
+} cgi_sink;
+
+void cgi_parse(void);
+/* parse CGI args */
+
+const char *cgi_get(const char *name);
+/* get an argument */
+
+void cgi_header(struct sink *output, const char *name, const char *value);
+/* output a header.  @name@ and @value@ are ASCII. */
+
+void cgi_body(struct sink *output);
+/* indicate the start of the body */
+
+void cgi_output(cgi_sink *output, const char *fmt, ...)
+  attribute((format (printf, 2, 3)));
+/* SGML-quote formatted UTF-8 data and write it.  Checks errors. */
+
+char *cgi_sgmlquote(const char *s, int raw);
+/* SGML-quote multibyte string @s@ */
+
+void cgi_attr(struct sink *output, const char *name, const char *value);
+/* write an attribute */
+
+void cgi_opentag(struct sink *output, const char *name, ...);
+/* write an open tag, including attribute name-value pairs terminate
+ * by (char *)0 */
+
+void cgi_closetag(struct sink *output, const char *name);
+/* write a close tag */
+
+struct cgi_expansion {
+  const char *name;
+  int minargs, maxargs;
+  unsigned flags;
+#define EXP_MAGIC 0x0001
+  void (*handler)(int nargs, char **args, cgi_sink *output, void *u);
+};
+
+void cgi_expand(const char *name,
+		const struct cgi_expansion *expansions,
+		size_t nexpansions,
+		cgi_sink *output,
+		void *u);
+/* find @name@ and substitute for expansions */
+
+void cgi_expand_string(const char *name,
+		       const char *template,
+		       const struct cgi_expansion *expansions,
+		       size_t nexpansions,
+		       cgi_sink *output,
+		       void *u);
+/* same but @template@ is text of template */
+
+char *cgi_makeurl(const char *url, ...);
+/* make up a URL */
+
+const char *cgi_label(const char *key);
+/* look up the translated label @key@ */
+
+char **cgi_columns(const char *name, int *nheadings);
+/* return the list of columns for @name@ */
+
+const char *cgi_transform(const char *type,
+			  const char *track,
+			  const char *context);
+/* transform a track or directory name for display */
+
+void cgi_set_option(const char *name, const char *value);
+/* set an option */
+
+#endif /* CGI_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:46351d0acf757f7867a139f369342949 */
diff --git a/server/cgimain.c b/server/cgimain.c
new file mode 100644
index 0000000..9be0d1b
--- /dev/null
+++ b/server/cgimain.c
@@ -0,0 +1,80 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+  
+int main(int argc, char **argv) {
+  const char *user, *conf;
+  dcgi_global g;
+  dcgi_state s;
+  cgi_sink output;
+
+  mem_init(0);
+  if(argc > 0) progname = argv[0];
+  cgi_parse();
+  if((conf = getenv("DISORDER_CONFIG"))) configfile = xstrdup(conf);
+  if(getenv("DISORDER_DEBUG")) debugging = 1;
+  if(config_read()) exit(EXIT_FAILURE);
+  memset(&g, 0, sizeof g);
+  memset(&s, 0, sizeof s);
+  s.g = &g;
+  g.client = disorder_get_client();
+  output.quote = 1;
+  output.sink = sink_stdio("stdout", stdout); 
+  if(!(user = getenv("REMOTE_USER"))) fatal(0, "REMOTE_USER is not set");
+  if(disorder_connect(g.client)) {
+    disorder_cgi_error(&output, &s, "connect");
+    return 0;
+  }
+  if(disorder_become(g.client, user)) {
+    disorder_cgi_error(&output, &s, "become");
+    return 0;
+  }
+  disorder_cgi(&output, &s);
+  if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:1c8016a19d7f117427c184c8aadf1bba */
diff --git a/server/daemonize.c b/server/daemonize.c
new file mode 100644
index 0000000..bddc167
--- /dev/null
+++ b/server/daemonize.c
@@ -0,0 +1,89 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "daemonize.h"
+#include "syscalls.h"
+#include "log.h"
+
+void daemonize(const char *tag, int fac, const char *pidfile) {
+  pid_t pid, r;
+  int w, dn;
+  FILE *fp;
+
+  D(("daemonize tag=%s fac=%d pidfile=%s",
+     tag ? tag : "NULL", fac, pidfile ? pidfile : "NULL"));
+  /* make sure that FDs 0, 1, 2 all at least exist (and get a
+   * /dev/null) */
+  do {
+    if((dn = open("/dev/null", O_RDWR, 0)) < 0)
+      fatal(errno, "error opening /dev/null");
+  } while(dn < 3);
+  pid = xfork();
+  if(pid) {
+    /* Parent process.  Wait for the first child to finish, then
+     * return to the caller. */
+    exitfn = _exit;
+    while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+      ;
+    if(r < 0) fatal(errno, "error calling waitpid");
+    if(w) error(0, "subprocess exited with wait status %#x", (unsigned)w);
+    _exit(0);
+  }
+  /* First child process.  This will be the session leader, and will
+   * be transient. */
+  D(("first child pid=%lu", (unsigned long)getpid()));
+  if(setsid() < 0) fatal(errno, "error calling setsid");
+  /* we'll log to syslog */
+  openlog(tag, LOG_PID, fac);
+  log_default = &log_syslog;
+  /* stdin/out/err we lose */
+  xdup2(dn, 0);
+  xdup2(dn, 1);
+  xdup2(dn, 2);
+  xclose(dn);
+  pid = xfork();
+  if(pid)
+    _exit(0);
+  /* second child.  Write a pidfile if someone wanted it. */
+  D(("second child pid=%lu", (unsigned long)getpid()));
+  if(pidfile) {
+    if(!(fp = fopen(pidfile, "w"))
+       || fprintf(fp, "%lu\n", (unsigned long)getpid()) < 0
+       || fclose(fp) < 0)
+      fatal(errno, "error creating %s", pidfile);
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:dc74d9007ddce97d782d3dacf9aa2ed6 */
diff --git a/server/daemonize.h b/server/daemonize.h
new file mode 100644
index 0000000..97e9edd
--- /dev/null
+++ b/server/daemonize.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef DAEMONIZE_H
+#define DAEMONIZE_H
+
+void daemonize(const char *tag, int fac, const char *pidfile);
+/* Go into background.  Send stdout/stderr to syslog.
+ * If @pri@ is non-null, it should be "facility.level"
+ * If @tag@ is non-null, it is used as a tag to each message
+ * If @pidfile@ is non-null, the PID is written to that file.
+ */
+
+#endif /* DAEMONIZE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:6fae05bd34750bba75b336637f6a0c56 */
diff --git a/server/dcgi.c b/server/dcgi.c
new file mode 100644
index 0000000..1f2d044
--- /dev/null
+++ b/server/dcgi.c
@@ -0,0 +1,1482 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "client.h"
+#include "mem.h"
+#include "vector.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "log.h"
+#include "configuration.h"
+#include "table.h"
+#include "queue.h"
+#include "plugin.h"
+#include "split.h"
+#include "words.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "syscalls.h"
+#include "printf.h"
+#include "regsub.h"
+#include "defs.h"
+#include "trackname.h"
+
+static void expand(cgi_sink *output,
+		   const char *template,
+		   dcgi_state *ds);
+static void expandstring(cgi_sink *output,
+			 const char *string,
+			 dcgi_state *ds);
+
+struct entry {
+  const char *path;
+  const char *sort;
+  const char *display;
+};
+
+static const char *nonce(void) {
+  static unsigned long count;
+  char *s;
+
+  byte_xasprintf(&s, "%lx%lx%lx",
+	   (unsigned long)time(0),
+	   (unsigned long)getpid(),
+	   count++);
+  return s;
+}
+
+static int compare_entry(const void *a, const void *b) {
+  const struct entry *ea = a, *eb = b;
+
+  return compare_tracks(ea->sort, eb->sort,
+			ea->display, eb->display,
+			ea->path, eb->path);
+}
+
+static const char *front_url(void) {
+  char *url;
+  const char *mgmt;
+
+  /* preserve management interface visibility */
+  if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
+    byte_xasprintf(&url, "%s?mgmt=true", config->url);
+    return url;
+  }
+  return config->url;
+}
+
+static void redirect(struct sink *output) {
+  const char *back;
+
+  cgi_header(output, "Location",
+	     (back = cgi_get("back")) ? back : front_url());
+  cgi_body(output);
+}
+
+static void lookups(dcgi_state *ds, unsigned want) {
+  unsigned need;
+  struct queue_entry *r, *rnext;
+  const char *dir, *re;
+
+  if(ds->g->client && (need = want ^ (ds->g->flags & want)) != 0) {
+    if(need & DC_QUEUE)
+      disorder_queue(ds->g->client, &ds->g->queue);
+    if(need & DC_PLAYING)
+      disorder_playing(ds->g->client, &ds->g->playing);
+    if(need & DC_RECENT) {
+      /* we need to reverse the order of the list */
+      disorder_recent(ds->g->client, &r);
+      while(r) {
+	rnext = r->next;
+	r->next = ds->g->recent;
+	ds->g->recent = r;
+	r = rnext;
+      }
+    }
+    if(need & DC_VOLUME)
+      disorder_get_volume(ds->g->client,
+			  &ds->g->volume_left, &ds->g->volume_right);
+    if(need & (DC_FILES|DC_DIRS)) {
+      if(!(dir = cgi_get("directory")))
+	dir = "";
+      re = cgi_get("regexp");
+      if(need & DC_DIRS)
+	if(disorder_directories(ds->g->client, dir, re,
+				&ds->g->dirs, &ds->g->ndirs))
+	  ds->g->ndirs = 0;
+      if(need & DC_FILES)
+	if(disorder_files(ds->g->client, dir, re,
+			  &ds->g->files, &ds->g->nfiles))
+	  ds->g->nfiles = 0;
+    }
+    ds->g->flags |= need;
+  }
+}
+
+/* actions ********************************************************************/
+
+static void act_disable(cgi_sink *output,
+			dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_disable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_enable(cgi_sink *output,
+			      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_enable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_random_disable(cgi_sink *output,
+			       dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_random_disable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_random_enable(cgi_sink *output,
+			      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_random_enable(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_remove(cgi_sink *output,
+		       dcgi_state *ds) {
+  const char *id;
+
+  if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+  if(ds->g->client)
+    disorder_remove(ds->g->client, id);
+  redirect(output->sink);
+}
+
+static void act_move(cgi_sink *output,
+		     dcgi_state *ds) {
+  const char *id, *delta;
+
+  if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+  if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
+  if(ds->g->client)
+    disorder_move(ds->g->client, id, atoi(delta));
+  redirect(output->sink);
+}
+
+static void act_scratch(cgi_sink *output,
+			dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_scratch(ds->g->client, cgi_get("id"));
+  redirect(output->sink);
+}
+
+static void act_playing(cgi_sink *output, dcgi_state *ds) {
+  char r[1024];
+  long refresh = config->refresh, length;
+  time_t now, fin;
+  int random_enabled = 0;
+  int enabled = 0;
+
+  lookups(ds, DC_PLAYING|DC_QUEUE);
+  cgi_header(output->sink, "Content-Type", "text/html");
+  disorder_random_enabled(ds->g->client, &random_enabled);
+  disorder_enabled(ds->g->client, &enabled);
+  if(ds->g->playing
+     && ds->g->playing->state == playing_started /* i.e. not paused */
+     && !disorder_length(ds->g->client, ds->g->playing->track, &length)
+     && length
+     && ds->g->playing->sofar >= 0) {
+    /* Try to put the next refresh at the start of the next track. */
+    time(&now);
+    fin = now + length - ds->g->playing->sofar + config->gap;
+    if(now + refresh > fin)
+      refresh = fin - now;
+  }
+  if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
+    /* next track is a scratch, don't leave more than the inter-track gap */
+    if(refresh > config->gap)
+      refresh = config->gap;
+  }
+  if(!ds->g->playing && ((ds->g->queue
+			  && ds->g->queue->state != playing_random)
+			 || random_enabled) && enabled) {
+    /* no track playing but playing is enabled and there is something coming
+     * up, must be in a gap */
+    if(refresh > config->gap)
+      refresh = config->gap;
+  }
+  byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
+		front_url());
+  cgi_header(output->sink, "Refresh", r);
+  cgi_body(output->sink);
+  expand(output, "playing", ds);
+}
+
+static void act_play(cgi_sink *output,
+		     dcgi_state *ds) {
+  const char *track, *dir;
+  char **tracks;
+  int ntracks, n;
+  struct entry *e;
+
+  if((track = cgi_get("file"))) {
+    disorder_play(ds->g->client, track);
+  } else if((dir = cgi_get("directory"))) {
+    if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
+    if(ntracks) {
+      e = xmalloc(ntracks * sizeof (struct entry));
+      for(n = 0; n < ntracks; ++n) {
+	e[n].path = tracks[n];
+	e[n].sort = trackname_transform("track", tracks[n], "sort");
+	e[n].display = trackname_transform("track", tracks[n], "display");
+      }
+      qsort(e, ntracks, sizeof (struct entry), compare_entry);
+      for(n = 0; n < ntracks; ++n)
+	disorder_play(ds->g->client, e[n].path);
+    }
+  }
+  /* XXX error handling */
+  redirect(output->sink);
+}
+
+static int clamp(int n, int min, int max) {
+  if(n < min)
+    return min;
+  if(n > max)
+    return max;
+  return n;
+}
+
+static const char *volume_url(void) {
+  char *url;
+  
+  byte_xasprintf(&url, "%s?action=volume", config->url);
+  return url;
+}
+
+static void act_volume(cgi_sink *output, dcgi_state *ds) {
+  const char *l, *r, *d, *back;
+  int nd, changed = 0;;
+
+  if((d = cgi_get("delta"))) {
+    lookups(ds, DC_VOLUME);
+    nd = clamp(atoi(d), -255, 255);
+    disorder_set_volume(ds->g->client,
+			clamp(ds->g->volume_left + nd, 0, 255),
+			clamp(ds->g->volume_right + nd, 0, 255));
+    changed = 1;
+  } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
+    disorder_set_volume(ds->g->client, atoi(l), atoi(r));
+    changed = 1;
+  }
+  if(changed) {
+    /* redirect back to ourselves (but without the volume-changing bits in the
+     * URL) */
+    cgi_header(output->sink, "Location",
+	       (back = cgi_get("back")) ? back : volume_url());
+    cgi_body(output->sink);
+  } else {
+    cgi_header(output->sink, "Content-Type", "text/html");
+    cgi_body(output->sink);
+    expand(output, "volume", ds);
+  }
+}
+
+static void act_prefs_errors(const char *msg,
+			     void attribute((unused)) *u) {
+  fatal(0, "error splitting parts list: %s", msg);
+}
+
+static const char *numbered_arg(const char *argname, int numfile) {
+  char *fullname;
+
+  byte_xasprintf(&fullname, "%d_%s", numfile, argname);
+  return cgi_get(fullname);
+}
+
+static void process_prefs(dcgi_state *ds, int numfile) {
+  const char *file, *name, *value, *part, *parts, *current, *context;
+  char **partslist;
+
+  if(!(file = numbered_arg("file", numfile)))
+    /* The first file doesn't need numbering. */
+    if(numfile > 0 || !(file = cgi_get("file")))
+      return;
+  if((parts = numbered_arg("parts", numfile))
+     || (parts = cgi_get("parts"))) {
+    /* Default context is display.  Other contexts not actually tested. */
+    if(!(context = numbered_arg("context", numfile))) context = "display";
+    partslist = split(parts, 0, 0, act_prefs_errors, 0);
+    while((part = *partslist++)) {
+      if(!(value = numbered_arg(part, numfile)))
+	continue;
+      /* If it's already right (whether regexps or db) don't change anything,
+       * so we don't fill the database up with rubbish. */
+      if(disorder_part(ds->g->client, (char **)¤t,
+		       file, context, part))
+	fatal(0, "disorder_part() failed");
+      if(!strcmp(current, value))
+	continue;
+      byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
+      disorder_set(ds->g->client, file, name, value);
+    }
+    if((value = numbered_arg("random", numfile)))
+      disorder_unset(ds->g->client, file, "pick_at_random");
+    else
+      disorder_set(ds->g->client, file, "pick_at_random", "0");
+    if((value = numbered_arg("tags", numfile)))
+      disorder_set(ds->g->client, file, "tags", value);
+  } else if((name = cgi_get("name"))) {
+    /* Raw preferences.  Not well supported in the templates at the moment. */
+    value = cgi_get("value");
+    if(value)
+      disorder_set(ds->g->client, file, name, value);
+    else
+      disorder_unset(ds->g->client, file, name);
+  }
+}
+
+static void act_prefs(cgi_sink *output, dcgi_state *ds) {
+  const char *files;
+  int nfiles, numfile;
+
+  if((files = cgi_get("files"))) nfiles = atoi(files);
+  else nfiles = 1;
+  for(numfile = 0; numfile < nfiles; ++numfile)
+    process_prefs(ds, numfile);
+  cgi_header(output->sink, "Content-Type", "text/html");
+  cgi_body(output->sink);
+  expand(output, "prefs", ds);
+}
+
+static void act_pause(cgi_sink *output,
+		      dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_pause(ds->g->client);
+  redirect(output->sink);
+}
+
+static void act_resume(cgi_sink *output,
+		       dcgi_state *ds) {
+  if(ds->g->client)
+    disorder_resume(ds->g->client);
+  redirect(output->sink);
+}
+
+static const struct action {
+  const char *name;
+  void (*handler)(cgi_sink *output, dcgi_state *ds);
+} actions[] = {
+  { "disable", act_disable },
+  { "enable", act_enable },
+  { "move", act_move },
+  { "pause", act_pause },
+  { "play", act_play },
+  { "playing", act_playing },
+  { "prefs", act_prefs },
+  { "random-disable", act_random_disable },
+  { "random-enable", act_random_enable },
+  { "remove", act_remove },
+  { "resume", act_resume },
+  { "scratch", act_scratch },
+  { "volume", act_volume },
+};
+
+/* expansions *****************************************************************/
+
+static void exp_include(int attribute((unused)) nargs,
+			char **args,
+			cgi_sink *output,
+			void *u) {
+  expand(output, args[0], u);
+}
+
+static void exp_server_version(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  const char *v;
+
+  if(ds->g->client) {
+    if(disorder_version(ds->g->client, (char **)&v)) v = "(cannot get version)";
+  } else
+    v = "(server not running)";
+  cgi_output(output, "%s", v);
+}
+
+static void exp_version(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void attribute((unused)) *u) {
+  cgi_output(output, "%s", disorder_version_string);
+}
+
+static void exp_nonce(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  cgi_output(output, "%s", nonce());
+}
+
+static void exp_label(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  cgi_output(output, "%s", cgi_label(args[0]));
+}
+
+struct trackinfo_state {
+  dcgi_state *ds;
+  const struct queue_entry *q;
+  long length;
+  time_t when;
+};
+
+static void exp_who(int attribute((unused)) nargs,
+		    char attribute((unused)) **args,
+		    cgi_sink *output,
+		    void *u) {
+  dcgi_state *ds = u;
+  
+  if(ds->track && ds->track->submitter)
+    cgi_output(output, "%s", ds->track->submitter);
+}
+
+static void exp_length(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  long length;
+
+  if(ds->track
+     && (ds->track->state == playing_started
+	 || ds->track->state == playing_paused)
+     && ds->track->sofar >= 0)
+    cgi_output(output, "%ld:%02ld/",
+	       ds->track->sofar / 60, ds->track->sofar % 60);
+  if(!ds->track || disorder_length(ds->g->client, ds->track->track, &length))
+    length = 0;
+  if(length)
+    cgi_output(output, "%ld:%02ld", length / 60, length % 60);
+  else
+    sink_printf(output->sink, "%s", " ");
+}
+
+static void exp_when(int attribute((unused)) nargs,
+		     char attribute((unused)) **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+  const struct tm *w = 0;
+
+  if(ds->track)
+    switch(ds->track->state) {
+    case playing_isscratch:
+    case playing_unplayed:
+    case playing_random:
+      if(ds->track->expected)
+	w = localtime(&ds->track->expected);
+      break;
+    case playing_failed:
+    case playing_no_player:
+    case playing_ok:
+    case playing_scratched:
+    case playing_started:
+    case playing_paused:
+    case playing_quitting:
+      if(ds->track->played)
+	w = localtime(&ds->track->played);
+      break;
+    }
+  if(w)
+    cgi_output(output, "%d:%02d", w->tm_hour, w->tm_min);
+  else
+    sink_printf(output->sink, " ");
+}
+
+static void exp_part(int nargs,
+		     char **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+  const char *s, *track, *part, *context;
+
+  if(nargs == 3)
+    track = args[2];
+  else {
+    if(ds->track)
+      track = ds->track->track;
+    else if(ds->tracks)
+      track = ds->tracks[0];
+    else
+      track = 0;
+  }
+  if(track) {
+    switch(nargs) {
+    case 1:
+      context = "display";
+      part = args[0];
+      break;
+    case 2:
+    case 3:
+      context = args[0];
+      part = args[1];
+      break;
+    default:
+      abort();
+    }
+    if(disorder_part(ds->g->client, (char **)&s, track, context, part))
+      fatal(0, "disorder_part() failed");
+    cgi_output(output, "%s", s);
+  } else
+    sink_printf(output->sink, " ");
+}
+
+static void exp_playing(int attribute((unused)) nargs,
+			char **args,
+			cgi_sink *output,
+			void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+
+  lookups(ds, DC_PLAYING);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  if(ds->g->playing) {
+    s.track = ds->g->playing;
+    expandstring(output, args[0], &s);
+  }
+}
+
+static void exp_queue(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+  struct queue_entry *q;
+
+  lookups(ds, DC_QUEUE);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  s.first = 1;
+  for(q = ds->g->queue; q; q = q->next) {
+    s.last = !q->next;
+    s.track = q;
+    expandstring(output, args[0], &s);
+    s.index++;
+    s.first = 0;
+  }
+}
+
+static void exp_recent(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void  *u) {
+  dcgi_state *ds = u;
+  dcgi_state s;
+  struct queue_entry *q;
+
+  lookups(ds, DC_RECENT);
+  memset(&s, 0, sizeof s);
+  s.g = ds->g;
+  s.first = 1;
+  for(q = ds->g->recent; q; q = q->next) {
+    s.last = !q;
+    s.track = q;
+    expandstring(output, args[0], &s);
+    s.index++;
+    s.first = 0;
+  }
+}
+
+static void exp_url(int attribute((unused)) nargs,
+		    char attribute((unused)) **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  cgi_output(output, "%s", config->url);
+}
+
+struct result {
+  char *track;
+  const char *sort;
+};
+
+static int compare_result(const void *a, const void *b) {
+  const struct result *ra = a, *rb = b;
+  int c;
+
+  if(!(c = strcmp(ra->sort, rb->sort)))
+    c = strcmp(ra->track, rb->track);
+  return c;
+}
+
+static void exp_search(int nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u, substate;
+  char **tracks;
+  const char *q, *context, *part, *template;
+  int ntracks, n, m;
+  struct result *r;
+
+  switch(nargs) {
+  case 2:
+    part = args[0];
+    context = "sort";
+    template = args[1];
+    break;
+  case 3:
+    part = args[0];
+    context = args[1];
+    template = args[2];
+    break;
+  default:
+    assert(!"should never happen");
+    part = context = template = 0;	/* quieten compiler */
+  }
+  if(ds->tracks == 0) {
+    /* we are the top level, let's get some search results */
+    if(!(q = cgi_get("query"))) return;	/* no results yet */
+    if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
+    if(!ntracks) return;
+  } else {
+    tracks = ds->tracks;
+    ntracks = ds->ntracks;
+  }
+  assert(ntracks != 0);
+  /* sort tracks by the appropriate part */
+  r = xmalloc(ntracks * sizeof *r);
+  for(n = 0; n < ntracks; ++n) {
+    r[n].track = tracks[n];
+    if(disorder_part(ds->g->client, (char **)&r[n].sort,
+		     tracks[n], context, part))
+      fatal(0, "disorder_part() failed");
+  }
+  qsort(r, ntracks, sizeof (struct result), compare_result);
+  /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
+   * array as we know it's guaranteed to be big enough and isn't going to be
+   * used for anything else any more. */
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  n = 0;
+  while(n < ntracks) {
+    substate.tracks = tracks;
+    substate.ntracks = 0;
+    m = n;
+    while(m < ntracks
+	  && !strcmp(r[m].sort, r[n].sort))
+      tracks[substate.ntracks++] = r[m++].track;
+    substate.last = (m == ntracks);
+    expandstring(output, template, &substate);
+    substate.index++;
+    substate.first = 0;
+    n = m;
+  }
+  assert(substate.last != 0);
+}
+
+static void exp_arg(int attribute((unused)) nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  const char *v;
+
+  if((v = cgi_get(args[0])))
+    cgi_output(output, "%s", v);
+}
+
+static void exp_stats(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  char **v;
+
+  cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
+  if(!disorder_stats(ds->g->client, &v, 0)) {
+    while(*v)
+      cgi_output(output, "%s\n", *v++);
+  }
+  cgi_closetag(output->sink, "pre");
+}
+
+static void exp_volume(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_VOLUME);
+  if(!strcmp(args[0], "left"))
+    cgi_output(output, "%d", ds->g->volume_left);
+  else
+    cgi_output(output, "%d", ds->g->volume_right);
+}
+
+static void exp_shell(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void attribute((unused)) *u) {
+  int w, p[2], n;
+  char buffer[4096];
+  pid_t pid;
+  
+  xpipe(p);
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    xclose(p[0]);
+    xdup2(p[1], 1);
+    xclose(p[1]);
+    execlp("sh", "sh", "-c", args[0], (char *)0);
+    fatal(errno, "error executing sh");
+  }
+  xclose(p[1]);
+  while((n = read(p[0], buffer, sizeof buffer))) {
+    if(n < 0) {
+      if(errno == EINTR) continue;
+      else fatal(errno, "error reading from pipe");
+    }
+    output->sink->write(output->sink, buffer, n);
+  }
+  xclose(p[0]);
+  while((n = waitpid(pid, &w, 0)) < 0 && errno == EINTR)
+    ;
+  if(n < 0) fatal(errno, "error calling waitpid");
+  if(w)
+    error(0, "shell command '%s' %s", args[0], wstat(w));
+}
+
+static inline int str2bool(const char *s) {
+  return !strcmp(s, "true");
+}
+
+static inline const char *bool2str(int n) {
+  return n ? "true" : "false";
+}
+
+static char *expandarg(const char *arg, dcgi_state *ds) {
+  struct dynstr d;
+  cgi_sink output;
+
+  dynstr_init(&d);
+  output.quote = 0;
+  output.sink = sink_dynstr(&d);
+  expandstring(&output, arg, ds);
+  dynstr_terminate(&d);
+  return d.vec;
+}
+
+static void exp_prefs(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  struct kvp *k;
+  const char *file = expandarg(args[0], ds);
+  
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  if(disorder_prefs(ds->g->client, file, &k)) return;
+  while(k) {
+    substate.last = !k->next;
+    substate.pref = k;
+    expandstring(output, args[1], &substate);
+    ++substate.index;
+    k = k->next;
+    substate.first = 0;
+  }
+}
+
+static void exp_pref(int attribute((unused)) nargs,
+		     char **args,
+		     cgi_sink *output,
+		     void *u) {
+  char *value;
+  dcgi_state *ds = u;
+
+  if(!disorder_get(ds->g->client, args[0], args[1], &value))
+    cgi_output(output, "%s", value);
+}
+
+static void exp_if(int nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+  int n = str2bool(expandarg(args[0], ds)) ? 1 : 2;
+  
+  if(n < nargs)
+    expandstring(output, args[n], ds);
+}
+
+static void exp_and(int nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void *u) {
+  dcgi_state *ds = u;
+  int n, result = 1;
+
+  for(n = 0; n < nargs; ++n)
+    if(!str2bool(expandarg(args[n], ds))) {
+      result = 0;
+      break;
+    }
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_or(int nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+  int n, result = 0;
+
+  for(n = 0; n < nargs; ++n)
+    if(str2bool(expandarg(args[n], ds))) {
+      result = 1;
+      break;
+    }
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_not(int attribute((unused)) nargs,
+		    char **args,
+		    cgi_sink *output,
+		    void attribute((unused)) *u) {
+  sink_printf(output->sink, "%s", bool2str(!str2bool(args[0])));
+}
+
+static void exp_isplaying(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_PLAYING);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->playing));
+}
+
+static void exp_isqueue(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_QUEUE);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->queue));
+}
+
+static void exp_isrecent(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_RECENT);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->recent));
+}
+
+static void exp_id(int attribute((unused)) nargs,
+		   char attribute((unused)) **args,
+		   cgi_sink *output,
+		   void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", ds->track->id);
+}
+
+static void exp_track(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", ds->track->track);
+}
+
+static void exp_parity(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+
+  cgi_output(output, "%s", ds->index % 2 ? "odd" : "even");
+}
+
+static void exp_comment(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink attribute((unused)) *output,
+			void attribute((unused)) *u) {
+  /* do nothing */
+}
+
+static void exp_prefname(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->pref && ds->pref->name)
+    cgi_output(output, "%s", ds->pref->name);
+}
+
+static void exp_prefvalue(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->pref && ds->pref->value)
+    cgi_output(output, "%s", ds->pref->value);
+}
+
+static void exp_isfiles(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_FILES);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
+}
+
+static void exp_isdirectories(int attribute((unused)) nargs,
+			      char attribute((unused)) **args,
+			      cgi_sink *output,
+			      void *u) {
+  dcgi_state *ds = u;
+
+  lookups(ds, DC_DIRS);
+  sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
+}
+
+static void exp_choose(int attribute((unused)) nargs,
+		       char **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  int nfiles, n;
+  char **files;
+  struct entry *e;
+  const char *type, *what = expandarg(args[0], ds);
+
+  if(!strcmp(what, "files")) {
+    lookups(ds, DC_FILES);
+    files = ds->g->files;
+    nfiles = ds->g->nfiles;
+    type = "track";
+  } else if(!strcmp(what, "directories")) {
+    lookups(ds, DC_DIRS);
+    files = ds->g->dirs;
+    nfiles = ds->g->ndirs;
+    type = "dir";
+  } else {
+    error(0, "unknown @choose@ argument '%s'", what);
+    return;
+  }
+  e = xmalloc(nfiles * sizeof (struct entry));
+  for(n = 0; n < nfiles; ++n) {
+    e[n].path = files[n];
+    e[n].sort = trackname_transform(type, files[n], "sort");
+    e[n].display = trackname_transform(type, files[n], "display");
+  }
+  qsort(e, nfiles, sizeof (struct entry), compare_entry);
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  substate.first = 1;
+  for(n = 0; n < nfiles; ++n) {
+    substate.last = (n == nfiles - 1);
+    substate.index = n;
+    substate.entry = &e[n];
+    expandstring(output, args[1], &substate);
+    substate.first = 0;
+  }
+}
+
+static void exp_file(int attribute((unused)) nargs,
+		     char attribute((unused)) **args,
+		     cgi_sink *output,
+		     void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->entry)
+    cgi_output(output, "%s", ds->entry->path);
+  else if(ds->track)
+    cgi_output(output, "%s", ds->track->track);
+  else if(ds->tracks)
+    cgi_output(output, "%s", ds->tracks[0]);
+}
+
+static void exp_transform(int nargs,
+			  char **args,
+			  cgi_sink *output,
+			  void attribute((unused)) *u) {
+  const char *context = nargs > 2 ? args[2] : "display";
+
+  cgi_output(output, "%s", trackname_transform(args[1], args[0], context));
+}
+
+static void exp_urlquote(int attribute((unused)) nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void attribute((unused)) *u) {
+  cgi_output(output, "%s", urlencodestring(args[0]));
+}
+
+static void exp_scratchable(int attribute((unused)) nargs,
+			    char attribute((unused)) **args,
+			    cgi_sink *output,
+			    void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  int result;
+
+  if(config->restrictions & RESTRICT_SCRATCH) {
+    lookups(ds, DC_PLAYING);
+    result = (ds->g->playing
+	      && (!ds->g->playing->submitter
+		  || !strcmp(ds->g->playing->submitter,
+			     disorder_user(ds->g->client))));
+  } else
+    result = 1;
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_removable(int attribute((unused)) nargs,
+			  char attribute((unused)) **args,
+			  cgi_sink *output,
+			  void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  int result;
+
+  if(config->restrictions & RESTRICT_REMOVE)
+    result = (ds->track
+	      && ds->track->submitter
+	      && !strcmp(ds->track->submitter,
+			 disorder_user(ds->g->client)));
+  else
+    result = 1;
+  sink_printf(output->sink, "%s", bool2str(result));
+}
+
+static void exp_navigate(int attribute((unused)) nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  const char *path = expandarg(args[0], ds);
+  const char *ptr;
+  int dirlen;
+
+  if(*path) {
+    memset(&substate, 0, sizeof substate);
+    substate.g = ds->g;
+    ptr = path + 1;			/* skip root */
+    dirlen = 0;
+    substate.nav_path = path;
+    substate.first = 1;
+    while(*ptr) {
+      while(*ptr && *ptr != '/')
+	++ptr;
+      substate.last = !*ptr;
+      substate.nav_len = ptr - path;
+      substate.nav_dirlen = dirlen;
+      expandstring(output, args[1], &substate);
+      dirlen = substate.nav_len;
+      if(*ptr) ++ptr;
+      substate.first = 0;
+    }
+  }
+}
+
+static void exp_fullname(int attribute((unused)) nargs,
+			 char attribute((unused)) **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
+}
+
+static void exp_basename(int nargs,
+			 char **args,
+			 cgi_sink *output,
+			 void *u) {
+  dcgi_state *ds = u;
+  const char *s;
+  
+  if(nargs) {
+    if((s = strrchr(args[0], '/'))) ++s;
+    else s = args[0];
+    cgi_output(output, "%s", s);
+  } else
+    cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
+	       ds->nav_path + ds->nav_dirlen + 1);
+}
+
+static void exp_dirname(int nargs,
+			char **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+  const char *s;
+  
+  if(nargs) {
+    if((s = strrchr(args[0], '/')))
+      cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
+  } else
+    cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
+}
+
+static void exp_eq(int attribute((unused)) nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void attribute((unused)) *u) {
+  cgi_output(output, "%s", bool2str(!strcmp(args[0], args[1])));
+}
+
+static void exp_ne(int attribute((unused)) nargs,
+		   char **args,
+		   cgi_sink *output,
+		   void attribute((unused)) *u) {
+  cgi_output(output, "%s", bool2str(strcmp(args[0], args[1])));
+}
+
+static void exp_enabled(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  int enabled = 0;
+
+  if(ds->g->client)
+    disorder_enabled(ds->g->client, &enabled);
+  cgi_output(output, "%s", bool2str(enabled));
+}
+
+static void exp_random_enabled(int attribute((unused)) nargs,
+			       char attribute((unused)) **args,
+			       cgi_sink *output,
+			       void *u) {
+  dcgi_state *ds = u;
+  int enabled = 0;
+
+  if(ds->g->client)
+    disorder_random_enabled(ds->g->client, &enabled);
+  cgi_output(output, "%s", bool2str(enabled));
+}
+
+static void exp_trackstate(int attribute((unused)) nargs,
+			   char **args,
+			   cgi_sink *output,
+			   void *u) {
+  dcgi_state *ds = u;
+  struct queue_entry *q;
+  char *track;
+
+  if(disorder_resolve(ds->g->client, &track, args[0])) return;
+  lookups(ds, DC_QUEUE|DC_PLAYING);
+  if(ds->g->playing && !strcmp(ds->g->playing->track, track))
+    cgi_output(output, "playing");
+  else {
+    for(q = ds->g->queue; q && strcmp(q->track, track); q = q->next)
+      ;
+    if(q)
+      cgi_output(output, "queued");
+  }
+}
+
+static void exp_thisurl(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void attribute((unused)) *u) {
+  kvp_set(&cgi_args, "nonce", nonce());	/* nonces had better differ! */
+  cgi_output(output, "%s?%s", config->url, kvp_urlencode(cgi_args, 0));
+}
+
+static void exp_isfirst(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  sink_printf(output->sink, "%s", bool2str(!!ds->first));
+}
+
+static void exp_islast(int attribute((unused)) nargs,
+			char attribute((unused)) **args,
+			cgi_sink *output,
+			void *u) {
+  dcgi_state *ds = u;
+
+  sink_printf(output->sink, "%s", bool2str(!!ds->last));
+}
+
+static void exp_action(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void attribute((unused)) *u) {
+  const char *action = cgi_get("action"), *mgmt;
+
+  if(!action) action = "playing";
+  if(!strcmp(action, "playing")
+     && (mgmt = cgi_get("mgmt"))
+     && !strcmp(mgmt, "true"))
+    action = "manage";
+  sink_printf(output->sink, "%s", action);
+}
+
+static void exp_resolve(int attribute((unused)) nargs,
+                      char  **args,
+                      cgi_sink *output,
+                      void attribute((unused)) *u) {
+  dcgi_state *ds = u;
+  char *track;
+  
+  if(!disorder_resolve(ds->g->client, &track, args[0]))
+    sink_printf(output->sink, "%s", track);
+}
+ 
+static void exp_paused(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  int paused = 0;
+
+  lookups(ds, DC_PLAYING);
+  if(ds->g->playing && ds->g->playing->state == playing_paused)
+    paused = 1;
+  cgi_output(output, "%s", bool2str(paused));
+}
+
+static void exp_state(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  if(ds->track)
+    cgi_output(output, "%s", playing_states[ds->track->state]);
+}
+
+static void exp_files(int attribute((unused)) nargs,
+		      char **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+  dcgi_state substate;
+  const char *nfiles_arg, *directory;
+  int nfiles, numfile;
+  struct kvp *k;
+
+  memset(&substate, 0, sizeof substate);
+  substate.g = ds->g;
+  if((directory = cgi_get("directory"))) {
+    /* Prefs for whole directory. */
+    lookups(ds, DC_FILES);
+    /* Synthesize args for the file list. */
+    nfiles = ds->g->nfiles;
+    for(numfile = 0; numfile < nfiles; ++numfile) {
+      k = xmalloc(sizeof *k);
+      byte_xasprintf((char **)&k->name, "%d_file", numfile);
+      k->value = ds->g->files[numfile];
+      k->next = cgi_args;
+      cgi_args = k;
+    }
+  } else {
+    /* Args already present. */
+    if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
+    else nfiles = 1;
+  }
+  for(numfile = 0; numfile < nfiles; ++numfile) {
+    substate.index = numfile;
+    expandstring(output, args[0], &substate);
+  }
+}
+
+static void exp_index(int attribute((unused)) nargs,
+		      char attribute((unused)) **args,
+		      cgi_sink *output,
+		      void *u) {
+  dcgi_state *ds = u;
+
+  cgi_output(output, "%d", ds->index);
+}
+
+static void exp_nfiles(int attribute((unused)) nargs,
+		       char attribute((unused)) **args,
+		       cgi_sink *output,
+		       void *u) {
+  dcgi_state *ds = u;
+  const char *files_arg;
+
+  if(cgi_get("directory")) {
+    lookups(ds, DC_FILES);
+    cgi_output(output, "%d", ds->g->nfiles);
+  } else if((files_arg = cgi_get("files")))
+    cgi_output(output, "%s", files_arg);
+  else
+    cgi_output(output, "1");
+}
+
+static const struct cgi_expansion expansions[] = {
+  { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
+  { "action", 0, 0, 0, exp_action },
+  { "and", 0, INT_MAX, EXP_MAGIC, exp_and },
+  { "arg", 1, 1, 0, exp_arg },
+  { "basename", 0, 1, 0, exp_basename },
+  { "choose", 2, 2, EXP_MAGIC, exp_choose },
+  { "dirname", 0, 1, 0, exp_dirname },
+  { "enabled", 0, 0, 0, exp_enabled },
+  { "eq", 2, 2, 0, exp_eq },
+  { "file", 0, 0, 0, exp_file },
+  { "files", 1, 1, EXP_MAGIC, exp_files },
+  { "fullname", 0, 0, 0, exp_fullname },
+  { "id", 0, 0, 0, exp_id },
+  { "if", 2, 3, EXP_MAGIC, exp_if },
+  { "include", 1, 1, 0, exp_include },
+  { "index", 0, 0, 0, exp_index },
+  { "isdirectories", 0, 0, 0, exp_isdirectories },
+  { "isfiles", 0, 0, 0, exp_isfiles },
+  { "isfirst", 0, 0, 0, exp_isfirst },
+  { "islast", 0, 0, 0, exp_islast },
+  { "isplaying", 0, 0, 0, exp_isplaying },
+  { "isqueue", 0, 0, 0, exp_isqueue },
+  { "isrecent", 0, 0, 0, exp_isrecent },
+  { "label", 1, 1, 0, exp_label },
+  { "length", 0, 0, 0, exp_length },
+  { "navigate", 2, 2, EXP_MAGIC, exp_navigate },
+  { "ne", 2, 2, 0, exp_ne },
+  { "nfiles", 0, 0, 0, exp_nfiles },
+  { "nonce", 0, 0, 0, exp_nonce },
+  { "not", 1, 1, 0, exp_not },
+  { "or", 0, INT_MAX, EXP_MAGIC, exp_or },
+  { "parity", 0, 0, 0, exp_parity },
+  { "part", 1, 3, 0, exp_part },
+  { "paused", 0, 0, 0, exp_paused },
+  { "playing", 1, 1, EXP_MAGIC, exp_playing },
+  { "pref", 2, 2, 0, exp_pref },
+  { "prefname", 0, 0, 0, exp_prefname },
+  { "prefs", 2, 2, EXP_MAGIC, exp_prefs },
+  { "prefvalue", 0, 0, 0, exp_prefvalue },
+  { "queue", 1, 1, EXP_MAGIC, exp_queue },
+  { "random-enabled", 0, 0, 0, exp_random_enabled },
+  { "recent", 1, 1, EXP_MAGIC, exp_recent },
+  { "removable", 0, 0, 0, exp_removable },
+  { "resolve", 1, 1, 0, exp_resolve },
+  { "scratchable", 0, 0, 0, exp_scratchable },
+  { "search", 2, 3, EXP_MAGIC, exp_search },
+  { "server-version", 0, 0, 0, exp_server_version },
+  { "shell", 1, 1, 0, exp_shell },
+  { "state", 0, 0, 0, exp_state },
+  { "stats", 0, 0, 0, exp_stats },
+  { "thisurl", 0, 0, 0, exp_thisurl },
+  { "track", 0, 0, 0, exp_track },
+  { "trackstate", 1, 1, 0, exp_trackstate },
+  { "transform", 2, 3, 0, exp_transform },
+  { "url", 0, 0, 0, exp_url },
+  { "urlquote", 1, 1, 0, exp_urlquote },
+  { "version", 0, 0, 0, exp_version },
+  { "volume", 1, 1, 0, exp_volume },
+  { "when", 0, 0, 0, exp_when },
+  { "who", 0, 0, 0, exp_who }
+};
+
+static void expand(cgi_sink *output,
+		   const char *template,
+		   dcgi_state *ds) {
+  cgi_expand(template,
+	     expansions, sizeof expansions / sizeof *expansions,
+	     output,
+	     ds);
+}
+
+static void expandstring(cgi_sink *output,
+			 const char *string,
+			 dcgi_state *ds) {
+  cgi_expand_string("",
+		    string,
+		    expansions, sizeof expansions / sizeof *expansions,
+		    output,
+		    ds);
+}
+
+static void perform_action(cgi_sink *output, dcgi_state *ds,
+			   const char *action) {
+  int n;
+
+  if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
+    actions[n].handler(output, ds);
+  else {
+    cgi_header(output->sink, "Content-Type", "text/html");
+    cgi_body(output->sink);
+    expand(output, action, ds);
+  }
+}
+
+void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
+  const char *action = cgi_get("action");
+
+  if(!action) action = "playing";
+  perform_action(output, ds, action);
+}
+
+void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
+			const char *msg) {
+  cgi_set_option("error", msg);
+  perform_action(output, ds, "error");
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:a3ec8cc0814587e3c39540b2b5ca9e18 */
diff --git a/server/dcgi.h b/server/dcgi.h
new file mode 100644
index 0000000..2846001
--- /dev/null
+++ b/server/dcgi.h
@@ -0,0 +1,66 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef DCGI_H
+#define DCGI_H
+
+typedef struct dcgi_global {
+  disorder_client *client;
+  unsigned flags;
+#define DC_QUEUE 0x0001
+#define DC_PLAYING 0x0002
+#define DC_RECENT 0x0004
+#define DC_VOLUME 0x0008
+#define DC_DIRS 0x0010
+#define DC_FILES 0x0020
+  struct queue_entry *queue, *playing, *recent;
+  int volume_left, volume_right;
+  char **files, **dirs;
+  int nfiles, ndirs;
+} dcgi_global;
+
+typedef struct dcgi_state {
+  dcgi_global *g;
+  struct queue_entry *track;
+  struct kvp *pref;
+  int index;
+  int first, last;
+  struct entry *entry;
+  /* for searching */
+  int ntracks;
+  char **tracks;
+  /* for @navigate@ */
+  const char *nav_path;
+  int nav_len, nav_dirlen;
+} dcgi_state;
+
+void disorder_cgi(cgi_sink *output, dcgi_state *ds);
+void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
+			const char *msg);
+
+#endif /* DCGI_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a555827f0549ee9a35303c2d0f9dabc5 */
diff --git a/server/deadlock.c b/server/deadlock.c
new file mode 100644
index 0000000..aa29b41
--- /dev/null
+++ b/server/deadlock.c
@@ -0,0 +1,124 @@
+/*
+ * This file is part of DisOrder 
+ * Copyright (C) 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "kvp.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-deadlock [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Deadlock manager for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-deadlock version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n, err, aborted;
+
+  set_progname(argv);
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* if stderr is a TTY then log there, otherwise to syslog */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  info("started");
+  trackdb_init(0);
+  while(getppid() != 1) {
+    if((err = trackdb_env->lock_detect(trackdb_env,
+				       0,
+				       DB_LOCK_DEFAULT,
+				       &aborted)))
+      fatal(0, "trackdb_env->lock_detect: %s", db_strerror(err));
+    if(aborted)
+      D(("aborted %d lock requests", aborted));
+    sleep(1);
+  }
+  /* if our parent goes away, it's time to stop */
+  info("stopped (parent terminated)");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:8VzpRqqo2qifzXWK1GxF5A */
diff --git a/server/disorderd.c b/server/disorderd.c
new file mode 100644
index 0000000..06788f8
--- /dev/null
+++ b/server/disorderd.c
@@ -0,0 +1,275 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "daemonize.h"
+#include "event.h"
+#include "log.h"
+#include "configuration.h"
+#include "trackdb.h"
+#include "queue.h"
+#include "mem.h"
+#include "play.h"
+#include "server.h"
+#include "state.h"
+#include "syscalls.h"
+#include "defs.h"
+#include "user.h"
+#include "mixer.h"
+#include "eventlog.h"
+
+static ev_source *ev;
+
+static void rescan_after(long offset);
+static void dbgc_after(long offset);
+static void volumecheck_after(long offset);
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "foreground", no_argument, 0, 'f' },
+  { "log", required_argument, 0, 'l' },
+  { "pidfile", required_argument, 0, 'P' },
+  { "no-initial-rescan", no_argument, 0, 'N' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorderd [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h               Display usage message\n"
+	  "  --version, -V            Display version number\n"
+	  "  --config PATH, -c PATH   Set configuration file\n"
+	  "  --debug, -d              Turn on debugging\n"
+	  "  --foreground, -f         Do not become a daemon\n"
+	  "  --pidfile PATH, -P PATH  Leave a pidfile\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorderd version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* SIGHUP callback */
+static int handle_sighup(ev_source attribute((unused)) *ev_,
+			 int attribute((unused)) sig,
+			 void attribute((unused)) *u) {
+  info("received SIGHUP");
+  reconfigure(ev, 1);
+  return 0;
+}
+
+/* fatal signals */
+
+static int handle_sigint(ev_source attribute((unused)) *ev_,
+			 int attribute((unused)) sig,
+			 void attribute((unused)) *u) {
+  info("received SIGINT");
+  quit(ev);
+}
+
+static int handle_sigterm(ev_source attribute((unused)) *ev_,
+			  int attribute((unused)) sig,
+			  void attribute((unused)) *u) {
+  info("received SIGTERM");
+  quit(ev);
+}
+
+static int rescan_again(ev_source *ev_,
+			const struct timeval attribute((unused)) *now,
+			void attribute((unused)) *u) {
+  trackdb_rescan(ev_);
+  rescan_after(86400);
+  return 0;
+}
+
+static void rescan_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, rescan_again, 0);
+}
+
+static int dbgc_again(ev_source attribute((unused)) *ev_,
+		      const struct timeval attribute((unused)) *now,
+		      void attribute((unused)) *u) {
+  trackdb_gc();
+  dbgc_after(60);
+  return 0;
+}
+
+static void dbgc_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, dbgc_again, 0);
+}
+
+static int volumecheck_again(ev_source attribute((unused)) *ev_,
+			     const struct timeval attribute((unused)) *now,
+			     void attribute((unused)) *u) {
+  int l, r;
+  char lb[32], rb[32];
+
+  if(!mixer_control(&l, &r, 0)) {
+    if(l != volume_left || r != volume_right) {
+      volume_left = l;
+      volume_right = r;
+      snprintf(lb, sizeof lb, "%d", l);
+      snprintf(rb, sizeof rb, "%d", r);
+      eventlog("volume", lb, rb, (char *)0);
+    }
+  }
+  volumecheck_after(60);
+  return 0;
+}
+
+static void volumecheck_after(long offset) {
+  struct timeval w;
+
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, volumecheck_again, 0);
+}
+
+ int main(int argc, char **argv) {
+  int n, background = 1;
+  const char *pidfile = 0;
+  int initial_rescan = 1;
+
+  set_progname(argv);
+  mem_init(1);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  /* garbage-collect PCRE's memory */
+  pcre_malloc = xmalloc;
+  pcre_free = xfree;
+  while((n = getopt_long(argc, argv, "hVc:dfP:N", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'f': background = 0; break;
+    case 'P': pidfile = optarg; break;
+    case 'N': initial_rescan = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* go into background if necessary */
+  if(background)
+    daemonize(progname, LOG_DAEMON, pidfile);
+  info("process ID %lu", (unsigned long)getpid());
+  srand(time(0));			/* don't start the same every time */
+  /* create event loop */
+  ev = ev_new();
+  if(ev_child_setup(ev)) fatal(0, "ev_child_setup failed");
+  /* read config */
+  if(config_read())
+    fatal(0, "cannot read configuration");
+  /* Start the speaker process (as root! - so it can choose its nice value) */
+  speaker_setup(ev);
+  /* set server nice value _after_ starting the speaker, so that they
+   * are independently niceable */
+  xnice(config->nice_server);
+  /* change user */
+  become_mortal();
+  /* make sure we're not root, whatever the config says */
+  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+  /* open a lockfile - we only want one copy of the server to run at once. */
+  if(config->lock) {
+    const char *lockfile;
+    int lockfd;
+   struct flock lock;
+    
+    lockfile = config_get_file("lock");
+    if((lockfd = open(lockfile, O_RDWR|O_CREAT, 0600)) < 0)
+      fatal(errno, "error opening %s", lockfile);
+    cloexec(lockfd);
+    memset(&lock, 0, sizeof lock);
+    lock.l_type = F_WRLCK;
+    lock.l_whence = SEEK_SET;
+    if(fcntl(lockfd, F_SETLK, &lock) < 0)
+      fatal(errno, "error locking %s", lockfile);
+  }
+  /* initialize database environment */
+  trackdb_init(TRACKDB_NORMAL_RECOVER);
+  trackdb_master(ev);
+  /* install new config */
+  reconfigure(ev, 0);
+  /* re-read config if we receive a SIGHUP */
+  if(ev_signal(ev, SIGHUP, handle_sighup, 0)) fatal(0, "ev_signal failed");
+  /* exit on SIGINT/SIGTERM */
+  if(ev_signal(ev, SIGINT, handle_sigint, 0)) fatal(0, "ev_signal failed");
+  if(ev_signal(ev, SIGTERM, handle_sigterm, 0)) fatal(0, "ev_signal failed");
+  /* ignore SIGPIPE */
+  signal(SIGPIPE, SIG_IGN);
+  /* start a rescan straight away */
+  if(initial_rescan)
+    trackdb_rescan(ev);
+  rescan_after(86400);
+  /* periodically tidy up the database */
+  dbgc_after(60);
+  /* periodically check the volume */
+  volumecheck_after(60);
+  /* set initial state */
+  add_random_track();
+  play(ev);
+  /* enter the event loop */
+  n = ev_run(ev);
+  /* if we exit the event loop, something must have gone wrong */
+  fatal(errno, "ev_run returned %d", n);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:eaf909880c8fd3d1fef94ca8f12efe78 */
diff --git a/server/dump.c b/server/dump.c
new file mode 100644
index 0000000..39af755
--- /dev/null
+++ b/server/dump.c
@@ -0,0 +1,458 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "client.h"
+#include "sink.h"
+#include "mem.h"
+#include "defs.h"
+#include "printf.h"
+#include "kvp.h"
+#include "vector.h"
+#include "inputline.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "charset.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "dump", no_argument, 0, 'd' },
+  { "undump", no_argument, 0, 'u' },
+  { "debug", no_argument, 0, 'D' },
+  { "recover", no_argument, 0, 'r' },
+  { "recover-fatal", no_argument, 0, 'R' },
+  { "trackdb", no_argument, 0, 't' },
+  { "searchdb", no_argument, 0, 's' },
+  { "recompute-aliases", no_argument, 0, 'a' },
+  { "remove-pathless", no_argument, 0, 'P' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-dump [OPTIONS] --dump|--undump PATH\n"
+	  "  disorder-dump [OPTIONS] --recompute-aliases\n"
+	  "Options:\n"
+	  "  --help, -h               Display usage message\n"
+	  "  --version, -V            Display version number\n"
+	  "  --config PATH, -c PATH   Set configuration file\n"
+	  "  --dump, -d               Dump state to PATH\n"
+	  "  --undump, -u             Restore state from PATH\n"
+	  "  --recover, -r            Run database recovery\n"
+	  "  --recompute-aliases, -a  Recompute aliases\n"
+	  "  --remove-pathless, -P    Remove pathless tracks\n"
+	  "  --debug                  Debug mode\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-dump version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* dump prefs to FP, return nonzero on error */
+static void do_dump(FILE *fp, const char *tag,
+		    int tracksdb, int searchdb) {
+  DBC *cursor = 0;
+  DB_TXN *tid;
+  struct sink *s = sink_stdio(tag, fp);
+  int err;
+  DBT k, d;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(fseek(fp, 0, SEEK_SET) < 0)
+      fatal(errno, "error calling fseek");
+    if(fflush(fp) < 0)
+      fatal(errno, "error calling fflush");
+    if(ftruncate(fileno(fp), 0) < 0)
+      fatal(errno, "error calling ftruncate");
+    if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
+      fatal(errno, "error writing to %s", tag);
+    cursor = trackdb_opencursor(trackdb_prefsdb, tid);
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                        DB_FIRST);
+    while(err == 0) {
+      if(fputc('P', fp) < 0
+         || urlencode(s, k.data, k.size)
+         || fputc('\n', fp) < 0
+         || urlencode(s, d.data, d.size)
+         || fputc('\n', fp) < 0)
+        fatal(errno, "error writing to %s", tag);
+      err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_NEXT);
+    }
+    if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+    cursor = 0;
+
+    if(tracksdb) {
+      cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+      err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+			  DB_FIRST);
+      while(err == 0) {
+	if(fputc('T', fp) < 0
+	   || urlencode(s, k.data, k.size)
+	   || fputc('\n', fp) < 0
+	   || urlencode(s, d.data, d.size)
+	   || fputc('\n', fp) < 0)
+	  fatal(errno, "error writing to %s", tag);
+	err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+			    DB_NEXT);
+      }
+      if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+      cursor = 0;
+    }
+
+    if(searchdb) {
+      cursor = trackdb_opencursor(trackdb_searchdb, tid);
+      err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+			  DB_FIRST);
+      while(err == 0) {
+	if(fputc('S', fp) < 0
+	   || urlencode(s, k.data, k.size)
+	   || fputc('\n', fp) < 0
+	   || urlencode(s, d.data, d.size)
+	   || fputc('\n', fp) < 0)
+	  fatal(errno, "error writing to %s", tag);
+	err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+			    DB_NEXT);
+      }
+      if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }      cursor = 0;
+    }
+
+    if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag);
+    if(err == DB_LOCK_DEADLOCK) {
+      error(0, "c->c_get: %s", db_strerror(err));
+      goto fail;
+    }
+    if(err && err != DB_NOTFOUND)
+      fatal(0, "cursor->c_get: %s", db_strerror(err));
+    if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+    break;
+fail:
+    trackdb_closecursor(cursor);
+    cursor = 0;
+    info("aborting transaction and retrying dump");
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(fflush(fp) < 0) fatal(errno, "error writing to %s", tag);
+  /* caller might not be paranoid so we are paranoid on their behalf */
+  if(fsync(fileno(fp)) < 0) fatal(errno, "error syncing %s", tag);
+}
+
+/* delete all aliases prefs, return 0 or DB_LOCK_DEADLOCK */
+static int remove_aliases(DB_TXN *tid, int remove_pathless) {
+  DBC *cursor;
+  int err;
+  DBT k, d;
+  struct kvp *data;
+  int alias, pathless;
+
+  info("removing aliases");
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_FIRST)) == DB_LOCK_DEADLOCK) {
+    error(0, "cursor->c_get: %s", db_strerror(err));
+    goto done;
+  }
+  while(err == 0) {
+    data = kvp_urldecode(d.data, d.size);
+    alias = !!kvp_get(data, "_alias_for");
+    pathless = !kvp_get(data, "_path");
+    if(pathless && !remove_pathless)
+      info("no _path for %s", utf82mb(xstrndup(k.data, k.size)));
+    if(alias || (remove_pathless && pathless)) {
+      switch(err = cursor->c_del(cursor, 0)) {
+      case 0: break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "cursor->c_get: %s", db_strerror(err));
+        goto done;
+      default:
+        fatal(0, "cursor->c_del: %s", db_strerror(err));
+      }
+    }
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), DB_NEXT);
+  }
+  if(err == DB_LOCK_DEADLOCK) {
+    error(0, "cursor operation: %s", db_strerror(err));
+    goto done;
+  }
+  if(err != DB_NOTFOUND) fatal(0, "cursor->c_get: %s", db_strerror(err));
+  err = 0;
+done:
+  if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* truncate (i.e. empty) a database, return 0 or DB_LOCK_DEADLOCK */
+static int truncdb(DB_TXN *tid, DB *db) {
+  int err;
+  u_int32_t count;
+
+  switch(err = db->truncate(db, tid, &count, 0)) {
+  case 0: break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "db->truncate: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "db->truncate: %s", db_strerror(err));
+  }
+  return err;
+}
+
+/* read a DBT from FP, return 0 on success or -1 on input error */
+static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
+  char *s;
+  struct dynstr d;
+
+  if(inputline(tag, fp, &s, '\n')) return -1;
+  dynstr_init(&d);
+  if(urldecode(sink_dynstr(&d), s, strlen(s)))
+    fatal(0, "invalid URL-encoded data in %s", tag);
+  dbt->data = d.vec;
+  dbt->size = d.nvec;
+  return 0;
+}
+
+/* undump from FP, return 0 or DB_LOCK_DEADLOCK */
+static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
+  int err, c;
+  DBT k, d;
+
+  info("undumping");
+  if(fseek(fp, 0, SEEK_SET) < 0)
+    fatal(errno, "error calling fseek on %s", tag);
+  if((err = truncdb(tid, trackdb_prefsdb))) return err;
+  if((err = truncdb(tid, trackdb_searchdb))) return err;
+  c = getc(fp);
+  while(!ferror(fp) && !feof(fp)) {
+    switch(c) {
+    case 'V':
+      c = getc(fp);
+      if(c != '0')
+        fatal(0, "unknown version '%c'", c);
+      break;
+    case 'E':
+      return 0;
+    case 'P':
+      if(undump_dbt(fp, tag, prepare_data(&k))
+         || undump_dbt(fp, tag, prepare_data(&d)))
+        break;
+      switch(err = trackdb_prefsdb->put(trackdb_prefsdb, tid, &k, &d, 0)) {
+      case 0:
+        break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "error updating prefs.db: %s", db_strerror(err));
+        return err;
+      default:
+        fatal(0, "error updating prefs.db: %s", db_strerror(err));
+      }
+      break;
+    case 'T':
+    case 'S':
+      if(undump_dbt(fp, tag, prepare_data(&k))
+         || undump_dbt(fp, tag, prepare_data(&d)))
+        break;
+      /* We don't restore the tracks.db or search.db entries, instead
+       * we recompute them */
+      break;
+    case '\n':
+      break;
+    }
+    c = getc(fp);
+  }
+  if(ferror(fp))
+    fatal(errno, "error reading %s", tag);
+  else
+    fatal(0, "unexpected EOF reading %s", tag);
+  return 0;
+}
+
+/* recompute aliases and search database from prefs, return 0 or
+ * DB_LOCK_DEADLOCK */
+static int recompute_aliases(DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  int err;
+  struct kvp *data;
+  const char *path, *track;
+
+  info("recomputing aliases");
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_FIRST)) == DB_LOCK_DEADLOCK) goto done;
+  while(err == 0) {
+    data = kvp_urldecode(d.data, d.size);
+    track = xstrndup(k.data, k.size);
+    if(!kvp_get(data, "_alias_for")) {
+      if(!(path = kvp_get(data, "_path")))
+	error(0, "%s is not an alias but has no path", utf82mb(track));
+      else
+	if((err = trackdb_notice_tid(track, path, tid)) == DB_LOCK_DEADLOCK)
+	  goto done;
+    }
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                        DB_NEXT);
+  }
+  switch(err) {
+  case 0:
+    break;
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    break;
+  default:
+    fatal(0, "cursor->c_get: %s", db_strerror(err));
+  }
+done:
+  if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* restore prefs from FP */
+static void do_undump(FILE *fp, const char *tag, int remove_pathless) {
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(remove_aliases(tid, remove_pathless)
+       || undump_from_fp(tid, fp, tag)
+       || recompute_aliases(tid)) goto fail;
+    break;
+fail:
+    info("aborting transaction and retrying undump");
+    trackdb_abort_transaction(tid);
+  }
+  info("committing undump");
+  trackdb_commit_transaction(tid);
+}
+
+/* just recompute alisaes */
+static void do_recompute(int remove_pathless) {
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(remove_aliases(tid, remove_pathless)
+       || recompute_aliases(tid)) goto fail;
+    break;
+fail:
+    info("aborting transaction and retrying recomputation");
+    trackdb_abort_transaction(tid);
+  }
+  info("committing recomputed aliases");
+  trackdb_commit_transaction(tid);
+}
+
+int main(int argc, char **argv) {
+  int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
+  int tracksdb = 0, searchdb = 0, remove_pathless = 0;
+  const char *path;
+  char *tmp;
+  FILE *fp;
+
+  mem_init(1);
+  while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': dump = 1; break;
+    case 'u': undump = 1; break;
+    case 'D': debugging = 1; break;
+    case 't': tracksdb = 1; break;
+    case 's': searchdb = 1; break;
+    case 'r': recover = TRACKDB_NORMAL_RECOVER;
+    case 'R': recover = TRACKDB_FATAL_RECOVER;
+    case 'a': recompute = 1; break;
+    case 'P': remove_pathless = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(dump + undump + recompute != 1)
+    fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
+  if((undump || recompute) && (tracksdb || searchdb))
+    fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases");
+  if(recompute) {
+    if(optind != argc)
+      fatal(0, "--recompute-aliases does not take a filename");
+    path = 0;
+  } else {
+    if(optind >= argc)
+      fatal(0, "missing dump file name");
+    if(optind + 1 < argc)
+      fatal(0, "specify only a dump file name");
+    path = argv[optind];
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  trackdb_init(recover);
+  trackdb_open();
+  if(dump) {
+    /* we write to a temporary file and rename into place */
+    byte_xasprintf(&tmp, "%s.%lx.tmp", path, (unsigned long)getpid());
+    if(!(fp = fopen(tmp, "w"))) fatal(errno, "error opening %s", tmp);
+    do_dump(fp, tmp, tracksdb, searchdb);
+    if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
+    if(rename(tmp, path) < 0)
+      fatal(errno, "error renaming %s to %s", tmp, path);
+  } else if(undump) {
+    /* the databases or logfiles might end up with wrong permissions
+     * if new ones are created */
+    if(getuid() == 0) info("you might need to chown database files");
+    if(!(fp = fopen(path, "r"))) fatal(errno, "error opening %s", path);
+    do_undump(fp, path, remove_pathless);
+    xfclose(fp);
+  } else if(recompute) {
+    do_recompute(remove_pathless);
+  }
+  trackdb_close();
+  trackdb_deinit();
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a446d6d9fcfbece4e3042e29c148a1cc */
diff --git a/server/play.c b/server/play.c
new file mode 100644
index 0000000..e70e116
--- /dev/null
+++ b/server/play.c
@@ -0,0 +1,725 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "log.h"
+#include "mem.h"
+#include "configuration.h"
+#include "queue.h"
+#include "trackdb.h"
+#include "play.h"
+#include "plugin.h"
+#include "wstat.h"
+#include "eventlog.h"
+#include "logfd.h"
+#include "syscalls.h"
+#include "speaker.h"
+#include "disorder.h"
+#include "signame.h"
+#include "hash.h"
+
+#define SPEAKER "disorder-speaker"
+
+struct queue_entry *playing;
+int paused;
+
+static void finished(ev_source *ev);
+
+static int speaker_fd = -1;
+static hash *player_pids;
+static int shutting_down;
+
+static void store_player_pid(const char *id, pid_t pid) {
+  if(!player_pids) player_pids = hash_new(sizeof (pid_t));
+  hash_add(player_pids, id, &pid, HASH_INSERT_OR_REPLACE);
+}
+
+static pid_t find_player_pid(const char *id) {
+  pid_t *pidp;
+
+  if(player_pids && (pidp = hash_find(player_pids, id))) return *pidp;
+  return -1;
+}
+
+static void forget_player_pid(const char *id) {
+  if(player_pids) hash_remove(player_pids, id);
+}
+
+/* called when speaker process terminates */
+static int speaker_terminated(ev_source attribute((unused)) *ev,
+			      pid_t attribute((unused)) pid,
+			      int attribute((unused)) status,
+			      const struct rusage attribute((unused)) *rusage,
+			      void attribute((unused)) *u) {
+  if(status)
+    error(0, "speaker subprocess terminated with status %s",
+	  wstat(status));
+  return 0;
+}
+
+/* called when speaker process has something to say */
+static int speaker_readable(ev_source *ev, int fd,
+			    void attribute((unused)) *u) {
+  struct speaker_message sm;
+  int ret = speaker_recv(fd, &sm, 0);
+  
+  if(ret < 0) return 0;			/* EAGAIN */
+  if(!ret) {				/* EOF */
+    ev_fd_cancel(ev, ev_read, fd);
+    return 0;
+  }
+  switch(sm.type) {
+  case SM_PAUSED:
+    /* track ID is paused, DATA seconds played */
+    D(("SM_PAUSED %s %ld", sm.id, sm.data));
+    playing->sofar = sm.data;
+    break;
+  case SM_FINISHED:
+    /* the playing track finished */
+    D(("SM_FINISHED %s", sm.id));
+    finished(ev);
+    break;
+  case SM_PLAYING:
+    /* track ID is playing, DATA seconds played */
+    D(("SM_PLAYING %s %ld", sm.id, sm.data));
+    playing->sofar = sm.data;
+    break;
+  default:
+    error(0, "unknown message type %d", sm.type);
+  }
+  return 0;
+}
+
+void speaker_setup(ev_source *ev) {
+  int sp[2], lfd;
+  pid_t pid;
+
+  if(socketpair(PF_UNIX, SOCK_DGRAM, 0, sp) < 0)
+    fatal(errno, "error calling socketpair");
+  if(!isatty(2))
+    lfd = logfd(ev, SPEAKER);
+  else
+    lfd = -1;
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    xdup2(sp[0], 0);
+    xdup2(sp[0], 1);
+    xclose(sp[0]);
+    xclose(sp[1]);
+    if(lfd != -1) {
+      xdup2(lfd, 2);
+      xclose(lfd);
+    }
+    signal(SIGPIPE, SIG_DFL);
+#if 0
+    execlp("valgrind", "valgrind", SPEAKER, "--config", configfile,
+	   debugging ? "--debug" : "--no-debug", (char *)0);
+#else
+    execlp(SPEAKER, SPEAKER, "--config", configfile,
+	   debugging ? "--debug" : "--no-debug", (char *)0);
+#endif
+    fatal(errno, "error invoking %s", SPEAKER);
+  }
+  ev_child(ev, pid, 0, speaker_terminated, 0);
+  speaker_fd = sp[1];
+  xclose(sp[0]);
+  cloexec(speaker_fd);
+  /* Don't need to make speaker_fd nonblocking because speaker_recv() uses
+   * MSG_DONTWAIT. */
+  ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0);
+}
+
+void speaker_reload(void) {
+  struct speaker_message sm;
+
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_RELOAD;
+  speaker_send(speaker_fd, &sm, -1);
+}
+
+/* timeout for play retry */
+static int play_again(ev_source *ev,
+		      const struct timeval attribute((unused)) *now,
+		      void attribute((unused)) *u) {
+  D(("play_again"));
+  play(ev);
+  return 0;
+}
+
+/* try calling play() again after @offset@ seconds */
+static void retry_play(ev_source *ev, int offset) {
+  struct timeval w;
+
+  D(("retry_play(%d)", offset));
+  gettimeofday(&w, 0);
+  w.tv_sec += offset;
+  ev_timeout(ev, 0, &w, play_again, 0);
+}
+
+/* Called when the currently playing track finishes playing.  This
+ * might be because the player finished or because the speaker process
+ * told us so. */
+static void finished(ev_source *ev) {
+  D(("finished playing=%p", (void *)playing));
+  if(!playing)
+    return;
+  if(playing->state != playing_scratched)
+    notify_not_scratched(playing->track, playing->submitter);
+  switch(playing->state) {
+  case playing_ok:
+    eventlog("completed", playing->track, (char *)0);
+    break;
+  case playing_scratched:
+    eventlog("scratched", playing->track, playing->scratched, (char *)0);
+    break;
+  case playing_failed:
+    eventlog("failed", playing->track, wstat(playing->wstat), (char *)0);
+    break;
+  default:
+    break;
+  }
+  queue_played(playing);
+  recent_write();
+  forget_player_pid(playing->id);
+  playing = 0;
+  if(ev) retry_play(ev, config->gap);
+}
+
+/* Called when a player terminates. */
+static int player_finished(ev_source *ev,
+			   pid_t pid,
+			   int status,
+			   const struct rusage attribute((unused)) *rusage,
+			   void *u) {
+  struct queue_entry *q = u;
+
+  D(("player_finished pid=%lu status=%#x",
+     (unsigned long)pid, (unsigned)status));
+  /* Record that this PID is dead.  If we killed the track we might know this
+   * already, but also it might have exited or crashed.  Either way we don't
+   * want to end up signalling it. */
+  if(pid == find_player_pid(q->id))
+    forget_player_pid(q->id);
+  switch(q->state) {
+  case playing_unplayed:
+  case playing_random:
+    /* If this was an SM_PREPARE track then either it failed or we deliberately
+     * stopped it because it was removed from the queue or moved down it.  So
+     * leave it state alone for future use. */
+    break;
+  default:
+    /* We actually started playing this track. */
+    if(status) {
+      if(q->state != playing_scratched)
+	q->state = playing_failed;
+    } else 
+      q->state = playing_ok;
+    break;
+  }
+  /* Regardless we always report and record the status and do cleanup for
+   * prefork calls. */
+  if(status)
+    error(0, "player for %s %s", q->track, wstat(status));
+  if(q->type & DISORDER_PLAYER_PREFORK)
+    play_cleanup(q->pl, q->data);
+  q->wstat = status;
+  /* If this actually was the current track, and does not use the speaker
+   * process, then it must have finished.  For raw-output players we will get a
+   * separate notification from the speaker process. */
+  if(q == playing
+     && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    finished(ev);
+  return 0;
+}
+
+/* Find the player for Q */
+static int find_player(const struct queue_entry *q) {
+  int n;
+  
+  for(n = 0; n < config->player.n; ++n)
+    if(fnmatch(config->player.s[n].s[0], q->track, 0) == 0)
+      break;
+  if(n >= config->player.n)
+    return -1;
+  else
+    return n;
+}
+
+/* Return values from start() */
+#define START_OK 0			/* Succeeded. */
+#define START_HARDFAIL 1		/* Track is broken. */
+#define START_SOFTFAIL 2	   /* Track OK, system (temporarily?) broken */
+
+/* Play or prepare Q */
+static int start(ev_source *ev,
+		 struct queue_entry *q,
+		 int smop) {
+  int n, lfd;
+  const char *p;
+  int sp[2];
+  struct speaker_message sm;
+  char buffer[64];
+  int optc;
+  ao_sample_format format;
+  ao_device *device;
+  int retries;
+  struct timespec ts;
+  const char *waitdevice = 0;
+  const char *const *optv;
+  pid_t pid;
+
+  memset(&sm, 0, sizeof sm);
+  if(find_player_pid(q->id) > 0) {
+    if(smop == SM_PREPARE) return START_OK;
+    /* We have already sent an SM_PREPARE for this track so we just need to
+     * tell the speaker process to start actually playing the queued up audio
+     * data */
+    strcpy(sm.id, q->id);
+    sm.type = SM_PLAY;
+    speaker_send(speaker_fd, &sm, -1);
+    return START_OK;
+  }
+  /* Find the player plugin. */
+  if((n = find_player(q)) < 0) return START_HARDFAIL;
+  if(!(q->pl = open_plugin(config->player.s[n].s[1], 0)))
+    return START_HARDFAIL;
+  q->type = play_get_type(q->pl);
+  /* Can't prepare non-raw tracks. */
+  if(smop == SM_PREPARE
+     && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return START_OK;
+  /* Call the prefork function. */
+  p = trackdb_rawpath(q->track);
+  if(q->type & DISORDER_PLAYER_PREFORK)
+    if(!(q->data = play_prefork(q->pl, p))) {
+      error(0, "prefork function for %s failed", q->track);
+      return START_HARDFAIL;
+    }
+  /* Use the second arg as the tag if available (it's probably a command name),
+   * otherwise the module name. */
+  lfd = logfd(ev, (config->player.s[n].s[2]
+		   ? config->player.s[n].s[2] : config->player.s[n].s[1]));
+  optc = config->player.s[n].n - 2;
+  optv = (void *)&config->player.s[n].s[2];
+  while(optc > 0 && optv[0][0] == '-') {
+    if(!strcmp(optv[0], "--")) {
+      ++optv;
+      --optc;
+      break;
+    }
+    if(!strcmp(optv[0], "--wait-for-device")
+       || !strncmp(optv[0], "--wait-for-device=", 18)) {
+      if((waitdevice = strchr(optv[0], '='))) {
+	++waitdevice;
+      } else
+	waitdevice = "";		/* use default */
+      ++optv;
+      --optc;
+    } else {
+      error(0, "unknown option %s", optv[0]);
+      return START_HARDFAIL;
+    }
+  }
+  switch(pid = fork()) {
+  case 0:			/* child */
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    signal(SIGPIPE, SIG_DFL);
+    xdup2(lfd, 1);
+    xdup2(lfd, 2);
+    xclose(lfd);			/* tidy up */
+    setpgid(0, 0);
+    if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+      /* Raw format players write down a pipe (in fact a socket) to
+       * the speaker process. */
+      sm.type = smop;
+      strcpy(sm.id, q->id);
+      if(socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0)
+	fatal(errno, "error calling socketpair");
+      xshutdown(sp[0], SHUT_WR);
+      xshutdown(sp[1], SHUT_RD);
+      speaker_send(speaker_fd, &sm, sp[0]);
+      /* Pass the file descriptor to the driver in an environment
+       * variable. */
+      snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", sp[1]);
+      if(putenv(buffer) < 0)
+	fatal(errno, "error calling putenv");
+      xclose(sp[0]);
+    }
+    if(waitdevice) {
+      ao_initialize();
+      if(*waitdevice) {
+	n = ao_driver_id(waitdevice);
+	if(n == -1)
+	  fatal(0, "invalid libao driver: %s", optv[0]);
+	} else
+	  n = ao_default_driver_id();
+      /* Make up a format. */
+      memset(&format, 0, sizeof format);
+      format.bits = 8;
+      format.rate = 44100;
+      format.channels = 1;
+      format.byte_format = AO_FMT_NATIVE;
+      retries = 20;
+      ts.tv_sec = 0;
+      ts.tv_nsec = 100000000;	/* 0.1s */
+      while((device = ao_open_live(n, &format, 0)) == 0 && retries-- > 0)
+	  nanosleep(&ts, 0);
+      if(device)
+	ao_close(device);
+    }
+    play_track(q->pl,
+	       optv, optc,
+	       p,
+	       q->track);
+    _exit(0);
+  case -1:			/* error */
+    error(errno, "error calling fork");
+    if(q->type & DISORDER_PLAYER_PREFORK)
+      play_cleanup(q->pl, q->data);	/* else would leak */
+    xclose(lfd);
+    return START_SOFTFAIL;
+  }
+  store_player_pid(q->id, pid);
+  xclose(lfd);
+  setpgid(pid, pid);
+  ev_child(ev, pid, 0, player_finished, q);
+  D(("player subprocess ID %lu", (unsigned long)pid));
+  return START_OK;
+}
+
+int prepare(ev_source *ev,
+	    struct queue_entry *q) {
+  int n;
+
+  /* Find the player plugin */
+  if(find_player_pid(q->id) > 0) return 0; /* Already going. */
+  if((n = find_player(q)) < 0) return -1; /* No player */
+  q->pl = open_plugin(config->player.s[n].s[1], 0); /* No player */
+  q->type = play_get_type(q->pl);
+  if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return 0;				/* Not a raw player */
+  return start(ev, q, SM_PREPARE);	/* Prepare it */
+}
+
+void abandon(ev_source attribute((unused)) *ev,
+	     struct queue_entry *q) {
+  struct speaker_message sm;
+  pid_t pid = find_player_pid(q->id);
+
+  if(pid < 0) return;			/* Not prepared. */
+  if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+    return;				/* Not a raw player. */
+  /* Terminate the player. */
+  kill(-pid, config->signal);
+  forget_player_pid(q->id);
+  /* Cancel the track. */
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_CANCEL;
+  strcpy(sm.id, q->id);
+  speaker_send(speaker_fd, &sm, -1);
+}
+
+int add_random_track(void) {
+  struct queue_entry *q;
+  const char *p;
+
+  /* If random play is not enabled then do nothing. */
+  if(shutting_down || !random_is_enabled())
+    return 0;
+  /* If there is already a random track, do nothing. */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if(q->state == playing_random)
+      return 0;
+  /* Try to pick a random track */
+  if(!(p = trackdb_random(16)))
+    return -1;
+  /* Add it to the end of the queue. */
+  q = queue_add(p, 0, WHERE_END);
+  q->state = playing_random;
+  /* Commit the queue */
+  queue_write();
+  D(("picked %p (%s) at random", (void *)q, q->track));
+  return 0;
+}
+
+/* try to play a track */
+void play(ev_source *ev) {
+  struct queue_entry *q;
+  int random_enabled = random_is_enabled();
+
+  D(("play playing=%p", (void *)playing));
+  if(shutting_down || playing || !playing_is_enabled()) return;
+  /* If the queue is empty then add a random track. */
+  if(qhead.next == &qhead) {
+    if(!random_enabled)
+      return;
+    if(add_random_track()) {
+      /* On error, try again in 10s. */
+      retry_play(ev, 10);
+      return;
+    }
+    /* Now there must be at least one track in the queue. */
+  }
+  q = qhead.next;
+  /* If random play is disabled but the track is a random one then don't play
+   * it.  play() will be called again when random play is re-enabled. */
+  if(!random_enabled && q->state == playing_random)
+    return;
+  D(("taken %p (%s) from queue", (void *)q, q->track));
+  /* Try to start playing. */
+  switch(start(ev, q, SM_PLAY)) {
+  case START_HARDFAIL:
+    if(q == qhead.next) {
+      queue_remove(q, 0);		/* Abandon this track. */
+      queue_played(q);
+      recent_write();
+    }
+    if(qhead.next == &qhead)
+      /* Queue is empty, wait a bit before trying something else (so we don't
+       * sit there looping madly in the presence of persistent problem).  Note
+       * that we might not reliably get a random track lookahead in this case,
+       * but if we get here then really there are bigger problems. */
+      retry_play(ev, 1);
+    else
+      /* More in queue, try again now. */
+      play(ev);
+    break;
+  case START_SOFTFAIL:
+    /* Try same track again in a bit. */
+    retry_play(ev, 10);
+    break;
+  case START_OK:
+    if(q == qhead.next) {
+      queue_remove(q, 0);
+      queue_write();
+    }
+    playing = q;
+    time(&playing->played);
+    playing->state = playing_started;
+    notify_play(playing->track, playing->submitter);
+    eventlog("playing", playing->track,
+	     playing->submitter ? playing->submitter : (const char *)0,
+	     (const char *)0);
+    /* Maybe add a random track. */
+    add_random_track();
+    /* If there is another track in the queue prepare it now.  This could
+     * potentially be a just-added random track. */
+    if(qhead.next != &qhead)
+      prepare(ev, qhead.next);
+    break;
+  }
+}
+
+int playing_is_enabled(void) {
+  const char *s = trackdb_get_global("playing");
+
+  return !s || !strcmp(s, "yes");
+}
+
+void enable_playing(const char *who, ev_source *ev) {
+  trackdb_set_global("playing", "yes", who);
+  /* Add a random track if necessary. */
+  add_random_track();
+  play(ev);
+}
+
+void disable_playing(const char *who) {
+  trackdb_set_global("playing", "no", who);
+}
+
+int random_is_enabled(void) {
+  const char *s = trackdb_get_global("random-play");
+
+  return !s || !strcmp(s, "yes");
+}
+
+void enable_random(const char *who, ev_source *ev) {
+  trackdb_set_global("random-play", "yes", who);
+  add_random_track();
+  play(ev);
+}
+
+void disable_random(const char *who) {
+  trackdb_set_global("random-play", "no", who);
+}
+
+void scratch(const char *who, const char *id) {
+  struct queue_entry *q;
+  struct speaker_message sm;
+  pid_t pid;
+
+  D(("scratch playing=%p state=%d id=%s playing->id=%s",
+     (void *)playing,
+     playing ? playing->state : 0,
+     id ? id : "(none)",
+     playing ? playing->id : "(none)"));
+  if(playing
+     && (playing->state == playing_started
+	 || playing->state == playing_paused)
+     && (!id
+	 || !strcmp(id, playing->id))) {
+    playing->state = playing_scratched;
+    playing->scratched = who ? xstrdup(who) : 0;
+    if((pid = find_player_pid(playing->id)) > 0) {
+      D(("kill -%d %lu", config->signal, (unsigned long)pid));
+      kill(-pid, config->signal);
+      forget_player_pid(playing->id);
+    } else
+      error(0, "could not find PID for %s", playing->id);
+    if((playing->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+      memset(&sm, 0, sizeof sm);
+      sm.type = SM_CANCEL;
+      strcpy(sm.id, playing->id);
+      speaker_send(speaker_fd, &sm, -1);
+      D(("sending SM_CANCEL for %s", playing->id));
+    }
+    /* put a scratch track onto the front of the queue (but don't
+     * bother if playing is disabled) */
+    if(playing_is_enabled() && config->scratch.n) {
+      int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+      q = queue_add(config->scratch.s[r], who, WHERE_START);
+      q->state = playing_isscratch;
+    }
+    notify_scratch(playing->track, playing->submitter, who,
+		   time(0) - playing->played);
+  }
+}
+
+void quitting(ev_source *ev) {
+  struct queue_entry *q;
+  pid_t pid;
+
+  /* Don't start anything new */
+  shutting_down = 1;
+  /* Shut down the current player */
+  if(playing) {
+    if((pid = find_player_pid(playing->id)) > 0) {
+      kill(-pid, config->signal);
+      forget_player_pid(playing->id);
+    } else
+      error(0, "could not find PID for %s", playing->id);
+    playing->state = playing_quitting;
+    finished(0);
+  }
+  /* Zap any other players */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if((pid = find_player_pid(q->id)) > 0) {
+      D(("kill -%d %lu", config->signal, (unsigned long)pid));
+      kill(-pid, config->signal);
+      forget_player_pid(q->id);
+    } else
+      error(0, "could not find PID for %s", q->id);
+  /* Don't need the speaker any more */
+  ev_fd_cancel(ev, ev_read, speaker_fd);
+  xclose(speaker_fd);
+}
+
+int pause_playing(const char *who) {
+  struct speaker_message sm;
+  long played;
+  
+  /* Can't pause if already paused or if nothing playing. */
+  if(!playing || paused) return 0;
+  switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+  case DISORDER_PLAYER_STANDALONE:
+    if(!(playing->type & DISORDER_PLAYER_PAUSES)) {
+    default:
+      error(0,  "cannot pause because player is not powerful enough");
+      return -1;
+    }
+    if(play_pause(playing->pl, &played, playing->data)) {
+      error(0, "player indicates it cannot pause");
+      return -1;
+    }
+    time(&playing->lastpaused);
+    playing->uptopause = played;
+    playing->lastresumed = 0;
+    break;
+  case DISORDER_PLAYER_RAW:
+    memset(&sm, 0, sizeof sm);
+    sm.type = SM_PAUSE;
+    speaker_send(speaker_fd, &sm, -1);
+    break;
+  }
+  if(who) info("paused by %s", who);
+  notify_pause(playing->track, who);
+  paused = 1;
+  if(playing->state == playing_started)
+    playing->state = playing_paused;
+  eventlog("state", "pause", (char *)0);
+  return 0;
+}
+
+void resume_playing(const char *who) {
+  struct speaker_message sm;
+
+  if(!paused) return;
+  paused = 0;
+  if(!playing) return;
+  switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+  case DISORDER_PLAYER_STANDALONE:
+    if(!playing->type & DISORDER_PLAYER_PAUSES) {
+    default:
+      /* Shouldn't happen */
+      return;
+    }
+    play_resume(playing->pl, playing->data);
+    time(&playing->lastresumed);
+    break;
+  case DISORDER_PLAYER_RAW:
+    memset(&sm, 0, sizeof sm);
+    sm.type = SM_RESUME;
+    speaker_send(speaker_fd, &sm, -1);
+    break;
+  }
+  if(who) info("resumed by %s", who);
+  notify_resume(playing->track, who);
+  if(playing->state == playing_paused)
+    playing->state = playing_started;
+  eventlog("state", "resume", (char *)0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:17f4e83cce4cbaa60a122475379e63f1 */
diff --git a/server/play.h b/server/play.h
new file mode 100644
index 0000000..396ff56
--- /dev/null
+++ b/server/play.h
@@ -0,0 +1,91 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef PLAY_H
+#define PLAY_H
+
+extern struct queue_entry *playing;	/* playing track or 0 */
+extern int paused;			/* non-0 if paused */
+
+void play(ev_source *ev);
+/* try to play something, if playing is enabled and nothing is playing
+ * already */
+
+int playing_is_enabled(void);
+/* return true iff playing is enabled */
+
+void enable_playing(const char *who, ev_source *ev);
+/* enable playing */
+
+void disable_playing(const char *who);
+/* disable playing. */
+
+int random_is_enabled(void);
+/* return true iff random play is enabled */
+
+void enable_random(const char *who, ev_source *ev);
+/* enable random play */
+
+void disable_random(const char *who);
+/* disable random play */
+
+void scratch(const char *who, const char *id);
+/* scratch the playing track.  @who@ identifies the scratcher. @id@ is
+ * the ID or a null pointer. */
+
+void quitting(ev_source *ev);
+/* called to terminate current track and shut down speaker process
+ * when quitting */
+
+void speaker_setup(ev_source *ev);
+/* set up the speaker subprocess */
+
+void speaker_reload(void);
+/* Tell the speaker process to reload its configuration. */
+
+int pause_playing(const char *who);
+/* Pause the current track.  Return 0 on success, -1 on error.  WHO
+ * can be 0. */
+
+void resume_playing(const char *who);
+/* Resume after a pause.  WHO can be 0. */
+
+int prepare(ev_source *ev,
+	    struct queue_entry *q);
+/* Prepare to play Q */
+
+void abandon(ev_source *ev,
+	     struct queue_entry *q);
+/* Abandon a possibly-prepared track. */
+
+int add_random_track(void);
+/* If random play is enabled then try to add a track to the queue.  On success
+ * (including deliberartely doing nothing) return 0.  On error return -1. */
+
+#endif /* PLAY_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:2bcb00c5b004ce3e91785adb8893e8de */
diff --git a/server/rescan.c b/server/rescan.c
new file mode 100644
index 0000000..0c1e6de
--- /dev/null
+++ b/server/rescan.c
@@ -0,0 +1,331 @@
+/*
+ * This file is part of DisOrder 
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "plugin.h"
+#include "inputline.h"
+#include "charset.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "printf.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static DB_TXN *global_tid;
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-rescan [OPTIONS] [PATH...]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Rescanner for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-rescan version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+static volatile sig_atomic_t signalled;
+
+static void signal_handler(int sig) {
+  if(sig == 0) _exit(-1);               /* "Cannot happen" */
+  signalled = sig;
+}
+
+static int aborted(void) {
+  return signalled || getppid() == 1;
+}
+
+/* Exit if our parent has gone away or we have been told to stop. */
+static void checkabort(void) {
+  if(getppid() == 1) {
+    info("parent has terminated");
+    trackdb_abort_transaction(global_tid);
+    exit(0);
+  }
+  if(signalled) {
+    info("received signal %d", signalled);
+    trackdb_abort_transaction(global_tid);
+    exit(0);
+  }
+}
+
+/* rescan a collection */
+static void rescan_collection(const struct collection *c) {
+  pid_t pid, r;
+  int p[2], n, w;
+  FILE *fp = 0;
+  char *path, *track;
+  long ntracks = 0, nnew = 0;
+  
+  checkabort();
+  info("rescanning %s with %s", c->root, c->module);
+  /* plugin runs in a subprocess */
+  xpipe(p);
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    xclose(p[0]);
+    xdup2(p[1], 1);
+    xclose(p[1]);
+    scan(c->module, c->root);
+    if(fflush(stdout) < 0)
+      fatal(errno, "error writing to scanner pipe");
+    _exit(0);
+  }
+  xclose(p[1]);
+  if(!(fp = fdopen(p[0], "r")))
+    fatal(errno, "error calling fdopen");
+  /* read tracks from the plugin */
+  while(!inputline("rescanner", fp, &path, 0)) {
+    checkabort();
+    /* actually we can cope relatively well within the server, but they'll go
+     * wrong in track listings */
+    if(strchr(path, '\n')) {
+      error(0, "cannot cope with tracks with newlines in the name");
+      continue;
+    }
+    if(!(track = any2utf8(c->encoding, path))) {
+      error(0, "cannot convert track path to UTF-8: %s", path);
+      continue;
+    }
+    D(("track %s", track));
+    /* only tracks with a known player are admitted */
+    for(n = 0; (n < config->player.n
+		&& fnmatch(config->player.s[n].s[0], track, 0) != 0); ++n)
+      ;
+    if(n < config->player.n) {
+      nnew += !!trackdb_notice(track, path);
+      ++ntracks;
+    }
+  }
+  /* tidy up */
+  if(ferror(fp)) {
+    error(errno, "error reading from scanner pipe");
+    goto done;
+  }
+  xfclose(fp);
+  fp = 0;
+  while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+    ;
+  if(r < 0) fatal(errno, "error calling waitpid");
+  pid = 0;
+  if(w) {
+    error(0, "scanner subprocess: %s", wstat(w));
+    goto done;
+  }
+  info("rescanned %s, %ld tracks, %ld new", c->root, ntracks, nnew);
+done:
+  if(fp)
+    xfclose(fp);
+  if(pid)
+    while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+      ;
+}
+
+struct recheck_state {
+  const struct collection *c;
+  long nobsolete, nlength;
+};
+
+/* called for each non-alias track */
+static int recheck_callback(const char *track,
+                            struct kvp *data,
+                            void *u,
+                            DB_TXN *tid) {
+  struct recheck_state *cs = u;
+  const struct collection *c = cs->c;
+  const char *path = kvp_get(data, "_path");
+  char buffer[20];
+  int err;
+  long n;
+
+  if(aborted()) return EINTR;
+  D(("rechecking %s", track));
+  /* see if the track has evaporated */
+  if(check(c->module, c->root, path) == 0) {
+    D(("obsoleting %s", track));
+    if((err = trackdb_obsolete(track, tid))) return err;
+    ++cs->nobsolete;
+    return 0;
+  }
+  /* make sure we know the length */
+  if(!kvp_get(data, "_length")) {
+    D(("recalculating length of %s", track));
+    n = tracklength(track, path);
+    if(n > 0) {
+      byte_snprintf(buffer, sizeof buffer, "%ld", n);
+      kvp_set(&data, "_length", buffer);
+      if((err = trackdb_putdata(trackdb_tracksdb, track, data, tid, 0)))
+        return err;
+      ++cs->nlength;
+    }
+  }
+  return 0;
+}
+
+/* recheck a collection */
+static void recheck_collection(const struct collection *c) {
+  struct recheck_state cs;
+
+  info("rechecking %s", c->root);
+  for(;;) {
+    checkabort();
+    global_tid = trackdb_begin_transaction();
+    memset(&cs, 0, sizeof cs);
+    cs.c = c;
+    if(trackdb_scan(c->root, recheck_callback, &cs, global_tid)) goto fail;
+    break;
+  fail:
+    /* Maybe we need to shut down */
+    checkabort();
+    /* Abort the transaction and try again in a bit. */
+    trackdb_abort_transaction(global_tid);
+    global_tid = 0;
+    /* Let anything else that is going on get out of the way. */
+    sleep(10);
+    checkabort();
+    info("resuming recheck of %s", c->root);
+  }
+  trackdb_commit_transaction(global_tid);
+  global_tid = 0;
+  info("rechecked %s, %ld obsoleted, %ld lengths calculated",
+       c->root, cs.nobsolete, cs.nlength);
+}
+
+/* rescan/recheck a collection by name */
+static void do_directory(const char *s,
+			 void (*fn)(const struct collection *c)) {
+  int n;
+  
+  for(n = 0; (n < config->collection.n
+	      && strcmp(config->collection.s[n].root, s)); ++n)
+    ;
+  if(n < config->collection.n)
+    fn(&config->collection.s[n]);
+  else
+    error(0, "no collection has root '%s'", s);
+}
+
+/* rescan/recheck all collections */
+static void do_all(void (*fn)(const struct collection *c)) {
+  int n;
+
+  for(n = 0; n < config->collection.n; ++n)
+    fn(&config->collection.s[n]);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  struct sigaction sa;
+  
+  set_progname(argv);
+  mem_init(1);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  /* If stderr is a TTY then log there, otherwise to syslog. */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  xnice(config->nice_rescan);
+  sa.sa_handler = signal_handler;
+  sa.sa_flags = SA_RESTART;
+  sigemptyset(&sa.sa_mask);
+  xsigaction(SIGTERM, &sa, 0);
+  xsigaction(SIGINT, &sa, 0);
+  info("started");
+  trackdb_init(0);
+  trackdb_open();
+  if(optind == argc) {
+    do_all(rescan_collection);
+    do_all(recheck_collection);
+  }
+  else {
+    for(n = optind; n < argc; ++n)
+      do_directory(argv[n], rescan_collection);
+    for(n = optind; n < argc; ++n)
+      do_directory(argv[n], recheck_collection);
+  }
+  trackdb_close();
+  trackdb_deinit();
+  info("completed");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:yBYnk5wX3mDtVI1MI3LlWg */
diff --git a/server/server.c b/server/server.c
new file mode 100644
index 0000000..89ff5ab
--- /dev/null
+++ b/server/server.c
@@ -0,0 +1,1105 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "server.h"
+#include "syscalls.h"
+#include "queue.h"
+#include "play.h"
+#include "log.h"
+#include "mem.h"
+#include "state.h"
+#include "charset.h"
+#include "split.h"
+#include "configuration.h"
+#include "hex.h"
+#include "trackdb.h"
+#include "table.h"
+#include "kvp.h"
+#include "mixer.h"
+#include "sink.h"
+#include "authhash.h"
+#include "plugin.h"
+#include "printf.h"
+#include "trackname.h"
+#include "eventlog.h"
+#include "defs.h"
+#include "cache.h"
+
+#ifndef NONCE_SIZE
+# define NONCE_SIZE 16
+#endif
+
+int volume_left, volume_right;		/* last known volume */
+
+struct listener {
+  const char *name;
+  int pf;
+};
+
+struct conn {
+  ev_reader *r;
+  ev_writer *w;
+  int fd;
+  unsigned tag;
+  char *who;
+  ev_source *ev;
+  unsigned char nonce[NONCE_SIZE];
+  ev_reader_callback *reader;
+  struct eventlog_output *lo;
+  const struct listener *l;
+};
+
+static int reader_callback(ev_source *ev,
+			   ev_reader *reader,
+			   int fd,
+			   void *ptr,
+			   size_t bytes,
+			   int eof,
+			   void *u);
+
+static const char *noyes[] = { "no", "yes" };
+
+static int writer_error(ev_source attribute((unused)) *ev,
+			int fd,
+			int errno_value,
+			void *u) {
+  struct conn *c = u;
+
+  D(("server writer_error %d %d", fd, errno_value));
+  if(errno_value == 0) {
+    /* writer is done */
+    c->w = 0;
+    if(c->r == 0) {
+      D(("server writer_error closes %d", fd));
+      xclose(fd);		/* reader is done too, close */
+    } else {
+      D(("server writer_error shutdown %d SHUT_WR", fd));
+      xshutdown(fd, SHUT_WR);	/* reader is not done yet */
+    }
+  } else {
+    if(errno_value != EPIPE)
+      error(errno_value, "S%x write error on socket", c->tag);
+    if(c->r)
+      ev_reader_cancel(c->r);
+    xclose(fd);
+  }
+  return 0;
+}
+
+static int reader_error(ev_source attribute((unused)) *ev,
+			int fd,
+			int errno_value,
+			void *u) {
+  struct conn *c = u;
+
+  D(("server reader_error %d %d", fd, errno_value));
+  error(errno, "S%x read error on socket", c->tag);
+  ev_writer_cancel(c->w);
+  xclose(fd);
+  return 0;
+}
+
+/* return true if we are talking to a trusted user */
+static int trusted(struct conn *c) {
+  int n;
+  
+  for(n = 0; (n < config->trust.n
+	      && strcmp(config->trust.s[n], c->who)); ++n)
+    ;
+  return n < config->trust.n;
+}
+
+static int c_disable(struct conn *c, char **vec, int nvec) {
+  if(nvec == 0)
+    disable_playing(c->who);
+  else if(nvec == 1 && !strcmp(vec[0], "now"))
+    disable_playing(c->who);
+  else {
+    sink_writes(ev_writer_sink(c->w), "550 invalid argument\n");
+    return 1;			/* completed */
+  }
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_enable(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  enable_playing(c->who, c->ev);
+  /* Enable implicitly unpauses if there is nothing playing */
+  if(paused && !playing) resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_enabled(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[playing_is_enabled()]);
+  return 1;			/* completed */
+}
+
+static int c_play(struct conn *c, char **vec,
+		  int attribute((unused)) nvec) {
+  const char *track;
+  struct queue_entry *q;
+  
+  if(!trackdb_exists(vec[0])) {
+    sink_writes(ev_writer_sink(c->w), "550 track is not in database\n");
+    return 1;
+  }
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  q = queue_add(track, c->who, WHERE_BEFORE_RANDOM);
+  queue_write();
+  /* If we added the first track, and something is playing, then prepare the
+   * new track.  If nothing is playing then we don't bother as it wouldn't gain
+   * anything. */
+  if(q == qhead.next && playing)
+    prepare(c->ev, q);
+  sink_writes(ev_writer_sink(c->w), "250 queued\n");
+  /* If the queue was empty but we are for some reason paused then
+   * unpause. */
+  if(!playing) resume_playing(0);
+  play(c->ev);
+  return 1;			/* completed */
+}
+
+static int c_remove(struct conn *c, char **vec,
+		    int attribute((unused)) nvec) {
+  struct queue_entry *q;
+
+  if(!(q = queue_find(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+    return 1;
+  }
+  if(config->restrictions & RESTRICT_REMOVE) {
+    /* can only remove tracks that you submitted */
+    if(!q->submitter || strcmp(q->submitter, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+      return 1;
+    }
+  }
+  queue_remove(q, c->who);
+  /* De-prepare the track. */
+  abandon(c->ev, q);
+  /* If we removed the random track then add another one. */
+  if(q->state == playing_random)
+    add_random_track();
+  /* Prepare whatever the next head track is. */
+  if(qhead.next != &qhead)
+    prepare(c->ev, qhead.next);
+  queue_write();
+  sink_writes(ev_writer_sink(c->w), "250 removed\n");
+  return 1;			/* completed */
+}
+
+static int c_scratch(struct conn *c,
+		     char **vec,
+		     int nvec) {
+  if(!playing) {
+    sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+    return 1;			/* completed */
+  }
+  if(config->restrictions & RESTRICT_SCRATCH) {
+    /* can only scratch tracks you submitted and randomly selected ones */
+    if(playing->submitter && strcmp(playing->submitter, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+      return 1;
+    }
+  }
+  scratch(c->who, nvec == 1 ? vec[0] : 0);
+  /* If you scratch an unpaused track then it is automatically unpaused */
+  resume_playing(0);
+  sink_writes(ev_writer_sink(c->w), "250 scratched\n");
+  return 1;			/* completed */
+}
+
+static int c_pause(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  if(!playing) {
+    sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+    return 1;			/* completed */
+  }
+  if(paused) {
+    sink_writes(ev_writer_sink(c->w), "250 already paused\n");
+    return 1;			/* completed */
+  }
+  if(pause_playing(c->who) < 0)
+    sink_writes(ev_writer_sink(c->w), "550 cannot pause this track\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 paused\n");
+  return 1;
+}
+
+static int c_resume(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  if(!paused) {
+    sink_writes(ev_writer_sink(c->w), "250 not paused\n");
+    return 1;			/* completed */
+  }
+  resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 paused\n");
+  return 1;
+}
+
+static int c_shutdown(struct conn *c,
+		      char attribute((unused)) **vec,
+		      int attribute((unused)) nvec) {
+  info("S%x shut down by %s", c->tag, c->who);
+  sink_writes(ev_writer_sink(c->w), "250 shutting down\n");
+  ev_writer_flush(c->w);
+  quit(c->ev);
+}
+
+static int c_reconfigure(struct conn *c,
+			 char attribute((unused)) **vec,
+			 int attribute((unused)) nvec) {
+  info("S%x reconfigure by %s", c->tag, c->who);
+  if(reconfigure(c->ev, 1))
+    sink_writes(ev_writer_sink(c->w), "550 error reading new config\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 installed new config\n");
+  return 1;				/* completed */
+}
+
+static int c_rescan(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  info("S%x rescan by %s", c->tag, c->who);
+  trackdb_rescan(c->ev);
+  sink_writes(ev_writer_sink(c->w), "250 initiated rescan\n");
+  return 1;				/* completed */
+}
+
+static int c_version(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  /* VERSION had better only use the basic character set */
+  sink_printf(ev_writer_sink(c->w), "251 %s\n", disorder_version_string);
+  return 1;			/* completed */
+}
+
+static int c_playing(struct conn *c,
+		     char attribute((unused)) **vec,
+		     int attribute((unused)) nvec) {
+  if(playing) {
+    queue_fix_sofar(playing);
+    playing->expected = 0;
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", queue_marshall(playing));
+  } else
+    sink_printf(ev_writer_sink(c->w), "259 nothing playing\n");
+  return 1;				/* completed */
+}
+
+static int c_become(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  c->who = vec[0];
+  sink_writes(ev_writer_sink(c->w), "230 OK\n");
+  return 1;
+}
+
+static int c_user(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  int n;
+  const char *res;
+  union {
+    struct sockaddr sa;
+    struct sockaddr_in in;
+    struct sockaddr_in6 in6;
+  } u;
+  socklen_t l;
+  char host[1024];
+
+  if(c->who) {
+    sink_writes(ev_writer_sink(c->w), "530 already authenticated\n");
+    return 1;
+  }
+  /* get connection data */
+  l = sizeof u;
+  if(getpeername(c->fd, &u.sa, &l) < 0) {
+    error(errno, "S%x error calling getpeername", c->tag);
+    sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+    return 1;
+  }
+  if(c->l->pf != PF_UNIX) {
+    if((n = getnameinfo(&u.sa, l,
+			host, sizeof host, 0, 0, NI_NUMERICHOST))) {
+      error(0, "S%x error calling getnameinfo: %s", c->tag, gai_strerror(n));
+      sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+      return 1;
+    }
+  }
+  /* find the user */
+  for(n = 0; n < config->allow.n
+	&& strcmp(config->allow.s[n].s[0], vec[0]); ++n)
+    ;
+  /* if it's a real user check whether the response is right */
+  if(n < config->allow.n) {
+    res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1]);
+    if(res && !strcmp(res, vec[1])) {
+      c->who = vec[0];
+      /* currently we only bother logging remote connections */
+      if(c->l->pf != PF_UNIX)
+	info("S%x %s connected from %s", c->tag, vec[0], host);
+      sink_writes(ev_writer_sink(c->w), "230 OK\n");
+      return 1;
+    }
+  }
+  /* oops, response was wrong */
+  if(c->l->pf != PF_UNIX)
+    info("S%x authentication failure for %s from %s", c->tag, vec[0], host);
+  else
+    info("S%x authentication failure for %s", c->tag, vec[0]);
+  sink_writes(ev_writer_sink(c->w), "530 authentication failed\n");
+  return 1;
+}
+
+static int c_recent(struct conn *c,
+		    char attribute((unused)) **vec,
+		    int attribute((unused)) nvec) {
+  const struct queue_entry *q;
+
+  sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+  for(q = phead.next; q != &phead; q = q->next)
+    sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+}
+
+static int c_queue(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  struct queue_entry *q;
+  time_t when = 0;
+  const char *l;
+  long length;
+
+  sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+  if(playing_is_enabled() && !paused) {
+    if(playing) {
+      queue_fix_sofar(playing);
+      if((l = trackdb_get(playing->track, "_length"))
+	 && (length = atol(l))) {
+	time(&when);
+	when += length - playing->sofar + config->gap;
+      }
+    } else
+      /* Nothing is playing but playing is enabled, so whatever is
+       * first in the queue can be expected to start immediately. */
+      time(&when);
+  }
+  for(q = qhead.next; q != &qhead; q = q->next) {
+    /* fill in estimated start time */
+    q->expected = when;
+    sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+    /* update for next track */
+    if(when) {
+      if((l = trackdb_get(q->track, "_length"))
+	 && (length = atol(l)))
+	when += length + config->gap;
+      else
+	when = 0;
+    }
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+}
+
+static int output_list(struct conn *c, char **vec) {
+  while(*vec)
+    sink_printf(ev_writer_sink(c->w), "%s\n", *vec++);
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int files_dirs(struct conn *c,
+		      char **vec,
+		      int nvec,
+		      enum trackdb_listable what) {
+  const char *dir, *re, *errstr;
+  int erroffset;
+  pcre *rec;
+  char **fvec, *key;
+  
+  switch(nvec) {
+  case 0: dir = 0; re = 0; break;
+  case 1: dir = vec[0]; re = 0; break;
+  case 2: dir = vec[0]; re = vec[1]; break;
+  default: abort();
+  }
+  /* A bit of a bodge to make sure the args don't trample on cache keys */
+  if(dir && strchr(dir, '\n')) {
+    sink_writes(ev_writer_sink(c->w), "550 invalid directory name\n");
+    return 1;
+  }
+  if(re && strchr(re, '\n')) {
+    sink_writes(ev_writer_sink(c->w), "550 invalid regexp\n");
+    return 1;
+  }
+  /* We bother eliminating "" because the web interface is relatively
+   * likely to send it */
+  if(re && *re) {
+    byte_xasprintf(&key, "%d\n%s\n%s", (int)what, dir ? dir : "", re);
+    fvec = (char **)cache_get(&cache_files_type, key);
+    if(fvec) {
+      /* Got a cache hit, don't store the answer in the cache */
+      key = 0;
+      ++cache_files_hits;
+      rec = 0;				/* quieten compiler */
+    } else {
+      /* Cache miss, we'll do the lookup and key != 0 so we'll store the answer
+       * in the cache. */
+      if(!(rec = pcre_compile(re, PCRE_CASELESS|PCRE_UTF8,
+			      &errstr, &erroffset, 0))) {
+	sink_printf(ev_writer_sink(c->w), "550 Error compiling regexp: %s\n",
+		    errstr);
+	return 1;
+      }
+      /* It only counts as a miss if the regexp was valid. */
+      ++cache_files_misses;
+    }
+  } else {
+    /* No regexp, don't bother caching the result */
+    rec = 0;
+    key = 0;
+    fvec = 0;
+  }
+  if(!fvec) {
+    /* No cache hit (either because a miss, or because we did not look) so do
+     * the lookup */
+    if(dir && *dir)
+      fvec = trackdb_list(dir, 0, what, rec);
+    else
+      fvec = trackdb_list(0, 0, what, rec);
+  }
+  if(key)
+    /* Put the answer in the cache */
+    cache_put(&cache_files_type, key, fvec);
+  sink_writes(ev_writer_sink(c->w), "253 Listing follow\n");
+  return output_list(c, fvec);
+}
+
+static int c_files(struct conn *c,
+		  char **vec,
+		  int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_files);
+}
+
+static int c_dirs(struct conn *c,
+		  char **vec,
+		  int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_directories);
+}
+
+static int c_allfiles(struct conn *c,
+		      char **vec,
+		      int nvec) {
+  return files_dirs(c, vec, nvec, trackdb_directories|trackdb_files);
+}
+
+static int c_get(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  const char *v;
+
+  if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_length(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  const char *track, *v;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if((v = trackdb_get(track, "_length")))
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_set(struct conn *c,
+		 char **vec,
+		 int attribute((unused)) nvec) {
+  if(vec[1][0] != '_' && !trackdb_set(vec[0], vec[1], vec[2]))
+    sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+static int c_prefs(struct conn *c,
+		   char **vec,
+		   int attribute((unused)) nvec) {
+  struct kvp *k;
+
+  k = trackdb_get_all(vec[0]);
+  sink_writes(ev_writer_sink(c->w), "253 prefs follow\n");
+  for(; k; k = k->next)
+    if(k->name[0] != '_')		/* omit internal values */
+      sink_printf(ev_writer_sink(c->w),
+		  " %s %s\n", quoteutf8(k->name), quoteutf8(k->value));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int c_exists(struct conn *c,
+		    char **vec,
+		    int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[trackdb_exists(vec[0])]);
+  return 1;
+}
+
+static void search_parse_error(const char *msg, void *u) {
+  *(const char **)u = msg;
+}
+
+static int c_search(struct conn *c,
+			  char **vec,
+			  int attribute((unused)) nvec) {
+  char **terms, **results;
+  int nterms, nresults, n;
+  const char *e = "unknown error";
+
+  /* This is a bit of a bodge.  Initially it's there to make the eclient
+   * interface a bit more convenient to add searching to, but it has the more
+   * compelling advantage that if everything uses it, then interpretation of
+   * user-supplied search strings will be the same everywhere. */
+  if(!(terms = split(vec[0], &nterms, SPLIT_QUOTES, search_parse_error, &e))) {
+    sink_printf(ev_writer_sink(c->w), "550 %s\n", e);
+  } else {
+    results = trackdb_search(terms, nterms, &nresults);
+    sink_printf(ev_writer_sink(c->w), "253 %d matches\n", nresults);
+    for(n = 0; n < nresults; ++n)
+      sink_printf(ev_writer_sink(c->w), "%s\n", results[n]);
+    sink_writes(ev_writer_sink(c->w), ".\n");
+  }
+  return 1;
+}
+
+static int c_random_enable(struct conn *c,
+			   char attribute((unused)) **vec,
+			   int attribute((unused)) nvec) {
+  enable_random(c->who, c->ev);
+  /* Enable implicitly unpauses if there is nothing playing */
+  if(paused && !playing) resume_playing(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_random_disable(struct conn *c,
+			    char attribute((unused)) **vec,
+			    int attribute((unused)) nvec) {
+  disable_random(c->who);
+  sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;			/* completed */
+}
+
+static int c_random_enabled(struct conn *c,
+			    char attribute((unused)) **vec,
+			    int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[random_is_enabled()]);
+  return 1;			/* completed */
+}
+
+static int c_stats(struct conn *c,
+		   char attribute((unused)) **vec,
+		   int attribute((unused)) nvec) {
+  char **v;
+  int nv, n;
+
+  v = trackdb_stats(&nv);
+  sink_printf(ev_writer_sink(c->w), "253 stats\n");
+  for(n = 0; n < nv; ++n) {
+    if(v[n][0] == '.')
+      sink_writes(ev_writer_sink(c->w), ".");
+    sink_printf(ev_writer_sink(c->w), "%s\n", v[n]);
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;
+}
+
+static int c_volume(struct conn *c,
+		    char **vec,
+		    int nvec) {
+  int l, r, set;
+  char lb[32], rb[32];
+
+  switch(nvec) {
+  case 0:
+    set = 0;
+    break;
+  case 1:
+    l = r = atoi(vec[0]);
+    set = 1;
+    break;
+  case 2:
+    l = atoi(vec[0]);
+    r = atoi(vec[1]);
+    set = 1;
+    break;
+  default:
+    abort();
+  }
+  if(mixer_control(&l, &r, set))
+    sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n");
+  else {
+    sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
+    if(l != volume_left || r != volume_right) {
+      volume_left = l;
+      volume_right = r;
+      snprintf(lb, sizeof lb, "%d", l);
+      snprintf(rb, sizeof rb, "%d", r);
+      eventlog("volume", lb, rb, (char *)0);
+    }
+  }
+  return 1;
+}
+
+/* we are logging, and some data is available to read */
+static int logging_reader_callback(ev_source *ev,
+				   ev_reader *reader,
+				   int fd,
+				   void *ptr,
+				   size_t bytes,
+				   int eof,
+				   void *u) {
+  struct conn *c = u;
+
+  /* don't log to this conn any more */
+  eventlog_remove(c->lo);
+  /* terminate the log output */
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  /* restore the reader callback */
+  c->reader = reader_callback;
+  /* ...and exit via it */
+  return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+static void logclient(const char *msg, void *user) {
+  struct conn *c = user;
+
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n",
+	      (uintmax_t)time(0), msg);
+}
+
+static int c_log(struct conn *c,
+		 char attribute((unused)) **vec,
+		 int attribute((unused)) nvec) {
+  time_t now;
+
+  sink_writes(ev_writer_sink(c->w), "254 OK\n");
+  /* pump out initial state */
+  time(&now);
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      playing_is_enabled() ? "enable_play" : "disable_play");
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      random_is_enabled() ? "enable_random" : "disable_random");
+  sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+	      (uintmax_t)now, 
+	      paused ? "pause" : "resume");
+  c->lo = xmalloc(sizeof *c->lo);
+  c->lo->fn = logclient;
+  c->lo->user = c;
+  eventlog_add(c->lo);
+  c->reader = logging_reader_callback;
+  return 0;
+}
+
+static void post_move_cleanup(void) {
+  struct queue_entry *q;
+
+  /* If we have caused the random track to not be at the end then we make it no
+   * longer be random. */
+  for(q = qhead.next; q != &qhead; q = q->next)
+    if(q->state == playing_random && q->next != &qhead)
+      q->state = playing_unplayed;
+  /* That might mean we need to add a new random track. */
+  add_random_track();
+  queue_write();
+}
+
+static int c_move(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  struct queue_entry *q;
+  int n;
+
+  if(config->restrictions & RESTRICT_MOVE) {
+    if(!trusted(c)) {
+      sink_writes(ev_writer_sink(c->w),
+		  "550 only trusted users can move tracks\n");
+      return 1;
+    }
+  }
+  if(!(q = queue_find(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+    return 1;
+  }
+  n = queue_move(q, atoi(vec[1]), c->who);
+  post_move_cleanup();
+  sink_printf(ev_writer_sink(c->w), "252 %d\n", n);
+  /* If we've moved to the head of the queue then prepare the track. */
+  if(q == qhead.next)
+    prepare(c->ev, q);
+  return 1;
+}
+
+static int c_moveafter(struct conn *c,
+		       char **vec,
+		       int attribute((unused)) nvec) {
+  struct queue_entry *q, **qs;
+  int n;
+
+  if(config->restrictions & RESTRICT_MOVE) {
+    if(!trusted(c)) {
+      sink_writes(ev_writer_sink(c->w),
+		  "550 only trusted users can move tracks\n");
+      return 1;
+    }
+  }
+  if(vec[0][0]) {
+    if(!(q = queue_find(vec[0]))) {
+      sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+      return 1;
+    }
+  } else
+    q = 0;
+  ++vec;
+  --nvec;
+  qs = xcalloc(nvec, sizeof *qs);
+  for(n = 0; n < nvec; ++n)
+    if(!(qs[n] = queue_find(vec[n]))) {
+      sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+      return 1;
+    }
+  queue_moveafter(q, nvec, qs, c->who);
+  post_move_cleanup();
+  sink_printf(ev_writer_sink(c->w), "250 Moved tracks\n");
+  /* If we've moved to the head of the queue then prepare the track. */
+  if(q == qhead.next)
+    prepare(c->ev, q);
+  return 1;
+}
+
+static int c_part(struct conn *c,
+		  char **vec,
+		  int attribute((unused)) nvec) {
+  sink_printf(ev_writer_sink(c->w), "252 %s\n",
+	      trackdb_getpart(vec[0], vec[1], vec[2]));
+  return 1;
+}
+
+static int c_resolve(struct conn *c,
+		     char **vec,
+		     int attribute((unused)) nvec) {
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", track);
+  return 1;
+}
+
+static int c_tags(struct conn *c,
+		  char attribute((unused)) **vec,
+		  int attribute((unused)) nvec) {
+  char **tags = trackdb_alltags();
+  
+  sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n");
+  while(*tags) {
+    sink_printf(ev_writer_sink(c->w), "%s%s\n",
+		**tags == '.' ? "." : "", *tags);
+    ++tags;
+  }
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;				/* completed */
+
+}
+
+static int c_set_global(struct conn *c,
+			char **vec,
+			int attribute((unused)) nvec) {
+  trackdb_set_global(vec[0], vec[1], c->who);
+  sink_printf(ev_writer_sink(c->w), "250 OK\n");
+  return 1;
+}
+
+static int c_get_global(struct conn *c,
+			char **vec,
+			int attribute((unused)) nvec) {
+  const char *s = trackdb_get_global(vec[0]);
+
+  if(s)
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", s);
+  else
+    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+  return 1;
+}
+
+#define C_AUTH		0001		/* must be authenticated */
+#define C_TRUSTED	0002		/* must be trusted user */
+
+static const struct command {
+  const char *name;
+  int minargs, maxargs;
+  int (*fn)(struct conn *, char **, int);
+  unsigned flags;
+} commands[] = {
+  { "allfiles",       0, 2,       c_allfiles,       C_AUTH },
+  { "become",         1, 1,       c_become,         C_AUTH|C_TRUSTED },
+  { "dirs",           0, 2,       c_dirs,           C_AUTH },
+  { "disable",        0, 1,       c_disable,        C_AUTH },
+  { "enable",         0, 0,       c_enable,         C_AUTH },
+  { "enabled",        0, 0,       c_enabled,        C_AUTH },
+  { "exists",         1, 1,       c_exists,         C_AUTH },
+  { "files",          0, 2,       c_files,          C_AUTH },
+  { "get",            2, 2,       c_get,            C_AUTH },
+  { "get-global",     1, 1,       c_get_global,     C_AUTH },
+  { "length",         1, 1,       c_length,         C_AUTH },
+  { "log",            0, 0,       c_log,            C_AUTH },
+  { "move",           2, 2,       c_move,           C_AUTH },
+  { "moveafter",      1, INT_MAX, c_moveafter,      C_AUTH },
+  { "part",           3, 3,       c_part,           C_AUTH },
+  { "pause",          0, 0,       c_pause,          C_AUTH },
+  { "play",           1, 1,       c_play,           C_AUTH },
+  { "playing",        0, 0,       c_playing,        C_AUTH },
+  { "prefs",          1, 1,       c_prefs,          C_AUTH },
+  { "queue",          0, 0,       c_queue,          C_AUTH },
+  { "random-disable", 0, 0,       c_random_disable, C_AUTH },
+  { "random-enable",  0, 0,       c_random_enable,  C_AUTH },
+  { "random-enabled", 0, 0,       c_random_enabled, C_AUTH },
+  { "recent",         0, 0,       c_recent,         C_AUTH },
+  { "reconfigure",    0, 0,       c_reconfigure,    C_AUTH|C_TRUSTED },
+  { "remove",         1, 1,       c_remove,         C_AUTH },
+  { "rescan",         0, 0,       c_rescan,         C_AUTH|C_TRUSTED },
+  { "resolve",        1, 1,       c_resolve,        C_AUTH },
+  { "resume",         0, 0,       c_resume,         C_AUTH },
+  { "scratch",        0, 1,       c_scratch,        C_AUTH },
+  { "search",         1, 1,       c_search,         C_AUTH },
+  { "set",            3, 3,       c_set,            C_AUTH, },
+  { "set-global",     2, 2,       c_set_global,     C_AUTH },
+  { "shutdown",       0, 0,       c_shutdown,       C_AUTH|C_TRUSTED },
+  { "stats",          0, 0,       c_stats,          C_AUTH },
+  { "tags",           0, 0,       c_tags,           C_AUTH },
+  { "unset",          2, 2,       c_set,            C_AUTH },
+  { "unset-global",   1, 1,       c_set_global,      C_AUTH },
+  { "user",           2, 2,       c_user,           0 },
+  { "version",        0, 0,       c_version,        C_AUTH },
+  { "volume",         0, 2,       c_volume,         C_AUTH }
+};
+
+static void command_error(const char *msg, void *u) {
+  struct conn *c = u;
+
+  sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg);
+}
+
+/* process a command.  Return 1 if complete, 0 if incomplete. */
+static int command(struct conn *c, char *line) {
+  char **vec;
+  int nvec, n;
+
+  D(("server command %s", line));
+  if(!(vec = split(line, &nvec, SPLIT_QUOTES, command_error, c))) {
+    sink_writes(ev_writer_sink(c->w), "500 cannot parse command\n");
+    return 1;
+  }
+  if(nvec == 0) {
+    sink_writes(ev_writer_sink(c->w), "500 do what?\n");
+    return 1;
+  }
+  if((n = TABLE_FIND(commands, struct command, name, vec[0])) < 0)
+    sink_writes(ev_writer_sink(c->w), "500 unknown command\n");
+  else {
+    if((commands[n].flags & C_AUTH) && !c->who) {
+      sink_writes(ev_writer_sink(c->w), "530 not authenticated\n");
+      return 1;
+    }
+    if((commands[n].flags & C_TRUSTED) && !trusted(c)) {
+      sink_writes(ev_writer_sink(c->w), "530 insufficient privilege\n");
+      return 1;
+    }
+    ++vec;
+    --nvec;
+    if(nvec < commands[n].minargs) {
+      sink_writes(ev_writer_sink(c->w), "500 missing argument(s)\n");
+      return 1;
+    }
+    if(nvec > commands[n].maxargs) {
+      sink_writes(ev_writer_sink(c->w), "500 too many arguments\n");
+      return 1;
+    }
+    return commands[n].fn(c, vec, nvec);
+  }
+  return 1;			/* completed */
+}
+
+/* redirect to the right reader callback for our current state */
+static int redirect_reader_callback(ev_source *ev,
+				    ev_reader *reader,
+				    int fd,
+				    void *ptr,
+				    size_t bytes,
+				    int eof,
+				    void *u) {
+  struct conn *c = u;
+
+  return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+/* the main command reader */
+static int reader_callback(ev_source attribute((unused)) *ev,
+			   ev_reader *reader,
+			   int attribute((unused)) fd,
+			   void *ptr,
+			   size_t bytes,
+			   int eof,
+			   void *u) {
+  struct conn *c = u;
+  char *eol;
+  int complete;
+
+  D(("server reader_callback"));
+  while((eol = memchr(ptr, '\n', bytes))) {
+    *eol++ = 0;
+    ev_reader_consume(reader, eol - (char *)ptr);
+    complete = command(c, ptr);
+    bytes -= (eol - (char *)ptr);
+    ptr = eol;
+    if(!complete) {
+      /* the command had better have set a new reader callback */
+      if(bytes || eof)
+	/* there are further bytes to read, or we are at eof; arrange for the
+	 * command's reader callback to handle them */
+	return ev_reader_incomplete(reader);
+      /* nothing's going on right now */
+      return 0;
+    }
+    /* command completed, we can go around and handle the next one */
+  }
+  if(eof) {
+    if(bytes)
+      error(0, "S%x unterminated line", c->tag);
+    c->r = 0;
+    return ev_writer_close(c->w);
+  }
+  return 0;
+}
+
+static int listen_callback(ev_source *ev,
+			   int fd,
+			   const struct sockaddr attribute((unused)) *remote,
+			   socklen_t attribute((unused)) rlen,
+			   void *u) {
+  const struct listener *l = u;
+  struct conn *c = xmalloc(sizeof *c);
+  static unsigned tags;
+
+  D(("server listen_callback fd %d (%s)", fd, l->name));
+  nonblock(fd);
+  cloexec(fd);
+  c->tag = tags++;
+  c->ev = ev;
+  c->w = ev_writer_new(ev, fd, writer_error, c);
+  c->r = ev_reader_new(ev, fd, redirect_reader_callback, reader_error, c);
+  c->fd = fd;
+  c->reader = reader_callback;
+  c->l = l;
+  gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
+  sink_printf(ev_writer_sink(c->w), "231 %s\n", hex(c->nonce, sizeof c->nonce));
+  return 0;
+}
+
+int server_start(ev_source *ev, int pf,
+		 size_t socklen, const struct sockaddr *sa,
+		 const char *name) {
+  int fd;
+  struct listener *l = xmalloc(sizeof *l);
+  static const int one = 1;
+
+  D(("server_init socket %s", name));
+  fd = xsocket(pf, SOCK_STREAM, 0);
+  xsetsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
+  if(bind(fd, sa, socklen) < 0) {
+    error(errno, "error binding to %s", name);
+    return -1;
+  }
+  xlisten(fd, 128);
+  nonblock(fd);
+  cloexec(fd);
+  l->name = name;
+  l->pf = pf;
+  if(ev_listen(ev, fd, listen_callback, l)) exit(EXIT_FAILURE);
+  return fd;
+}
+
+int server_stop(ev_source *ev, int fd) {
+  xclose(fd);
+  return ev_listen_cancel(ev, fd);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:eb9b30c87008880f3f53535101356ab5 */
diff --git a/server/server.h b/server/server.h
new file mode 100644
index 0000000..012497c
--- /dev/null
+++ b/server/server.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef SERVER_H
+#define SERVER_H
+
+/* The command-response server.
+ *
+ * See disorder_protocol(5) for sometimes up-to-date protocol documentation.
+ */
+
+int server_start(ev_source *ev, int pf,
+		 size_t socklen, const struct sockaddr *sa,
+		 const char *name);
+/* start listening.  Return the fd. */
+
+int server_stop(ev_source *ev, int fd);
+/* Stop listening on @fd@ */
+
+extern int volume_left, volume_right;	/* last known volume */
+
+#endif /* SERVER_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:db22be3b0e3ce0914513e917df553937 */
diff --git a/server/speaker.c b/server/speaker.c
new file mode 100644
index 0000000..ef31931
--- /dev/null
+++ b/server/speaker.c
@@ -0,0 +1,634 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/* This program deliberately does not use the garbage collector even though it
+ * might be convenient to do so.  This is for two reasons.  Firstly some libao
+ * drivers are implemented using threads and we do not want to have to deal
+ * with potential interactions between threading and garbage collection.
+ * Secondly this process needs to be able to respond quickly and this is not
+ * compatible with the collector hanging the program even relatively
+ * briefly. */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "speaker.h"
+#include "user.h"
+
+#define BUFFER_SECONDS 5                /* How many seconds of input to
+                                         * buffer. */
+
+#define FRAMES 4096                     /* Frame batch size */
+
+#define NFDS 256                        /* Max FDs to poll for */
+
+/* Known tracks are kept in a linked list.  We don't normally to have
+ * more than two - maybe three at the outside. */
+static struct track {
+  struct track *next;                   /* next track */
+  int fd;                               /* input FD */
+  char id[24];                          /* ID */
+  size_t start, used;                   /* start + bytes used */
+  int eof;                              /* input is at EOF */
+  int got_format;                       /* got format yet? */
+  ao_sample_format format;              /* sample format */
+  unsigned long long played;            /* number of frames played */
+  char *buffer;                         /* sample buffer */
+  size_t size;                          /* sample buffer size */
+  int slot;                             /* poll array slot */
+} *tracks, *playing;                    /* all tracks + playing track */
+
+static time_t last_report;              /* when we last reported */
+static int paused;                      /* pause status */
+static snd_pcm_t *pcm;                  /* current pcm handle */
+static ao_sample_format pcm_format;     /* current format if aodev != 0 */
+static size_t bpf;                      /* bytes per frame */
+static struct pollfd fds[NFDS];         /* if we need more than that */
+static int fdno;                        /* fd number */
+static snd_pcm_uframes_t pcm_bufsize;   /* buffer size */
+static int forceplay;                   /* frames to force play */
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { "no-debug", no_argument, 0, 'D' },
+  { 0, 0, 0, 0 }
+};
+
+/* Display usage message and terminate. */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  disorder-speaker [OPTIONS]\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n"
+          "\n"
+          "Speaker process for DisOrder.  Not intended to be run\n"
+          "directly.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* Display version number and terminate. */
+static void version(void) {
+  xprintf("disorder-speaker version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+/* Return the number of bytes per frame in FORMAT. */
+static size_t bytes_per_frame(const ao_sample_format *format) {
+  return format->channels * format->bits / 8;
+}
+
+/* Find track ID, maybe creating it if not found. */
+static struct track *findtrack(const char *id, int create) {
+  struct track *t;
+
+  D(("findtrack %s %d", id, create));
+  for(t = tracks; t && strcmp(id, t->id); t = t->next)
+    ;
+  if(!t && create) {
+    t = xmalloc(sizeof *t);
+    t->next = tracks;
+    strcpy(t->id, id);
+    t->fd = -1;
+    tracks = t;
+    /* The initial input buffer will be the sample format. */
+    t->buffer = (void *)&t->format;
+    t->size = sizeof t->format;
+  }
+  return t;
+}
+
+/* Remove track ID (but do not destroy it). */
+static struct track *removetrack(const char *id) {
+  struct track *t, **tt;
+
+  D(("removetrack %s", id));
+  for(tt = &tracks; (t = *tt) && strcmp(id, t->id); tt = &t->next)
+    ;
+  if(t)
+    *tt = t->next;
+  return t;
+}
+
+/* Destroy a track. */
+static void destroy(struct track *t) {
+  D(("destroy %s", t->id));
+  if(t->fd != -1) xclose(t->fd);
+  if(t->buffer != (void *)&t->format) free(t->buffer);
+  free(t);
+}
+
+/* Notice a new FD. */
+static void acquire(struct track *t, int fd) {
+  D(("acquire %s %d", t->id, fd));
+  if(t->fd != -1)
+    xclose(t->fd);
+  t->fd = fd;
+  nonblock(fd);
+}
+
+/* Read data into a sample buffer.  Return 0 on success, -1 on EOF. */
+static int fill(struct track *t) {
+  size_t where, left;
+  int n;
+
+  D(("fill %s: eof=%d used=%zu size=%zu  got_format=%d",
+     t->id, t->eof, t->used, t->size, t->got_format));
+  if(t->eof) return -1;
+  if(t->used < t->size) {
+    /* there is room left in the buffer */
+    where = (t->start + t->used) % t->size;
+    if(t->got_format) {
+      /* We are reading audio data, get as much as we can */
+      if(where >= t->start) left = t->size - where;
+      else left = t->start - where;
+    } else
+      /* We are still waiting for the format, only get that */
+      left = sizeof (ao_sample_format) - t->used;
+    do {
+      n = read(t->fd, t->buffer + where, left);
+    } while(n < 0 && errno == EINTR);
+    if(n < 0) {
+      if(errno != EAGAIN) fatal(errno, "error reading sample stream");
+      return 0;
+    }
+    if(n == 0) {
+      D(("fill %s: eof detected", t->id));
+      t->eof = 1;
+      return -1;
+    }
+    t->used += n;
+    if(!t->got_format && t->used >= sizeof (ao_sample_format)) {
+      assert(t->used == sizeof (ao_sample_format));
+      /* Check that our assumptions are met. */
+      if(t->format.bits & 7)
+        fatal(0, "bits per sample not a multiple of 8");
+      /* Make a new buffer for audio data. */
+      t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS;
+      t->buffer = xmalloc(t->size);
+      t->used = 0;
+      t->got_format = 1;
+      D(("got format for %s", t->id));
+    }
+  }
+  return 0;
+}
+
+/* Return true if A and B denote identical libao formats, else false. */
+static int formats_equal(const ao_sample_format *a,
+                         const ao_sample_format *b) {
+  return (a->bits == b->bits
+          && a->rate == b->rate
+          && a->channels == b->channels
+          && a->byte_format == b->byte_format);
+}
+
+/* Close the sound device. */
+static void idle(void) {
+  int  err;
+
+  D(("idle"));
+  if(pcm) {
+    if((err = snd_pcm_nonblock(pcm, 0)) < 0)
+      fatal(0, "error calling snd_pcm_nonblock: %d", err);
+    D(("draining pcm"));
+    snd_pcm_drain(pcm);
+    D(("closing pcm"));
+    snd_pcm_close(pcm);
+    pcm = 0;
+    forceplay = 0;
+    D(("released audio device"));
+  }
+}
+
+/* Abandon the current track */
+static void abandon(void) {
+  struct speaker_message sm;
+
+  D(("abandon"));
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_FINISHED;
+  strcpy(sm.id, playing->id);
+  speaker_send(1, &sm, 0);
+  removetrack(playing->id);
+  destroy(playing);
+  playing = 0;
+  forceplay = 0;
+}
+
+/* Make sure the sound device is open and has the right sample format.  Return
+ * 0 on success and -1 on error. */
+static int activate(void) {
+  int err;
+  snd_pcm_hw_params_t *hwparams;
+  snd_pcm_sw_params_t *swparams;
+  int sample_format = 0;
+  unsigned rate;
+
+  /* If we don't know the format yet we cannot start. */
+  if(!playing->got_format) {
+    D((" - not got format for %s", playing->id));
+    return -1;
+  }
+  /* If we need to change format then close the current device. */
+  if(pcm && !formats_equal(&playing->format, &pcm_format))
+     idle();
+  if(!pcm) {
+    D(("snd_pcm_open"));
+    if((err = snd_pcm_open(&pcm,
+                           config->device,
+                           SND_PCM_STREAM_PLAYBACK,
+                           SND_PCM_NONBLOCK))) {
+      error(0, "error from snd_pcm_open: %d", err);
+      goto error;
+    }
+    snd_pcm_hw_params_alloca(&hwparams);
+    D(("set up hw params"));
+    if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+    if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
+                                           SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
+    switch(playing->format.bits) {
+    case 8:
+      sample_format = SND_PCM_FORMAT_S8;
+      break;
+    case 16:
+      switch(playing->format.byte_format) {
+      case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break;
+      case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
+      case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
+        error(0, "unrecognized byte format %d", playing->format.byte_format);
+        goto fatal;
+      }
+      break;
+    default:
+      error(0, "unsupported sample size %d", playing->format.bits);
+      goto fatal;
+    }
+    if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+                                           sample_format)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+            sample_format, err);
+      goto fatal;
+    }
+    rate = playing->format.rate;
+    if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
+            playing->format.rate, err);
+      goto fatal;
+    }
+    if(rate != (unsigned)playing->format.rate)
+      info("want rate %d, got %u", playing->format.rate, rate);
+    if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
+                                             playing->format.channels)) < 0) {
+      error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+            playing->format.channels, err);
+      goto fatal;
+    }
+    pcm_bufsize = 3 * FRAMES;
+    if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+                                                     &pcm_bufsize)) < 0)
+      fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+            3 * FRAMES, err);
+    if(pcm_bufsize != 3 * FRAMES)
+      info("asked for PCM buffer of %d frames, got %d",
+           3 * FRAMES, (int)pcm_bufsize);
+    if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+      fatal(0, "error calling snd_pcm_hw_params: %d", err);
+    D(("set up sw params"));
+    snd_pcm_sw_params_alloca(&swparams);
+    if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+    if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+            FRAMES, err);
+    if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+      fatal(0, "error calling snd_pcm_sw_params: %d", err);
+    pcm_format = playing->format;
+    bpf = bytes_per_frame(&pcm_format);
+    D(("acquired audio device"));
+  }
+  return 0;
+fatal:
+  abandon();
+error:
+  /* We assume the error is temporary and that we'll retry in a bit. */
+  if(pcm) {
+    snd_pcm_close(pcm);
+    pcm = 0;
+  }
+  return -1;
+}
+
+/* Check to see whether the current track has finished playing */
+static void maybe_finished(void) {
+  if(playing
+     && playing->eof
+     && (!playing->got_format
+         || playing->used < bytes_per_frame(&playing->format)))
+    abandon();
+}
+
+static void play(size_t frames) {
+  snd_pcm_sframes_t written_frames;
+  size_t avail_bytes, avail_frames, written_bytes;
+  int err;
+
+  if(activate()) {
+    if(playing)
+      forceplay = frames;
+    else
+      forceplay = 0;                    /* Must have called abandon() */
+    return;
+  }
+  D(("play: play %zu/%zu%s %dHz %db %dc",  frames, playing->used / bpf,
+     playing->eof ? " EOF" : "",
+     playing->format.rate,
+     playing->format.bits,
+     playing->format.channels));
+  /* If we haven't got enough bytes yet wait until we have.  Exception: when
+   * we are at eof. */
+  if(playing->used < frames * bpf && !playing->eof) {
+    forceplay = frames;
+    return;
+  }
+  /* We have got enough data so don't force play again */
+  forceplay = 0;
+  /* Figure out how many frames there are available to write */
+  if(playing->start + playing->used > playing->size)
+    avail_bytes = playing->size - playing->start;
+  else
+    avail_bytes = playing->used;
+  avail_frames = avail_bytes / bpf;
+  if(avail_frames > frames)
+    avail_frames = frames;
+  if(!avail_frames)
+    return;
+  written_frames = snd_pcm_writei(pcm,
+                                  playing->buffer + playing->start,
+                                  avail_frames);
+  D(("actually play %zu frames, wrote %d",
+     avail_frames, (int)written_frames));
+  if(written_frames < 0) {
+    switch(written_frames) {
+    case -EPIPE:                        /* underrun */
+      error(0, "snd_pcm_writei reports underrun");
+      if((err = snd_pcm_prepare(pcm)) < 0)
+        fatal(0, "error calling snd_pcm_prepare: %d", err);
+      return;
+    case -EAGAIN:
+      return;
+    default:
+      fatal(0, "error calling snd_pcm_writei: %d", (int)written_frames);
+    }
+  }
+  written_bytes = written_frames * bpf;
+  playing->start += written_bytes;
+  playing->used -= written_bytes;
+  playing->played += written_frames;
+  /* If the pointer is at the end of the buffer (or the buffer is completely
+   * empty) wrap it back to the start. */
+  if(!playing->used || playing->start == playing->size)
+    playing->start = 0;
+  frames -= written_frames;
+}
+
+/* Notify the server what we're up to. */
+static void report(void) {
+  struct speaker_message sm;
+
+  if(playing && playing->buffer != (void *)&playing->format) {
+    memset(&sm, 0, sizeof sm);
+    sm.type = paused ? SM_PAUSED : SM_PLAYING;
+    strcpy(sm.id, playing->id);
+    sm.data = playing->played / playing->format.rate;
+    speaker_send(1, &sm, 0);
+  }
+  time(&last_report);
+}
+
+static int addfd(int fd, int events) {
+  if(fdno < NFDS) {
+    fds[fdno].fd = fd;
+    fds[fdno].events = events;
+    return fdno++;
+  } else
+    return -1;
+}
+
+int main(int argc, char **argv) {
+  int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, err;
+  unsigned short alsa_revents;
+  struct track *t;
+  struct speaker_message sm;
+
+  set_progname(argv);
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    case 'D': debugging = 0; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(getenv("DISORDER_DEBUG_SPEAKER")) debugging = 1;
+  /* If stderr is a TTY then log there, otherwise to syslog. */
+  if(!isatty(2)) {
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
+  if(config_read()) fatal(0, "cannot read configuration");
+  /* ignore SIGPIPE */
+  signal(SIGPIPE, SIG_IGN);
+  /* set nice value */
+  xnice(config->nice_speaker);
+  /* change user */
+  become_mortal();
+  /* make sure we're not root, whatever the config says */
+  if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+  info("started");
+  while(getppid() != 1) {
+    fdno = 0;
+    /* Always ready for commands from the main server. */
+    stdin_slot = addfd(0, POLLIN);
+    /* Try to read sample data for the currently playing track if there is
+     * buffer space. */
+    if(playing && !playing->eof && playing->used < playing->size) {
+      playing->slot = addfd(playing->fd, POLLIN);
+    } else if(playing)
+      playing->slot = -1;
+    /* If forceplay is set then wait until it succeeds before waiting on the
+     * sound device. */
+    if(pcm && !forceplay) {
+      alsa_slots = fdno;
+      alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno);
+      fdno += alsa_nslots;
+    } else
+      alsa_slots = -1;
+    /* If any other tracks don't have a full buffer, try to read sample data
+     * from them. */
+    for(t = tracks; t; t = t->next)
+      if(t != playing) {
+        if(!t->eof && t->used < t->size) {
+          t->slot = addfd(t->fd,  POLLIN);
+        } else
+          t->slot = -1;
+      }
+    /* Wait up to a second before thinking about current state */
+    n = poll(fds, fdno, 1000);
+    if(n < 0) {
+      if(errno == EINTR) continue;
+      fatal(errno, "error calling poll");
+    }
+    /* Play some sound before doing anything else */
+    if(alsa_slots != -1) {
+      if((err = snd_pcm_poll_descriptors_revents(pcm,
+                                                 &fds[alsa_slots],
+                                                 alsa_nslots,
+                                                 &alsa_revents)) < 0)
+        fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+      if(alsa_revents & POLLOUT)
+        play(3 * FRAMES);
+    } else {
+      /* Some attempt to play must have failed */
+      if(playing && !paused)
+        play(forceplay);
+      else
+        forceplay = 0;                  /* just in case */
+    }
+    /* Perhaps we have a command to process */
+    if(fds[stdin_slot].revents & POLLIN) {
+      n = speaker_recv(0, &sm, &fd);
+      if(n > 0)
+	switch(sm.type) {
+	case SM_PREPARE:
+          D(("SM_PREPARE %s %d", sm.id, fd));
+	  if(fd == -1) fatal(0, "got SM_PREPARE but no file descriptor");
+	  t = findtrack(sm.id, 1);
+          acquire(t, fd);
+	  break;
+	case SM_PLAY:
+          D(("SM_PLAY %s %d", sm.id, fd));
+          if(playing) fatal(0, "got SM_PLAY but already playing something");
+	  t = findtrack(sm.id, 1);
+          if(fd != -1) acquire(t, fd);
+          playing = t;
+          play(pcm_bufsize);
+          report();
+	  break;
+	case SM_PAUSE:
+          D(("SM_PAUSE"));
+	  paused = 1;
+          report();
+          break;
+	case SM_RESUME:
+          D(("SM_RESUME"));
+          if(paused) {
+            paused = 0;
+            if(playing)
+              play(pcm_bufsize);
+          }
+          report();
+	  break;
+	case SM_CANCEL:
+          D(("SM_CANCEL %s",  sm.id));
+	  t = removetrack(sm.id);
+	  if(t) {
+	    if(t == playing) {
+              sm.type = SM_FINISHED;
+              strcpy(sm.id, playing->id);
+              speaker_send(1, &sm, 0);
+	      playing = 0;
+            }
+	    destroy(t);
+	  } else
+	    error(0, "SM_CANCEL for unknown track %s", sm.id);
+          report();
+	  break;
+	case SM_RELOAD:
+          D(("SM_RELOAD"));
+	  if(config_read()) error(0, "cannot read configuration");
+          info("reloaded configuration");
+	  break;
+	default:
+	  error(0, "unknown message type %d", sm.type);
+        }
+    }
+    /* Read in any buffered data */
+    for(t = tracks; t; t = t->next)
+      if(t->slot != -1 && (fds[t->slot].revents & POLLIN))
+         fill(t);
+    /* We might be able to play now */
+    if(pcm && forceplay && playing && !paused)
+      play(forceplay);
+    /* Maybe we finished playing a track somewhere in the above */
+    maybe_finished();
+    /* If we don't need the sound device for now then close it for the benefit
+     * of anyone else who wants it. */
+    if((!playing || paused) && pcm)
+      idle();
+    /* If we've not reported out state for a second do so now. */
+    if(time(0) > last_report)
+      report();
+  }
+  info("stopped (parent terminated)");
+  exit(0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:HQ4ayCGCjeBF97RuRnvcyg */
diff --git a/server/state.c b/server/state.c
new file mode 100644
index 0000000..60fea26
--- /dev/null
+++ b/server/state.c
@@ -0,0 +1,171 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "play.h"
+#include "trackdb.h"
+#include "state.h"
+#include "configuration.h"
+#include "log.h"
+#include "queue.h"
+#include "server.h"
+#include "printf.h"
+#include "addr.h"
+
+static const char *current_unix;
+static int current_unix_fd;
+
+static struct addrinfo *current_listen_addrinfo;
+static int current_listen_fd;
+
+void quit(ev_source *ev) {
+  quitting(ev);
+  trackdb_close();
+  trackdb_deinit();
+  info("terminating");
+  _exit(0);
+}
+
+static void reset_socket(ev_source *ev) {
+  const char *new_unix;
+  struct addrinfo *res;
+  struct sockaddr_un sun;
+  char *name;
+  
+  static const struct addrinfo pref = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_STREAM,
+    IPPROTO_TCP,
+    0,
+    0,
+    0,
+    0
+  };
+
+  /* unix first */
+  new_unix = config_get_file("socket");
+  if(!current_unix || strcmp(current_unix, new_unix)) {
+    /* either there was no socket, or there was but a different path */
+    if(current_unix) {
+      /* stop the old one and remove it from the filesystem */
+      server_stop(ev, current_unix_fd);
+      if(unlink(current_unix) < 0)
+	fatal(errno, "unlink %s", current_unix);
+    }
+    /* start the new one */
+    if(strlen(new_unix) >= sizeof sun.sun_path)
+      fatal(0, "socket path %s is too long", new_unix);
+    memset(&sun, 0, sizeof sun);
+    sun.sun_family = AF_UNIX;
+    strcpy(sun.sun_path, new_unix);
+    if(unlink(new_unix) < 0 && errno != ENOENT)
+      fatal(errno, "unlink %s", new_unix);
+    if((current_unix_fd = server_start(ev, PF_UNIX, sizeof sun,
+				       (const struct sockaddr *)&sun,
+				       new_unix)) >= 0) {
+      current_unix = new_unix;
+      if(chmod(new_unix, 0777) < 0)
+	fatal(errno, "error calling chmod %s", new_unix);
+    } else
+      current_unix = 0;
+  }
+
+  /* get the new listen config */
+  if(config->listen.n)
+    res = get_address(&config->listen, &pref, &name);
+  else
+    res = 0;
+
+  if((res && !current_listen_addrinfo)
+     || (current_listen_addrinfo
+	 && (!res
+	     || addrinfocmp(res, current_listen_addrinfo)))) {
+    /* something has to change */
+    if(current_listen_addrinfo) {
+      /* delete the old listener */
+      server_stop(ev, current_listen_fd);
+      freeaddrinfo(current_listen_addrinfo);
+      current_listen_addrinfo = 0;
+    }
+    if(res) {
+      /* start the new listener */
+      if((current_listen_fd = server_start(ev, res->ai_family, res->ai_addrlen,
+					   res->ai_addr, name)) >= 0) {
+	current_listen_addrinfo = res;
+	res = 0;
+      }
+    }
+  }
+  /* if res is still set it needs freeing */
+  if(res)
+    freeaddrinfo(res);
+}
+
+int reconfigure(ev_source *ev, int reload) {
+  int need_another_rescan = 0;
+  int ret = 0;
+
+  D(("reconfigure(%d)", reload));
+  if(reload) {
+    need_another_rescan = trackdb_rescan_cancel();
+    trackdb_close();
+    if(config_read())
+      ret = -1;
+    else {
+      /* Tell the speaker it needs to reload its config too. */
+      speaker_reload();
+      info("%s: installed new configuration", configfile);
+    }
+  }
+  trackdb_open();
+  if(need_another_rescan)
+    trackdb_rescan(ev);
+  if(!ret) {
+    queue_read();
+    recent_read();
+    reset_socket(ev);
+  }
+  return ret;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:94e23a75c2ebdf8a11e17ed7b0fd8cb6 */
diff --git a/server/state.h b/server/state.h
new file mode 100644
index 0000000..ecfdc8e
--- /dev/null
+++ b/server/state.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef STATE_H
+#define STATE_H
+
+void quit(ev_source *ev) attribute((noreturn));
+/* terminate the daemon */
+
+int reconfigure(ev_source *ev, int reload);
+/* reconfigure.  If @reload@ is nonzero, update the configuration. */
+
+#endif /* QUIT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a119be84b4ca00f632dad62837a8f965 */
diff --git a/server/trackdb-int.h b/server/trackdb-int.h
new file mode 100644
index 0000000..7e14fb7
--- /dev/null
+++ b/server/trackdb-int.h
@@ -0,0 +1,124 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef TRACKDB_INT_H
+#define TRACKDB_INT_H
+
+extern DB_ENV *trackdb_env;
+
+extern DB *trackdb_tracksdb;
+extern DB *trackdb_prefsdb;
+extern DB *trackdb_searchdb;
+
+DBC *trackdb_opencursor(DB *db, DB_TXN *tid);
+/* open a transaction */
+
+int trackdb_closecursor(DBC *c);
+/* close transaction, returns 0 or DB_LOCK_DEADLOCK */
+
+int trackdb_notice(const char *track,
+                   const char *path);
+int trackdb_notice_tid(const char *track,
+                       const char *path,
+                       DB_TXN *tid);
+/* notice a track; return DB_NOTFOUND if new, else 0.  _tid can return
+ * DB_LOCK_DEADLOCK too. */
+
+int trackdb_obsolete(const char *track, DB_TXN *tid);
+/* obsolete a track */
+
+DB_TXN *trackdb_begin_transaction(void);
+void trackdb_abort_transaction(DB_TXN *tid);
+void trackdb_commit_transaction(DB_TXN *tid);
+/* begin, abort or commit a transaction */
+
+int trackdb_getdata(DB *db,
+                    const char *track,
+                    struct kvp **kp,
+                    DB_TXN *tid);
+/* fetch and decode a database entry.  Returns 0, DB_NOTFOUND or
+ * DB_LOCK_DEADLOCK. */
+
+int trackdb_putdata(DB *db,
+                    const char *track,
+                    const struct kvp *k,
+                    DB_TXN *tid,
+                    u_int32_t flags);
+/* encode and store a database entry.  Returns 0, DB_KEYEXIST or
+ * DB_LOCK_DEADLOCK. */
+
+int trackdb_delkey(DB *db,
+                   const char *track,
+                   DB_TXN *tid);
+/* delete a database entry.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+
+int trackdb_delkeydata(DB *db,
+                       const char *word,
+                       const char *track,
+                       DB_TXN *tid);
+/* delete a (key,data) pair.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+
+int trackdb_scan(const char *root,
+                 int (*callback)(const char *track,
+                                 struct kvp *data,
+                                 void *u,
+                                 DB_TXN *tid),
+                 void *u,
+                 DB_TXN *tid);
+/* Call CALLBACK for each non-alias track below ROOT.  Return 0 or
+ * DB_LOCK_DEADLOCK.  CALLBACK should return 0 on success or EINTR to cancel
+ * the scan. */
+
+/* fill KEY in with S, returns KEY */
+static inline DBT *make_key(DBT *key, const char *s) {
+  memset(key, 0, sizeof *key);
+  key->data = (void *)s;
+  key->size = strlen(s);
+  return key;
+}
+
+/* set DATA up to receive data, returns DATA */
+static inline DBT *prepare_data(DBT *data) {
+  memset(data, 0, sizeof *data);
+  data->flags = DB_DBT_MALLOC;
+  return data;
+}
+
+/* encode K and store in DATA, returns DATA */
+static inline DBT *encode_data(DBT *data, const struct kvp *k) {
+  size_t size;
+  
+  memset(data, 0, sizeof *data);
+  data->data = kvp_urlencode(k, &size);
+  data->size = size;
+  return data;
+}
+
+#endif /* TRACKDB_INT_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:BJ2D2N4ftvK2bQRnaLuFIg */
diff --git a/server/trackdb.c b/server/trackdb.c
new file mode 100644
index 0000000..223dc80
--- /dev/null
+++ b/server/trackdb.c
@@ -0,0 +1,1833 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "event.h"
+#include "mem.h"
+#include "kvp.h"
+#include "log.h"
+#include "vector.h"
+#include "trackdb.h"
+#include "configuration.h"
+#include "syscalls.h"
+#include "wstat.h"
+#include "words.h"
+#include "printf.h"
+#include "filepart.h"
+#include "trackname.h"
+#include "trackdb-int.h"
+#include "logfd.h"
+#include "cache.h"
+#include "eventlog.h"
+#include "hash.h"
+
+#define RESCAN "disorder-rescan"
+#define DEADLOCK "disorder-deadlock"
+
+static const char *getpart(const char *track,
+                           const char *context,
+                           const char *part,
+                           const struct kvp *p,
+                           int *used_db);
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp);
+static int trackdb_get_global_tid(const char *name,
+                                  DB_TXN *tid,
+                                  const char **rp);
+
+const struct cache_type cache_files_type = { 86400 };
+unsigned long cache_files_hits, cache_files_misses;
+
+/* setup and teardown ********************************************************/
+
+static const char *home;                /* home had better not change */
+DB_ENV *trackdb_env;			/* db environment */
+DB *trackdb_tracksdb;			/* the db itself */
+DB *trackdb_prefsdb;			/* preferences */
+DB *trackdb_searchdb;			/* the search database */
+DB *trackdb_tagsdb;			/* the tags database */
+DB *trackdb_globaldb;                   /* global preferences */
+static pid_t db_deadlock_pid = -1;      /* deadlock manager PID */
+static pid_t rescan_pid = -1;           /* rescanner PID */
+static int initialized, opened;         /* state */
+
+/* tracks matched by required_tags */
+static char **reqtracks;
+static size_t nreqtracks;
+
+/* comparison function for keys */
+static int compare(DB attribute((unused)) *db_,
+		   const DBT *a, const DBT *b) {
+  return compare_path_raw(a->data, a->size, b->data, b->size);
+}
+
+/* open environment */
+void trackdb_init(int recover) {
+  int err;
+  static int recover_type[] = { 0, DB_RECOVER, DB_RECOVER_FATAL };
+
+  /* sanity checks */
+  assert(initialized == 0);
+  ++initialized;
+  if(home) {
+    if(strcmp(home, config->home))
+      fatal(0, "cannot change db home without server restart");
+    home = config->home;
+  }
+
+  /* create environment */
+  if((err = db_env_create(&trackdb_env, 0))) fatal(0, "db_env_create: %s",
+                                                   db_strerror(err));
+  if((err = trackdb_env->set_alloc(trackdb_env,
+                                   xmalloc_noptr, xrealloc_noptr, xfree)))
+    fatal(0, "trackdb_env->set_alloc: %s", db_strerror(err));
+  if((err = trackdb_env->set_lk_max_locks(trackdb_env, 10000)))
+    fatal(0, "trackdb_env->set_lk_max_locks: %s", db_strerror(err));
+  if((err = trackdb_env->set_lk_max_objects(trackdb_env, 10000)))
+    fatal(0, "trackdb_env->set_lk_max_objects: %s", db_strerror(err));
+  if((err = trackdb_env->open(trackdb_env, config->home,
+                              DB_INIT_LOG
+                              |DB_INIT_LOCK
+                              |DB_INIT_MPOOL
+                              |DB_INIT_TXN
+                              |DB_CREATE
+                              |recover_type[recover],
+                              0666)))
+    fatal(0, "trackdb_env->open: %s", db_strerror(err));
+  trackdb_env->set_errpfx(trackdb_env, "DB");
+  trackdb_env->set_errfile(trackdb_env, stderr);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_DEADLOCK, 1);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_RECOVERY, 1);
+  trackdb_env->set_verbose(trackdb_env, DB_VERB_REPLICATION, 1);
+  D(("initialized database environment"));
+}
+
+/* called when deadlock manager terminates */
+static int reap_db_deadlock(ev_source attribute((unused)) *ev,
+                            pid_t attribute((unused)) pid,
+                            int status,
+                            const struct rusage attribute((unused)) *rusage,
+                            void attribute((unused)) *u) {
+  db_deadlock_pid = -1;
+  if(initialized)
+    fatal(0, "deadlock manager unexpectedly terminated: %s",
+          wstat(status));
+  else
+    D(("deadlock manager terminated: %s", wstat(status)));
+  return 0;
+}
+
+static pid_t subprogram(ev_source *ev, const char *prog) {
+  pid_t pid;
+  int lfd;
+
+  /* If we're in the background then trap subprocess stdout/stderr */
+  if(!isatty(2))
+    lfd = logfd(ev, prog);
+  else
+    lfd = -1;
+  if(!(pid = xfork())) {
+    exitfn = _exit;
+    ev_signal_atfork(ev);
+    signal(SIGPIPE, SIG_DFL);
+    if(lfd != -1) {
+      xdup2(lfd, 1);
+      xdup2(lfd, 2);
+    }
+    /* If we were negatively niced, undo it.  We don't bother checking for
+     * error, it's not that important. */
+    setpriority(PRIO_PROCESS, 0, 0);
+    execlp(prog, prog, "--config", configfile,
+           debugging ? "--debug" : "--no-debug",
+           (char *)0);
+    fatal(errno, "error invoking %s", prog);
+  }
+  if(lfd != -1) xclose(lfd);
+  return pid;
+}
+
+/* start deadlock manager */
+void trackdb_master(ev_source *ev) {
+  assert(db_deadlock_pid == -1);
+  db_deadlock_pid = subprogram(ev, DEADLOCK);
+  ev_child(ev, db_deadlock_pid, 0, reap_db_deadlock, 0);
+  D(("started deadlock manager"));
+}
+
+/* close environment */
+void trackdb_deinit(void) {
+  int err;
+
+  /* sanity checks */
+  assert(initialized == 1);
+  --initialized;
+
+  /* close the environment */
+  if((err = trackdb_env->close(trackdb_env, 0)))
+    fatal(0, "trackdb_env->close: %s", db_strerror(err));
+
+  if(rescan_pid != -1 && kill(rescan_pid, SIGTERM) < 0)
+    fatal(errno, "error killing rescanner");
+
+  /* terminate the deadlock manager */
+  if(db_deadlock_pid != -1 && kill(db_deadlock_pid, SIGTERM) < 0)
+    fatal(errno, "error killing deadlock manager");
+  db_deadlock_pid = -1;
+
+  D(("deinitialized database environment"));
+}
+
+/* open a specific database */
+static DB *open_db(const char *path,
+                   u_int32_t dbflags,
+                   DBTYPE dbtype,
+                   u_int32_t openflags,
+                   int mode) {
+  int err;
+  DB *db;
+
+  D(("open %s", path));
+  path = config_get_file(path);
+  if((err = db_create(&db, trackdb_env, 0)))
+    fatal(0, "db_create %s: %s", path, db_strerror(err));
+  if(dbflags)
+    if((err = db->set_flags(db, dbflags)))
+      fatal(0, "db->set_flags %s: %s", path, db_strerror(err));
+  if(dbtype == DB_BTREE)
+    if((err = db->set_bt_compare(db, compare)))
+      fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err));
+  if((err = db->open(db, 0, path, 0, dbtype,
+                     openflags | DB_AUTO_COMMIT, mode)))
+    fatal(0, "db->open %s: %s", path, db_strerror(err));
+  return db;
+}
+
+/* open track databases */
+void trackdb_open(void) {
+  /* sanity checks */
+  assert(opened == 0);
+  ++opened;
+  /* open the databases */
+  trackdb_tracksdb = open_db("tracks.db",
+                             DB_RECNUM, DB_BTREE, DB_CREATE, 0666);
+  trackdb_searchdb = open_db("search.db",
+                             DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+  trackdb_tagsdb = open_db("tags.db",
+                           DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+  trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, DB_CREATE, 0666);
+  trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666);
+  D(("opened databases"));
+}
+
+/* close track databases */
+void trackdb_close(void) {
+  int err;
+  
+  /* sanity checks */
+  assert(opened == 1);
+  --opened;
+  if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0)))
+    fatal(0, "error closing tracks.db: %s", db_strerror(err));
+  if((err = trackdb_searchdb->close(trackdb_searchdb, 0)))
+    fatal(0, "error closing search.db: %s", db_strerror(err));
+  if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0)))
+    fatal(0, "error closing tags.db: %s", db_strerror(err));
+  if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0)))
+    fatal(0, "error closing prefs.db: %s", db_strerror(err));
+  if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
+    fatal(0, "error closing global.db: %s", db_strerror(err));
+  trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
+  trackdb_tagsdb = trackdb_globaldb = 0;
+  D(("closed databases"));
+}
+
+/* generic db routines *******************************************************/
+
+/* fetch and decode a database entry.  Returns 0, DB_NOTFOUND or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_getdata(DB *db,
+                    const char *track,
+                    struct kvp **kp,
+                    DB_TXN *tid) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->get(db, tid, make_key(&key, track),
+                       prepare_data(&data), 0)) {
+  case 0:
+    *kp = kvp_urldecode(data.data, data.size);
+    return 0;
+  case DB_NOTFOUND:
+    *kp = 0;
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error querying database: %s", db_strerror(err));
+  }
+}
+
+/* encode and store a database entry.  Returns 0, DB_KEYEXIST or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_putdata(DB *db,
+                    const char *track,
+                    const struct kvp *k,
+                    DB_TXN *tid,
+                    u_int32_t flags) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->put(db, tid, make_key(&key, track),
+                       encode_data(&data, k), flags)) {
+  case 0:
+  case DB_KEYEXIST:
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* delete a database entry */
+int trackdb_delkey(DB *db,
+                   const char *track,
+                   DB_TXN *tid) {
+  int err;
+
+  DBT key;
+  switch(err = db->del(db, tid, make_key(&key, track), 0)) {
+  case 0:
+  case DB_NOTFOUND:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* open a database cursor */
+DBC *trackdb_opencursor(DB *db, DB_TXN *tid) {
+  int err;
+  DBC *c;
+
+  switch(err = db->cursor(db, tid, &c, 0)) {
+  case 0: break;
+  default: fatal(0, "error creating cursor: %s", db_strerror(err));
+  }
+  return c;
+}
+
+/* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */
+int trackdb_closecursor(DBC *c) {
+  int err;
+
+  if(!c) return 0;
+  switch(err = c->c_close(c)) {
+  case 0:
+    return err;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error closing cursor: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error closing cursor: %s", db_strerror(err));
+  }
+}
+
+/* delete a (key,data) pair.  Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+int trackdb_delkeydata(DB *db,
+                       const char *word,
+                       const char *track,
+                       DB_TXN *tid) {
+  int err;
+  DBC *c;
+  DBT key, data;
+
+  c = trackdb_opencursor(db, tid);
+  switch(err = c->c_get(c, make_key(&key, word),
+                        make_key(&data, track), DB_GET_BOTH)) {
+  case 0:
+    switch(err = c->c_del(c, 0)) {
+    case 0:
+      break;
+    case DB_KEYEMPTY:
+      err = 0;
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error updating database: %s", db_strerror(err));
+      break;
+    default:
+      fatal(0, "c->c_del: %s", db_strerror(err));
+    }
+    break;
+  case DB_NOTFOUND:
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+  if(trackdb_closecursor(c)) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* start a transaction */
+DB_TXN *trackdb_begin_transaction(void) {
+  DB_TXN *tid;
+  int err;
+
+  if((err = trackdb_env->txn_begin(trackdb_env, 0, &tid, 0)))
+    fatal(0, "trackdb_env->txn_begin: %s", db_strerror(err));
+  return tid;
+}
+
+/* abort transaction */
+void trackdb_abort_transaction(DB_TXN *tid) {
+  int err;
+
+  if(tid)
+    if((err = tid->abort(tid)))
+      fatal(0, "tid->abort: %s", db_strerror(err));
+}
+
+/* commit transaction */
+void trackdb_commit_transaction(DB_TXN *tid) {
+  int err;
+
+  if((err = tid->commit(tid, 0)))
+    fatal(0, "tid->commit: %s", db_strerror(err));
+}
+
+/* search/tags shared code ***************************************************/
+
+/* comparison function used by dedupe() */
+static int wordcmp(const void *a, const void *b) {
+  return strcmp(*(const char **)a, *(const char **)b);
+}
+
+/* sort and de-dupe VEC */
+static char **dedupe(char **vec, int nvec) {
+  int m, n;
+
+  qsort(vec, nvec, sizeof (char *), wordcmp);
+  m = n = 0;
+  if(nvec) {
+    vec[m++] = vec[0];
+    for(n = 1; n < nvec; ++n)
+      if(strcmp(vec[n], vec[m - 1]))
+	vec[m++] = vec[n];
+  }
+  vec[m] = 0;
+  return vec;
+}
+
+/* update a key/track database.  Returns 0 or DB_DEADLOCK. */
+static int register_word(DB *db, const char *what,
+                         const char *track, const char *word,
+                         DB_TXN *tid) {
+  int err;
+  DBT key, data;
+
+  switch(err = db->put(db, tid, make_key(&key, word),
+                       make_key(&data, track), DB_NODUPDATA)) {
+  case 0:
+  case DB_KEYEXIST:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating %s.db: %s", what, db_strerror(err));
+    return err;
+  default:
+    fatal(0, "error updating %s.db: %s", what,  db_strerror(err));
+  }
+}
+
+/* search primitives *********************************************************/
+
+/* return true iff NAME is a trackname_display_ pref */
+static int is_display_pref(const char *name) {
+  static const char prefix[] = "trackname_display_";
+  return !strncmp(name, prefix, (sizeof prefix) - 1);
+}
+
+/* compute the words of a track name */
+static char **track_to_words(const char *track,
+                             const struct kvp *p) {
+  struct vector v;
+  char **w;
+  int nw;
+
+  vector_init(&v);
+  if((w = words(casefold(strip_extension(track_rootless(track))), &nw)))
+    vector_append_many(&v, w, nw);
+
+  for(; p; p = p->next)
+    if(is_display_pref(p->name))
+      if((w = words(casefold(p->value), &nw)))
+        vector_append_many(&v, w, nw);
+  vector_terminate(&v);
+  return dedupe(v.vec, v.nvec);
+}
+
+/* return nonzero iff WORD is a stopword */
+static int stopword(const char *word) {
+  int n;
+
+  for(n = 0; n < config->stopword.n
+        && strcmp(word, config->stopword.s[n]); ++n)
+    ;
+  return n < config->stopword.n;
+}
+
+/* record that WORD appears in TRACK.  Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_search_word(const char *track, const char *word,
+                                DB_TXN *tid) {
+  if(stopword(word)) return 0;
+  return register_word(trackdb_searchdb, "search", track, word, tid);
+}
+
+/* Tags **********************************************************************/
+
+/* Return nonzero if C is a valid tag character */
+static int tagchar(int c) {
+  switch(c) {
+  case ',':
+    return 0;
+  default:
+    return c >= ' ';
+  }
+}
+
+/* Parse and de-dupe a tag list.  If S=0 then assumes "". */
+static char **parsetags(const char *s) {
+  const char *t;
+  struct vector v;
+
+  vector_init(&v);
+  if(s) {
+    /* skip initial separators */
+    while(*s && (!tagchar(*s) || *s == ' '))
+      ++s;
+    while(*s) {
+      /* find the extent of the tag */
+      t = s;
+      while(*s && tagchar(*s))
+        ++s;
+      /* strip trailing spaces */
+      while(s > t && s[-1] == ' ')
+        --s;
+      vector_append(&v, xstrndup(t, s - t));
+      /* skip intermediate and trailing separators */
+      while(*s && (!tagchar(*s) || *s == ' '))
+        ++s;
+    }
+  }
+  vector_terminate(&v);
+  return dedupe(v.vec, v.nvec);
+}
+
+/* Record that TRACK has TAG.  Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_tag(const char *track, const char *tag, DB_TXN *tid) {
+  return register_word(trackdb_tagsdb, "tags", track, tag, tid);
+}
+
+/* aliases *******************************************************************/
+
+/* compute the alias and store at aliasp.  Returns 0 or DB_LOCK_DEADLOCK.  If
+ * there is no alias sets *aliasp to 0. */
+static int compute_alias(char **aliasp,
+                         const char *track,
+                         const struct kvp *p,
+                         DB_TXN *tid) {
+  struct dynstr d;
+  const char *s = config->alias, *t, *expansion, *part;
+  int c, used_db = 0, slash_prefix, err;
+  struct kvp *at;
+
+  if(strstr(track, "Troggs"))
+    D(("computing alias for %s", track));
+  dynstr_init(&d);
+  dynstr_append_string(&d, find_track_root(track));
+  while((c = (unsigned char)*s++)) {
+    if(c != '{') {
+      dynstr_append(&d, c);
+      continue;
+    }
+    if((slash_prefix = (*s == '/')))
+      s++;
+    t = strchr(s, '}');
+    assert(t != 0);			/* validated at startup */
+    part = xstrndup(s, t - s);
+    expansion = getpart(track, "display", part, p, &used_db);
+    if(*expansion) {
+      if(slash_prefix) dynstr_append(&d, '/');
+      dynstr_append_string(&d, expansion);
+    }
+    s = t + 1;				/* skip {part} */
+  }
+  /* only admit to the alias if we used the db... */
+  if(!used_db) {
+    *aliasp = 0;
+    return 0;
+  }
+  dynstr_terminate(&d);
+  /* ...and the answer differs from the original... */
+  if(!strcmp(track, d.vec)) {
+    *aliasp = 0;
+    return 0;
+  }
+  /* ...and there isn't already a different track with that name (including as
+   * an alias) */
+  switch(err = trackdb_getdata(trackdb_tracksdb, d.vec, &at, tid)) {
+  case 0:
+    if(strstr(track, "Troggs"))
+      D(("found a hit for alias"));
+    if((s = kvp_get(at, "_alias_for"))
+       && !strcmp(s, track)) {
+    case DB_NOTFOUND:
+      if(strstr(track, "Troggs"))
+        D(("accepting anyway"));
+      *aliasp = d.vec;
+    } else {
+      if(strstr(track, "Troggs")) {
+        D(("rejecting"));
+        D(("%s", track));
+        D(("%s", s ? s : "(null)"));
+      }
+      *aliasp = 0;
+    }
+    return 0;
+  default:
+    return err;
+  }
+}
+
+/* get track and prefs data (if tp/pp not null pointers).  Returns 0 on
+ * success, DB_NOTFOUND if the track does not exist or DB_LOCK_DEADLOCK.
+ * Always sets the return values, even if only to null pointers. */
+static int gettrackdata(const char *track,
+                        struct kvp **tp,
+                        struct kvp **pp,
+                        const char **actualp,
+                        unsigned flags,
+#define GTD_NOALIAS 0x0001
+                        DB_TXN *tid) {
+  int err;
+  const char *actual = track;
+  struct kvp *t = 0, *p = 0;
+  
+  if((err = trackdb_getdata(trackdb_tracksdb, track, &t, tid))) goto done;
+  if((actual = kvp_get(t, "_alias_for"))) {
+    if(flags & GTD_NOALIAS) {
+      error(0, "alias passed to gettrackdata where real path required");
+      abort();
+    }
+    if((err = trackdb_getdata(trackdb_tracksdb, actual, &t, tid))) goto done;
+  } else
+    actual = track;
+  assert(actual != 0);
+  if(pp) {
+    if((err = trackdb_getdata(trackdb_prefsdb, actual, &p, tid)) == DB_LOCK_DEADLOCK)
+      goto done;
+  }
+  err = 0;
+done:
+  if(actualp) *actualp = actual;
+  if(tp) *tp = t;
+  if(pp) *pp = p;
+  return err;
+}
+
+/* trackdb_notice() **********************************************************/
+
+/* notice a track */
+int trackdb_notice(const char *track,
+                   const char *path) {
+  int err;
+  DB_TXN *tid;
+  
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    err = trackdb_notice_tid(track, path, tid);
+    if((err == DB_LOCK_DEADLOCK)) goto fail;
+    break;
+  fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return err;
+}
+
+int trackdb_notice_tid(const char *track,
+                       const char *path,
+                       DB_TXN *tid) {
+  int err, n;
+  struct kvp *t, *a, *p;
+  int t_changed, ret;
+  char *alias, **w;
+  
+  /* notice whether the tracks.db entry changes */
+  t_changed = 0;
+  /* get any existing tracks entry */
+  if((err = gettrackdata(track, &t, &p, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+    return err;
+  ret = err;
+  /* this is a real track */
+  t_changed += kvp_set(&t, "_alias_for", 0);
+  t_changed += kvp_set(&t, "_path", path);
+  /* if we have an alias record it in the database */
+  if((err = compute_alias(&alias, track, p, tid))) return err;
+  if(alias) {
+    /* won't overwrite someone else's alias as compute_alias() checks */
+    D(("%s: alias %s", track, alias));
+    a = 0;
+    kvp_set(&a, "_alias_for", track);
+    if((err = trackdb_putdata(trackdb_tracksdb, alias, a, tid, 0))) return err;
+  }
+  /* update search.db */
+  w = track_to_words(track, p);
+  for(n = 0; w[n]; ++n)
+    if((err = register_search_word(track, w[n], tid)))
+      return err;
+  /* update tags.db */
+  w = parsetags(kvp_get(p, "tags"));
+  for(n = 0; w[n]; ++n)
+    if((err = register_tag(track, w[n], tid)))
+      return err;
+  reqtracks = 0;
+  /* only store the tracks.db entry if it has changed */
+  if(t_changed && (err = trackdb_putdata(trackdb_tracksdb, track, t, tid, 0)))
+    return err;
+  return ret;
+}
+
+/* trackdb_obsolete() ********************************************************/
+
+/* obsolete a track */
+int trackdb_obsolete(const char *track, DB_TXN *tid) {
+  int err, n;
+  struct kvp *p;
+  char *alias, **w;
+
+  if((err = gettrackdata(track, 0, &p, 0,
+                         GTD_NOALIAS, tid)) == DB_LOCK_DEADLOCK)
+    return err;
+  else if(err == DB_NOTFOUND) return 0;
+  /* compute the alias, if any, and delete it */
+  if(compute_alias(&alias, track, p, tid)) return err;
+  if(alias) {
+    /* if the alias points to some other track then compute_alias won't
+     * return it */
+    if(trackdb_delkey(trackdb_tracksdb, alias, tid))
+      return err;
+  }
+  /* update search.db */
+  w = track_to_words(track, p);
+  for(n = 0; w[n]; ++n)
+    if(trackdb_delkeydata(trackdb_searchdb,
+                          w[n], track, tid) == DB_LOCK_DEADLOCK)
+      return err;
+  /* update tags.db */
+  w = parsetags(kvp_get(p, "tags"));
+  for(n = 0; w[n]; ++n)
+    if(trackdb_delkeydata(trackdb_tagsdb,
+                          w[n], track, tid) == DB_LOCK_DEADLOCK)
+      return err;
+  reqtracks = 0;
+  /* update tracks.db */
+  if(trackdb_delkey(trackdb_tracksdb, track, tid) == DB_LOCK_DEADLOCK)
+    return err;
+  /* We don't delete the prefs, so they survive temporary outages of the
+   * (possibly virtual) track filesystem */
+  return 0;
+}
+
+/* trackdb_stats() ***********************************************************/
+
+#define H(name) { #name, offsetof(DB_HASH_STAT, name) }
+#define B(name) { #name, offsetof(DB_BTREE_STAT, name) }
+
+static const struct statinfo {
+  const char *name;
+  size_t offset;
+} statinfo_hash[] = {
+  H(hash_magic),
+  H(hash_version),
+  H(hash_nkeys),
+  H(hash_ndata),
+  H(hash_pagesize),
+  H(hash_ffactor),
+  H(hash_buckets),
+  H(hash_free),
+  H(hash_bfree),
+  H(hash_bigpages),
+  H(hash_big_bfree),
+  H(hash_overflows),
+  H(hash_ovfl_free),
+  H(hash_dup),
+  H(hash_dup_free),
+}, statinfo_btree[] = {
+  B(bt_magic),
+  B(bt_version),
+  B(bt_nkeys),
+  B(bt_ndata),
+  B(bt_pagesize),
+  B(bt_minkey),
+  B(bt_re_len),
+  B(bt_re_pad),
+  B(bt_levels),
+  B(bt_int_pg),
+  B(bt_leaf_pg),
+  B(bt_dup_pg),
+  B(bt_over_pg),
+  B(bt_free),
+  B(bt_int_pgfree),
+  B(bt_leaf_pgfree),
+  B(bt_dup_pgfree),
+  B(bt_over_pgfree),
+};
+
+/* look up stats for DB */
+static int get_stats(struct vector *v,
+                     DB *database,
+                     const struct statinfo *si,
+                     size_t nsi,
+                     DB_TXN *tid) {
+  void *sp;
+  size_t n;
+  char *str;
+  int err;
+
+  if(database) {
+    switch(err = database->stat(database, tid, &sp, 0)) {
+    case 0:
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error querying database: %s", db_strerror(err));
+      return err;
+    default:
+      fatal(0, "error querying database: %s", db_strerror(err));
+    }
+    for(n = 0; n < nsi; ++n) {
+      byte_xasprintf(&str, "%s=%"PRIuMAX, si[n].name,
+		     (uintmax_t)*(u_int32_t *)((char *)sp + si[n].offset));
+      vector_append(v, str);
+    }
+  }
+  return 0;
+}
+
+struct search_entry {
+  char *word;
+  int n;
+};
+
+/* find the top COUNT words in the search database */
+static int search_league(struct vector *v, int count, DB_TXN *tid) {
+  struct search_entry *se;
+  DBT k, d;
+  DBC *cursor;
+  int err, n = 0, nse = 0, i;
+  char *word = 0;
+  size_t wl = 0;
+  char *str;
+
+  cursor = trackdb_opencursor(trackdb_searchdb, tid);
+  se = xmalloc(count * sizeof *se);
+  while(!(err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                              DB_NEXT))) {
+    if(word && wl == k.size && !strncmp(word, k.data, wl))
+      ++n;
+    else {
+#define FINALIZE() do {						\
+  if(word && (nse < count || n > se[nse - 1].n)) {		\
+    if(nse == count)						\
+      i = nse - 1;						\
+    else							\
+      i = nse++;						\
+    while(i > 0 && n > se[i - 1].n)				\
+      --i;							\
+    memmove(&se[i + 1], &se[i], (nse - i) * sizeof *se);	\
+    se[i].word = word;						\
+    se[i].n = n;						\
+  }								\
+} while(0)
+      FINALIZE();
+      word = xstrndup(k.data, wl = k.size);
+      n = 1;
+    }
+  }
+  switch(err) {
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying search database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error querying search database: %s", db_strerror(err));
+  }
+  if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+  if(err) return err;
+  FINALIZE();
+  byte_xasprintf(&str, "Top %d search words:", nse);
+  vector_append(v, str);
+  for(i = 0; i < nse; ++i) {
+    byte_xasprintf(&str, "%4d: %5d %s", i + 1, se[i].n, se[i].word);
+    vector_append(v, str);
+  }
+  return 0;
+}
+
+#define SI(what) statinfo_##what, \
+                 sizeof statinfo_##what / sizeof (struct statinfo)
+
+/* return a list of database stats */
+char **trackdb_stats(int *nstatsp) {
+  DB_TXN *tid;
+  struct vector v;
+  char *s;
+  
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    v.nvec = 0;
+    vector_append(&v, (char *)"Tracks database stats:");
+    if(get_stats(&v, trackdb_tracksdb, SI(btree), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Search database stats:");
+    if(get_stats(&v, trackdb_searchdb, SI(hash), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Prefs database stats:");
+    if(get_stats(&v, trackdb_prefsdb, SI(hash), tid)) goto fail;
+    vector_append(&v, (char *)"");
+    if(search_league(&v, 10, tid)) goto fail;
+    vector_append(&v, (char *)"");
+    vector_append(&v, (char *)"Server stats:");
+    byte_xasprintf(&s, "track lookup cache hits: %lu", cache_files_hits);
+    vector_append(&v, (char *)s);
+    byte_xasprintf(&s, "track lookup cache misses: %lu", cache_files_misses);
+    vector_append(&v, (char *)s);
+    vector_terminate(&v);
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(nstatsp) *nstatsp = v.nvec;
+  return v.vec;
+}
+
+/* set a pref (remove if value=0) */
+int trackdb_set(const char *track,
+                const char *name,
+                const char *value) {
+  struct kvp *t, *p, *a;
+  DB_TXN *tid;
+  int err, cmp;
+  char *oldalias, *newalias, **oldtags = 0, **newtags;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = gettrackdata(track, &t, &p, 0,
+                           0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    if(err == DB_NOTFOUND) break;
+    if(name[0] == '_') {
+      if(kvp_set(&t, name, value))
+        if(trackdb_putdata(trackdb_tracksdb, track, t, tid, 0))
+          goto fail;
+    } else {
+      /* get the old alias name */
+      if(compute_alias(&oldalias, track, p, tid)) goto fail;
+      /* get the old tags */
+      if(!strcmp(name, "tags"))
+        oldtags = parsetags(kvp_get(p, "tags"));
+      /* set the value */
+      if(kvp_set(&p, name, value))
+        if(trackdb_putdata(trackdb_prefsdb, track, p, tid, 0))
+          goto fail;
+      /* compute the new alias name */
+      if((err = compute_alias(&newalias, track, p, tid))) goto fail;
+      /* check whether alias has changed */
+      if(!(oldalias == newalias
+           || (oldalias && newalias && !strcmp(oldalias, newalias)))) {
+        /* adjust alias records to fit change */
+        if(oldalias
+           && trackdb_delkey(trackdb_tracksdb, oldalias, tid)) goto fail;
+        if(newalias) {
+          a = 0;
+          kvp_set(&a, "_alias_for", track);
+          if(trackdb_putdata(trackdb_tracksdb, newalias, a, tid, 0)) goto fail;
+        }
+      }
+      /* check whether tags have changed */
+      if(!strcmp(name, "tags")) {
+        newtags = parsetags(value);
+        while(*oldtags || *newtags) {
+          if(*oldtags && *newtags) {
+            cmp = strcmp(*oldtags, *newtags);
+            if(!cmp) {
+              /* keeping this tag */
+              ++oldtags;
+              ++newtags;
+            } else if(cmp < 0)
+              /* old tag fits into a gap in the new list, so delete old */
+              goto delete_old;
+            else
+              /* new tag fits into a gap in the old list, so insert new */
+              goto insert_new;
+          } else if(*oldtags) {
+            /* we've run out of new tags, so remaining old ones are to be
+             * deleted */
+          delete_old:
+            if(trackdb_delkeydata(trackdb_tagsdb,
+                                  *oldtags, track, tid) == DB_LOCK_DEADLOCK)
+              goto fail;
+            ++oldtags;
+          } else {
+            /* we've run out of old tags, so remainig new ones are to be
+             * inserted */
+          insert_new:
+            if(register_tag(track, *newtags, tid)) goto fail;
+            ++newtags;
+          }
+        }
+        reqtracks = 0;
+      }
+    }
+    err = 0;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return err == 0 ? 0 : -1;
+}
+
+/* get a pref */
+const char *trackdb_get(const char *track,
+                        const char *name) {
+  return kvp_get(trackdb_get_all(track), name);
+}
+
+/* get all prefs as a 0-terminated array */
+struct kvp *trackdb_get_all(const char *track) {
+  struct kvp *t, *p, **pp;
+  DB_TXN *tid;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  for(pp = &p; *pp; pp = &(*pp)->next)
+    ;
+  *pp = t;
+  return p;
+}
+
+/* resolve alias */
+const char *trackdb_resolve(const char *track) {
+  DB_TXN *tid;
+  const char *actual;
+  
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, 0, 0, &actual, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return actual;
+}
+
+int trackdb_isalias(const char *track) {
+  const char *actual = trackdb_resolve(track);
+
+  return strcmp(actual, track);
+}
+
+/* test whether a track exists (perhaps an alias) */
+int trackdb_exists(const char *track) {
+  DB_TXN *tid;
+  int err;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    /* unusually, here we want the return value */
+    if((err = gettrackdata(track, 0, 0, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return (err == 0);
+}
+
+/* return the list of tags */
+char **trackdb_alltags(void) {
+  DB_TXN *tid;
+  int err;
+  char **taglist;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    err = trackdb_alltags_tid(tid, &taglist);
+    if(!err) break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return taglist;
+}
+
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp) {
+  struct vector v;
+  DBC *c;
+  DBT k, d;
+  int err;
+
+  vector_init(&v);
+  c = trackdb_opencursor(trackdb_tagsdb, tid);
+  memset(&k, 0, sizeof k);
+  while(!(err = c->c_get(c, &k, prepare_data(&d), DB_NEXT_NODUP)))
+    vector_append(&v, xstrndup(k.data, k.size));
+  switch(err) {
+  case DB_NOTFOUND:
+    break;
+  case DB_LOCK_DEADLOCK:
+      return err;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+  if((err = trackdb_closecursor(c))) return err;
+  vector_terminate(&v);
+  *taglistp = v.vec;
+  return 0;
+}
+
+/* return 1 iff sorted tag lists A and B have at least one member in common */
+static int tag_intersection(char **a, char **b) {
+  int cmp;
+
+  /* Same sort of logic as trackdb_set() above */
+  while(*a && *b) {
+    if(!(cmp = strcmp(*a, *b))) return 1;
+    else if(cmp < 0) ++a;
+    else ++b;
+  }
+  return 0;
+}
+
+/* Check whether a track is suitable for random play.  Returns 0 if it is,
+ * DB_NOTFOUND if it or DB_LOCK_DEADLOCK. */
+static int check_suitable(const char *track,
+                          DB_TXN *tid,
+                          char **required_tags,
+                          char **prohibited_tags) {
+  char **track_tags;
+  time_t last, now;
+  struct kvp *p, *t;
+  const char *pick_at_random, *played_time;
+
+  /* don't pick aliases - only pick the canonical form */
+  if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+    return DB_LOCK_DEADLOCK;
+  if(kvp_get(t, "_alias_for"))
+    return DB_NOTFOUND;
+  /* check that random play is not suppressed for this track */
+  if((pick_at_random = kvp_get(p, "pick_at_random"))
+     && !strcmp(pick_at_random, "0"))
+    return DB_NOTFOUND;
+  /* don't pick a track that's been played in the last 8 hours */
+  if((played_time = kvp_get(p, "played_time"))) {
+    last = atoll(played_time);
+    now = time(0);
+    if(now < last + 8 * 3600)       /* TODO configurable */
+      return DB_NOTFOUND;
+  }
+  track_tags = parsetags(kvp_get(p, "tags"));
+  /* check that no prohibited tag is present for this track */
+  if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
+    return DB_NOTFOUND;
+  /* check that at least one required tags is present for this track */
+  if(*required_tags && !tag_intersection(track_tags, required_tags))
+    return DB_NOTFOUND;
+  return 0;
+}
+
+/* attempt to pick a random non-alias track */
+const char *trackdb_random(int tries) {
+  DBT key, data;
+  DB_BTREE_STAT *sp;
+  int err, n;
+  DB_TXN *tid;
+  const char *track, *candidate;
+  db_recno_t r;
+  const char *tags;
+  char **required_tags, **prohibited_tags, **tp;
+  hash *h;
+  DBC *c = 0;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = trackdb_get_global_tid("required-tags", tid, &tags)))
+      goto fail;
+    required_tags = parsetags(tags);
+    if((err = trackdb_get_global_tid("prohibited-tags", tid, &tags)))
+      goto fail;
+    prohibited_tags = parsetags(tags);
+    track = 0;
+    if(*required_tags) {
+      /* Bung all the suitable tracks into a hash and convert to a list of keys
+       * (to eliminate duplicates).  We cache this list since it is possible
+       * that it will be very large. */
+      if(!reqtracks) {
+        h = hash_new(0);
+        for(tp = required_tags; *tp; ++tp) {
+          c = trackdb_opencursor(trackdb_tagsdb, tid);
+          memset(&key, 0, sizeof key);
+          key.data = *tp;
+          key.size = strlen(*tp);
+          n = 0;
+          err = c->c_get(c, &key, prepare_data(&data), DB_SET);
+          while(err == 0) {
+            hash_add(h, xstrndup(data.data, data.size), 0,
+                     HASH_INSERT_OR_REPLACE);
+            ++n;
+            err = c->c_get(c, &key, prepare_data(&data), DB_NEXT_DUP);
+          }
+          switch(err) {
+          case 0:
+          case DB_NOTFOUND:
+            break;
+          case DB_LOCK_DEADLOCK:
+            goto fail;
+          default:
+            fatal(0, "error querying tags.db: %s", db_strerror(err));
+          }
+          trackdb_closecursor(c);
+          c = 0;
+          if(!n)
+            error(0, "required tag %s does not match any tracks", *tp);
+        }
+        nreqtracks = hash_count(h);
+        reqtracks = hash_keys(h);
+      }
+      while(nreqtracks && !track && tries-- > 0) {
+        r = (rand() * (double)nreqtracks / (RAND_MAX + 1.0));
+        candidate = reqtracks[r];
+        switch(check_suitable(candidate, tid,
+                              required_tags, prohibited_tags)) {
+        case 0:
+          track = candidate;
+          break;
+        case DB_NOTFOUND:
+          break;
+        case DB_LOCK_DEADLOCK:
+          goto fail;
+        }
+      }
+    } else {
+      /* No required tags.  We pick random record numbers in the database
+       * instead. */
+      switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp,
+                                          DB_RECORDCOUNT)) {
+      case 0:
+        break;
+      case DB_LOCK_DEADLOCK:
+        error(0, "error querying tracks.db: %s", db_strerror(err));
+        goto fail;
+      default:
+        fatal(0, "error querying tracks.db: %s", db_strerror(err));
+      }
+      if(!sp->bt_nkeys)
+        error(0, "cannot pick tracks at random from an empty database");
+      while(sp->bt_nkeys && !track && tries-- > 0) {
+        /* record numbers count from 1 upwards */
+        r = 1 + (rand() * (double)sp->bt_nkeys / (RAND_MAX + 1.0));
+        memset(&key, sizeof key, 0);
+        key.flags = DB_DBT_MALLOC;
+        key.size = sizeof r;
+        key.data = &r;
+        switch(err = trackdb_tracksdb->get(trackdb_tracksdb, tid, &key, prepare_data(&data),
+                                           DB_SET_RECNO)) {
+        case 0:
+          break;
+        case DB_LOCK_DEADLOCK:
+          error(0, "error querying tracks.db: %s", db_strerror(err));
+          goto fail;
+        default:
+          fatal(0, "error querying tracks.db: %s", db_strerror(err));
+        }
+        candidate = xstrndup(key.data, key.size);
+        switch(check_suitable(candidate, tid,
+                              required_tags, prohibited_tags)) {
+        case 0:
+          track = candidate;
+          break;
+        case DB_NOTFOUND:
+          break;
+        case DB_LOCK_DEADLOCK:
+          goto fail;
+        }
+      }
+    }
+    break;
+fail:
+    trackdb_closecursor(c);
+    c = 0;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(!track)
+    error(0, "could not pick a random track");
+  return track;
+}
+
+/* get a track name given the prefs.  Set *used_db to 1 if we got the answer
+ * from the prefs. */
+static const char *getpart(const char *track,
+                           const char *context,
+                           const char *part,
+                           const struct kvp *p,
+                           int *used_db) {
+  const char *result;
+  char *pref;
+
+  byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+  if((result = kvp_get(p, pref)))
+    *used_db = 1;
+  else
+    result = trackname_part(track, context, part);
+  assert(result != 0);
+  return result;
+}
+
+/* get a track name part, like trackname_part(), but taking the database into
+ * account. */
+const char *trackdb_getpart(const char *track,
+                            const char *context,
+                            const char *part) {
+  struct kvp *p;
+  DB_TXN *tid;
+  char *pref;
+  const char *actual;
+  int used_db, err;
+
+  /* construct the full pref */
+  byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if((err = gettrackdata(track, 0, &p, &actual, 0, tid)) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return getpart(actual, context, part, p, &used_db);
+}
+
+/* get the raw path name for @track@ (might be an alias) */
+const char *trackdb_rawpath(const char *track) {
+  DB_TXN *tid;
+  struct kvp *t;
+  const char *path;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(gettrackdata(track, &t, 0, 0, 0, tid) == DB_LOCK_DEADLOCK)
+      goto fail;
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  if(!(path = kvp_get(t, "_path"))) path = track;
+  return path;
+}
+
+/* trackdb_list **************************************************************/
+
+/* this is incredibly ugly, sorry, perhaps it will be rewritten to be actually
+ * readable at some point */
+
+/* return true if the basename of TRACK[0..TL-1], as defined by DL, matches RE.
+ * If RE is a null pointer then it matches everything. */
+static int track_matches(size_t dl, const char *track, size_t tl,
+			 const pcre *re) {
+  int ovec[3], rc;
+
+  if(!re)
+    return 1;
+  track += dl + 1;
+  tl -= (dl + 1);
+  switch(rc = pcre_exec(re, 0, track, tl, 0, 0, ovec, 3)) {
+  case PCRE_ERROR_NOMATCH: return 0;
+  default:
+    if(rc < 0) {
+      error(0, "pcre_exec returned %d, subject '%s'", rc, track);
+      return 0;
+    }
+    return 1;
+  }
+}
+
+static int do_list(struct vector *v, const char *dir,
+                   enum trackdb_listable what, const pcre *re, DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  size_t dl;
+  char *ptr;
+  int err;
+  size_t l, last_dir_len = 0;
+  char *last_dir = 0, *track, *alias;
+  struct kvp *p;
+  
+  dl = strlen(dir);
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  make_key(&k, dir);
+  prepare_data(&d);
+  /* find the first key >= dir */
+  err = cursor->c_get(cursor, &k, &d, DB_SET_RANGE);
+  /* keep going while we're dealing with  */
+  while(err == 0
+	&& k.size > dl
+	&& ((char *)k.data)[dl] == '/'
+	&& !memcmp(k.data, dir, dl)) {
+    ptr = memchr((char *)k.data + dl + 1, '/', k.size - (dl + 1));
+    if(ptr) {
+      /* we have , so  is a directory */
+      l = ptr - (char *)k.data;
+      if(what & trackdb_directories)
+	if(!(last_dir
+	     && l == last_dir_len
+	     && !memcmp(last_dir, k.data, l))) {
+	  last_dir = xstrndup(k.data, last_dir_len = l);
+	  if(track_matches(dl, k.data, l, re))
+	    vector_append(v, last_dir);
+	}
+    } else {
+      /* found a plain file */
+      if((what & trackdb_files)) {
+	track = xstrndup(k.data, k.size);
+        if((err = trackdb_getdata(trackdb_prefsdb,
+                                  track, &p, tid)) == DB_LOCK_DEADLOCK)
+          goto deadlocked;
+	/* if this file has an alias in the same directory then we skip it */
+        if((err = compute_alias(&alias, track, p, tid)))
+          goto deadlocked;
+        if(!(alias && !strcmp(d_dirname(alias), d_dirname(track))))
+	  if(track_matches(dl, k.data, k.size, re))
+	    vector_append(v, track);
+      }
+    }
+    err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+  }
+  switch(err) {
+  case 0:
+    break;
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying database: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error querying database: %s", db_strerror(err));
+  }
+deadlocked:
+  if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/* return the directories or files below @dir@ */
+char **trackdb_list(const char *dir, int *np, enum trackdb_listable what,
+                    const pcre *re) {
+  DB_TXN *tid;
+  int n;
+  struct vector v;
+
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    v.nvec = 0;
+    if(dir) {
+      if(do_list(&v, dir, what, re, tid))
+        goto fail;
+    } else {
+      for(n = 0; n < config->collection.n; ++n)
+        if(do_list(&v, config->collection.s[n].root, what, re, tid))
+          goto fail;
+    }
+    break;
+fail:
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  vector_terminate(&v);
+  if(np)
+    *np = v.nvec;
+  return v.vec;
+}  
+
+/* If S is tag:something, return something.  Else return 0. */
+static const char *checktag(const char *s) {
+  if(!strncmp(s, "tag:", 4))
+    return s + 4;
+  else
+    return 0;
+}
+
+/* return a list of tracks containing all of the words given.  If you
+ * ask for only stopwords you get no tracks. */
+char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
+  const char **w, *best = 0, *tag;
+  char **twords, **tags;
+  int i, j, n, err, what;
+  DBC *cursor = 0;
+  DBT k, d;
+  struct vector u, v;
+  DB_TXN *tid;
+  struct kvp *p;
+  int ntags = 0;
+  DB *db;
+  const char *dbname;
+
+  *ntracks = 0;				/* for early returns */
+  /* casefold all the words */
+  w = xmalloc(nwordlist * sizeof (char *));
+  for(n = 0; n < nwordlist; ++n) {
+    w[n] = casefold(wordlist[n]);
+    if(checktag(w[n])) ++ntags;         /* count up tags */
+  }
+  /* find the longest non-stopword */
+  for(n = 0; n < nwordlist; ++n)
+    if(!stopword(w[n]) && !checktag(w[n]))
+      if(!best || strlen(w[n]) > strlen(best))
+	best = w[n];
+  /* TODO: we should at least in principal be able to identify the word or tag
+   * with the least matches in log time, and choose that as our primary search
+   * term. */
+  if(ntags && !best) {
+    /* Only tags are listed.  We limit to the first and narrow down with the
+     * rest. */
+    best = checktag(w[0]);
+    db = trackdb_tagsdb;
+    dbname = "tags";
+  } else if(best) {
+    /* We can limit to some word. */
+    db = trackdb_searchdb;
+    dbname = "search";
+  } else {
+    /* Only stopwords */
+    return 0;
+  }
+  vector_init(&u);
+  vector_init(&v);
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    /* find all the tracks that have that word */
+    make_key(&k, best);
+    prepare_data(&d);
+    what = DB_SET;
+    v.nvec = 0;
+    cursor = trackdb_opencursor(db, tid);
+    while(!(err = cursor->c_get(cursor, &k, &d, what))) {
+      vector_append(&v, xstrndup(d.data, d.size));
+      what = DB_NEXT_DUP;
+    }
+    switch(err) {
+    case DB_NOTFOUND:
+      err = 0;
+      break;
+    case DB_LOCK_DEADLOCK:
+      error(0, "error querying %s database: %s", dbname, db_strerror(err));
+      break;
+    default:
+      fatal(0, "error querying %s database: %s", dbname, db_strerror(err));
+    }
+    if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+    cursor = 0;
+    /* do a naive search over that (hopefuly fairly small) list of tracks */
+    u.nvec = 0;
+    for(n = 0; n < v.nvec; ++n) {
+      if((err = gettrackdata(v.vec[n], 0, &p, 0, 0, tid) == DB_LOCK_DEADLOCK))
+        goto fail;
+      else if(err) {
+        error(0, "track %s unexpected error: %s", v.vec[n], db_strerror(err));
+        continue;
+      }
+      twords = track_to_words(v.vec[n], p);
+      tags = parsetags(kvp_get(p, "tags"));
+      for(i = 0; i < nwordlist; ++i) {
+        if((tag = checktag(w[i]))) {
+          /* Track must have this tag */
+          for(j = 0; tags[j]; ++j)
+            if(!strcmp(tag, tags[j])) break; /* tag found */
+          if(!tags[j]) break;           /* tag not found */
+        } else {
+          /* Track must contain this word */
+          for(j = 0; twords[j]; ++j)
+            if(!strcmp(w[i], twords[j])) break; /* word found */
+          if(!twords[j]) break;		/* word not found */
+        }
+      }
+      if(i >= nwordlist)                /* all words found */
+        vector_append(&u, v.vec[n]);
+    }
+    break;
+  fail:
+    trackdb_closecursor(cursor);
+    cursor = 0;
+    trackdb_abort_transaction(tid);
+    info("retrying search");
+  }
+  trackdb_commit_transaction(tid);
+  vector_terminate(&u);
+  if(ntracks)
+    *ntracks = u.nvec;
+  return u.vec;
+}
+
+/* trackdb_scan **************************************************************/
+
+int trackdb_scan(const char *root,
+                 int (*callback)(const char *track,
+                                 struct kvp *data,
+                                 void *u,
+                                 DB_TXN *tid),
+                 void *u,
+                 DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  size_t root_len = strlen(root);
+  int err;
+  struct kvp *data;
+
+  cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+  err = cursor->c_get(cursor, make_key(&k, root), prepare_data(&d),
+                      DB_SET_RANGE);
+  while(!err) {
+    if(k.size > root_len
+       && !strncmp(k.data, root, root_len)
+       && ((char *)k.data)[root_len] == '/') {
+      data = kvp_urldecode(d.data, d.size);
+      if(kvp_get(data, "_path"))
+        if((err = callback(xstrndup(k.data, k.size), data, u, tid)))
+          break;
+      err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+    } else
+      break;
+  }
+  trackdb_closecursor(cursor);
+  switch(err) {
+  case EINTR:
+    return err;
+  case 0:
+  case DB_NOTFOUND:
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    error(0, "c->c_get: %s", db_strerror(err));
+    return err;
+  default:
+    fatal(0, "c->c_get: %s", db_strerror(err));
+  }
+}
+
+/* trackdb_rescan ************************************************************/
+
+/* called when the rescanner terminates */
+static int reap_rescan(ev_source attribute((unused)) *ev,
+                       pid_t pid,
+                       int status,
+                       const struct rusage attribute((unused)) *rusage,
+                       void attribute((unused)) *u) {
+  if(pid == rescan_pid) rescan_pid = -1;
+  if(status)
+    error(0, "disorderd-rescan: %s", wstat(status));
+  else
+    D(("disorderd-rescan terminate: %s", wstat(status)));
+  /* Our cache of file lookups is out of date now */
+  cache_clean(&cache_files_type);
+  return 0;
+}
+
+void trackdb_rescan(ev_source *ev) {
+  if(rescan_pid != -1) {
+    error(0, "rescan already underway");
+    return;
+  }
+  rescan_pid = subprogram(ev, RESCAN);
+  ev_child(ev, rescan_pid, 0, reap_rescan, 0);
+  D(("started rescanner"));
+  
+}
+
+int trackdb_rescan_cancel(void) {
+  if(rescan_pid == -1) return 0;
+  if(kill(rescan_pid, SIGTERM) < 0)
+    fatal(errno, "error killing rescanner");
+  rescan_pid = -1;
+  return 1;
+}
+
+/* global prefs **************************************************************/
+
+void trackdb_set_global(const char *name,
+                        const char *value,
+                        const char *who) {
+  DB_TXN *tid;
+  DBT k, d;
+  int err;
+  int state;
+
+  memset(&k, 0, sizeof k);
+  memset(&d, 0, sizeof d);
+  k.data = (void *)name;
+  k.size = strlen(name);
+  if(value) {
+    d.data = (void *)value;
+    d.size = strlen(value);
+  }
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(value)
+      err = trackdb_globaldb->put(trackdb_globaldb, tid, &k, &d, 0);
+    else
+      err = trackdb_globaldb->del(trackdb_globaldb, tid, &k, 0);
+    if(!err || err == DB_NOTFOUND) break;
+    if(err != DB_LOCK_DEADLOCK)
+      fatal(0, "error updating database: %s", db_strerror(err));
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  /* log important state changes */
+  if(!strcmp(name, "playing")) {
+    state = !value || !strcmp(value, "yes");
+    info("playing %s by %s",
+         state ? "enabled" : "disabled",
+         who ? who : "-");
+    eventlog("state", state ? "enable_play" : "disable_play", (char *)0);
+  }
+  if(!strcmp(name, "random-play")) {
+    state = !value || !strcmp(value, "yes");
+    info("random play %s by %s",
+         state ? "enabled" : "disabled",
+         who ? who : "-");
+    eventlog("state", state ? "enable_random" : "disable_random", (char *)0);
+  }
+  if(!strcmp(name, "required-tags"))
+    reqtracks = 0;
+}
+
+const char *trackdb_get_global(const char *name) {
+  DB_TXN *tid;
+  int err;
+  const char *r;
+
+  for(;;) {
+    tid = trackdb_begin_transaction();
+    if(!(err = trackdb_get_global_tid(name, tid, &r)))
+      break;
+    trackdb_abort_transaction(tid);
+  }
+  trackdb_commit_transaction(tid);
+  return r;
+}
+
+static int trackdb_get_global_tid(const char *name,
+                                  DB_TXN *tid,
+                                  const char **rp) {
+  DBT k, d;
+  int err;
+
+  memset(&k, 0, sizeof k);
+  k.data = (void *)name;
+  k.size = strlen(name);
+  switch(err = trackdb_globaldb->get(trackdb_globaldb, tid, &k,
+                                     prepare_data(&d), 0)) {
+  case 0:
+    *rp = xstrndup(d.data, d.size);
+    return 0;
+  case DB_NOTFOUND:
+    *rp = 0;
+    return 0;
+  case DB_LOCK_DEADLOCK:
+    return err;
+  default:
+    fatal(0, "error updating database: %s", db_strerror(err));
+  }
+}
+
+/* tidying up ****************************************************************/
+
+void trackdb_gc(void) {
+  int err;
+  char **logfiles;
+
+  if((err = trackdb_env->txn_checkpoint(trackdb_env,
+                                        config->checkpoint_kbyte,
+                                        config->checkpoint_min,
+                                        0)))
+    fatal(0, "trackdb_env->txn_checkpoint: %s", db_strerror(err));
+  if((err = trackdb_env->log_archive(trackdb_env, &logfiles, DB_ARCH_REMOVE)))
+    fatal(0, "trackdb_env->log_archive: %s", db_strerror(err));
+  /* This makes catastrophic recovery impossible.  However, the user can still
+   * preserve the important data by using disorder-dump to snapshot their
+   * prefs, and later to restore it.  This is likely to have much small
+   * long-term storage requirements than record the db logfiles. */
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:/CBQ6uebrasgkefvjc+fHQ */
diff --git a/server/trackdb.h b/server/trackdb.h
new file mode 100644
index 0000000..54e7214
--- /dev/null
+++ b/server/trackdb.h
@@ -0,0 +1,130 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef TRACKDB_H
+#define TRACKDB_H
+
+struct ev_source;
+
+extern const struct cache_type cache_files_type;
+extern unsigned long cache_files_hits, cache_files_misses;
+/* Cache entry type and tracking for regexp-based lookups */
+
+void trackdb_init(int recover);
+#define TRACKDB_NO_RECOVER 0
+#define TRACKDB_NORMAL_RECOVER 1
+#define TRACKDB_FATAL_RECOVER 2
+void trackdb_deinit(void);
+/* close/close environment */
+
+void trackdb_master(struct ev_source *ev);
+/* start deadlock manager */
+
+void trackdb_open(void);
+void trackdb_close(void);
+/* open/close track databases */
+
+char **trackdb_stats(int *nstatsp);
+/* return a list of database stats */
+
+int trackdb_set(const char *track,
+                const char *name,
+                const char *value);
+/* set a pref (remove if value=0).  Return 0 t */
+
+const char *trackdb_get(const char *track,
+                        const char *name);
+/* get a pref */
+
+struct kvp *trackdb_get_all(const char *track);
+/* get all prefs */
+
+const char *trackdb_resolve(const char *track);
+/* resolve alias - returns a null pointer if not found */
+
+int trackdb_isalias(const char *track);
+/* return true if TRACK is an alias */
+
+int trackdb_exists(const char *track);
+/* test whether a track exists (perhaps an alias) */
+
+const char *trackdb_random(int tries);
+/* Pick a random non-alias track, making at most TRIES attempts.  Returns a
+ * null pointer on failure. */
+
+char **trackdb_alltags(void);
+/* Return the list of all tags */
+
+const char *trackdb_getpart(const char *track,
+                            const char *context,
+                            const char *part);
+/* get a track name part, like trackname_part(), but taking the database into
+ * account. */
+
+const char *trackdb_rawpath(const char *track);
+/* get the raw path name for TRACK (might be an alias); returns a null pointer
+ * if not found. */
+
+enum trackdb_listable {
+  trackdb_files = 1,
+  trackdb_directories = 2
+};
+
+char **trackdb_list(const char *dir, int *np, enum trackdb_listable what,
+                    const pcre *rec);
+/* Return the directories and/or files below DIR.  If DIR is a null pointer
+ * then concatenate the listing of all collections.
+ *
+ * If REC is not a null pointer then only names where the basename matches the
+ * regexp are returned.
+ */
+
+char **trackdb_search(char **wordlist, int nwordlist, int *ntracks);
+/* return a list of tracks containing all of the words given.  If you
+ * ask for only stopwords you get no tracks. */
+
+void trackdb_rescan(struct ev_source *ev);
+/* Start a rescan, if one is not running already */
+
+int trackdb_rescan_cancel(void);
+/* interrupt any running rescan.  Return 1 if one was running, else 0. */
+
+void trackdb_gc(void);
+/* tidy up old database log files */
+
+void trackdb_set_global(const char *name,
+                        const char *value,
+                        const char *who);
+/* set a global pref (remove if value=0). */
+
+const char *trackdb_get_global(const char *name);
+/* get a global pref */
+
+#endif /* TRACKDB_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:Y8z+2jDRros3Nz67LFBlzA */
diff --git a/server/trackname.c b/server/trackname.c
new file mode 100644
index 0000000..a24e3bf
--- /dev/null
+++ b/server/trackname.c
@@ -0,0 +1,96 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include 
+#include "types.h"
+
+#include 
+#include 
+#include 
+#include 
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "trackname.h"
+#include "mem.h"
+#include "charset.h"
+#include "defs.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "config", required_argument, 0, 'c' },
+  { "debug", no_argument, 0, 'd' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+	  "  trackname [OPTIONS] TRACK CONTEXT PART\n"
+	  "Options:\n"
+	  "  --help, -h              Display usage message\n"
+	  "  --version, -V           Display version number\n"
+	  "  --config PATH, -c PATH  Set configuration file\n"
+	  "  --debug, -d             Turn on debugging\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  const char *s;
+
+  mem_init(0);
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVc:d", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'c': configfile = optarg; break;
+    case 'd': debugging = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(argc - optind < 3) fatal(0, "not enough arguments");
+  if(argc - optind > 3) fatal(0, "too many arguments");
+  if(config_read()) fatal(0, "cannot read configuration");
+  s = trackname_part(argv[optind], argv[optind+1], argv[optind+2]);
+  if(!s) fatal(0, "trackname_part returned NULL");
+  xprintf("%s\n", nullcheck(utf82mb(s)));
+  if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:DiPOUeRLYf0fgoUqzejUDA */
diff --git a/sounds/Makefile.am b/sounds/Makefile.am
new file mode 100644
index 0000000..cc07db8
--- /dev/null
+++ b/sounds/Makefile.am
@@ -0,0 +1,24 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+pkgdata_DATA=slap.ogg scratch.ogg
+
+EXTRA_DIST=${pkgdata_DATA}
+# arch-tag:cd42d893e1fce039edeb219309bb4796
diff --git a/sounds/scratch.ogg b/sounds/scratch.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..05c09133b9d9edde19c71d829e4ad1257405896c
GIT binary patch
literal 10804
zcmeHsc|6qJ`}Y|JL#nZcsKH<`%9^z>cG($w3S(Dx*}4q|Lqm)uBxENP((X>ko;A`Y
zWlP9bREpncxbN@x{yzWwUeEJ;{(4@|d7aPsT%YS)*LBW$pX;1+of%UP4@*D?{z@J?
zmwjktFeT6kb_5pc7wF;@L}Sot*U(-7#5BQn-}hmrG|hh{n&xf|VZS(=*3N$>Ci*=~
z0+6_^m%ryR(?AcDua}GUu6>jNN>*B0PFh)7nP!05yE)ar_2e}412fKP6JMS+C
zW7snxg442w0Sy4qLH4rlRYBvC%K|tm?-W07FmgFp(}=B@AJ^xzDCkP%Z7aehyH>{x
zrr`li!K3-=>&V{b&520e2OcjXy=YWdbK+QJ4~<&>S0PgGL5eNNrhOO0fF08wtu
zHq=N=6GSaF-+LYTg;q;nrH9HOXG
z{QD65?HB-n0R$wcpImC2I%AtEVn?NA18P}#`NG4x-F$3UT^|55bIxb}oJ_lV1OO0_
zR+(pV1v$B@GL?mtP(ufZ0KlPU9JQ>BIcQYyTT0O;ltEAn(j2lYmjP8tHtOfQ8Shd{zLAd;SbcF`I{#n
zjAiWV$`~>i{-a46kFpCgZM-}lDS;T3r2a!57>~TzQk)lVQ&icoAmgoJ@AG-l{Gf-*
zGx?_d%3^zf(*X^HU_7UV{$B-;3t2ERKafSDvWRLZQC(TwBhia(?34Yw=vVn%*_ejI
zh~F8sPpR+Vaaek8;1HBKyw~aZ{Gfj3UHniG_BcjfLYld*U_6gYk9b9Xtf55J9-JuJ
z)Bs>ED=@+)V9q8mw3k=;2UgMM>{0{l3L|nlBl2hPp>B3p&%X){vAY-Y>fR-Xd*|Q0
zOnLV`WlAaaA30%f=+II}k&w8(fryv$1RYR82xhYXJxtn!nsMsU{0(i;fbJc}8jr{u
zuWOrrH8TC?XwH3pXv1-U`~1K+E7NaQ<_h?cJ$J#M!CSk%ynFu5Ke7Yk?hv8ntU?tQ
zrwWNvrdlHHV9Z|kOl*3
z><+%QP2+6IXcw!<>LOQG`!BWixmjC+xpS)Uz
zL#J*oFFG(gAgEd0DpaUG&2=$O0S>IU>aBcloLiJXs#D|A&viy
z1=={STyq_j3=Bq!yawgTLjSI!R%k*zqz%aPhNp*t(;*?T06ceOQ+44crRrX9c0O#!T=Jvw0e_Ht8
zr2n%)c0i^B{+MJpSZ6(U5^53M-2k!~2IYwrFlaQ8FoxE<+nSax0Yqryrvv5y9MDNe
z8*`jYE%C||O-6SQ%NujrWtV&9VQ|o)!ucV*z$0%(Z9vXAryP?fiqsk5gbtrc8tif9
z?H@sWLJb_?=m3ARi(TGqc!5}Qc~v#qU_=etrHAC9@Wg;x9$o;Q242I5=@KCH|lkEm*HY9tYeMLhRju&EI8&HnHB(y$IwGhyqRa0AwV4jL}^{|j{G1Kp;As)C^})YNS2w)&oGpk
zg+dAgao(VQ<+;*Kit=23kcA;6*AK;CG+sW`2E;Ql6c-^uYGs+j%5(e>+E%-0mvqg-
zg*A8KwtxZFR9DD-!izogve9075Ehuca!~_l0wAKghlVE&{VeE&-GyFalneSc#^cX4
zV3Q#w2nT5hUOWUFG_BCgJd`E8qM_!-d-pu8wt>~r1`k0Nvp~bR0e}@a6DXoPY^|px
z&bJRMPY|J(?#~YNA(yA5@5XuT2BN#6rAVORUVO)*VIC`KRn<7_(zRs1(c!;lo9YDBgAR#B&^@UGp9EIj7
z5I8rq$$LB~`1_kD2>Bo+cYp?ynIROU?ty41F0dOULP*k_NplxdOj+r$@*cVn_J08j
zIjjZK9^w%7_bfnjBoe=C%FNI}Bnye7RY1J5ztnc)_+1{REYr$DeAfUpyZlN}aLl7A
z#*626E@3`&WN%j?0Ki&2Y$#r(5Gz#8(_Ubqn-!F1pHxX@wOCfL8br-vvXf;xBXQBPEIwQZPi9_(&y65xYX_QuNK6BO9?l>KS|-7VAY?EdT<Yb^+KAR-mBF5wNih^*Iwfef@*OqKG6iB@Nmjp~MHGpqs0RhPJkR5DzckVSa%l
zM^S=8!f0q4-@ApQhyV`Xi*lg71fut@*+i*beL%Zr)A&OLZW_w4_+yI7iju-|3VQll
z+9n37Dyk>dPN|>J&`>$0rl_obGlNQ8g4Kyz+I1bz;-Nb=pIzXnkGKHkG(I(N962$wLLmZ
z&#IS+I0!wruI)GUbODRCF0Ot)cuw{t?X*93#4f*a*p|A|@axQFIn%AnGJN7}hmw-a
zd33orN2;xyjBZ-|V8Y;yfgv;L86p7Vc3+&DgmDqd)n_dEei8Gl^XHbXX;!nGiQ}ma
zfwL&lnj9RA6(6>LOY=ze3g1TDFPI@dhJ{!SN4Bjf!?A*;3FoE>WWgkXEBodShiWIj
zAJTiA7&4w7bblY~=snm;t#9Ww2HZh48U^sosDuweJlV75hi`78uswgeT5U{~O++1k
zF2Oo_5!^gLD;SRh*zS2<&91@PHYbhUTAk{=a&(=2SA-E?v2pV}e@l!A43>jr&g+wn
zIxzL^q2H#|E9%tpYVbA2W6hz>%?vSU+lNwgdn%&=D&JD*ZAnlMID|Aie3f8~l$Q$~
z=IW#4bAz`|{#x=YBsXs@zxO=*J^#T~MpnGXTtuALbb2&cc6D_IqV0^P!)Mk8^2$7}
zymY+4T&OP9ys5mdT(NobdO?NXE5sX~U)#kY#QOKq^{E0o=*MhRk5&tJ&HOB1Opy6T
z2PYoK#1L7CV#1nJ@m=g{Uqk-69!iNX3JO8DVT+avQ+eLSyTUVQiP$sa%MyVjwFgZDsBHLjYdZqrkSpan%duH%Gjeeogcr9AG{
zX6Mr8;MVGSqSp>rh0|e9h#WPuMR!t_4CI`x$GJJYArVsZ5>J)~C6FRtKy@$&bg9J^B_xm>b&G_Iim4x^3+&QFEgT>hHrto)2_aH>5rF
z%7sssCGbfG8X3r9f_t*%5HE>oE1gHLipHdu%&8EgwMB%I2*J`y>1KekaQ$0^<8oz%
zDZ!Z6b5sTcqVlzpvx8-?uqSg!Dp766$EokuSf}{jG!`mb{eoe>SNV6Wr-
zl+Dv(g}lp5(jxn<00xw(+GQW_Vt>4tcqREe!q{+>p*n<-AVm)~9NJ
z426;ATjh45=Uabo=MGB<$Mv7!W^B3CyNXFw@e;teuOMWE74Bs9L>xZI?=Ju
z>(`U|_4Zq%*-G>!lz0yvCBqq`JdQifE%X28eq8lXrK|5mr3;{zh=z-0EAI5WH`4if
zxb#miac9e#d4+$NN=Okc^cqYY0^l3~9c72W=!)@Ur5e
z3gIJXE4<}?acmZ!czRq$_H^y0FR;8)Z
zWJW~r+{evn)kDq4Pl@S>3+sg4jfb_=>)zqLl0$Z`9t#j?RK{DUFG$@|+Mr@HqzdY-
z+eEcz_8CxcvO2OiKVj&Dy{@l&U%lXRB*I(5jakqdE&&?!`=ku$np|qL(+%EX&IX@#
zuJcF|dQh+EGVi=~^5fP`dmHtE@_nTF${l=L@No2wRfB}Eg+mhuk+0?{AI<|ue&8#_
zF(T_jZL+SL&o-VMd#CVH`|NR828u;Q>jZBW9n&NTo~ynv{Py~_PvHwy0&cuKAFH?-
zi{_&3@LCf(0d{OnK3pHdc%S9MRIa+R5z$-YUv{$X7
zW#MZb-JX>yZ)bpZ*JjlvaIu#Y60?d82x|6yabp;~;u6VsuUgoGbW8wBUN9yX&x7ue5r~SV4
z0Y>st?>Az8**!Np^LfO2l!sX7^ula3bE=iRu%>I``fQP`QYsxV+2TBv`QC*4?9UBF
zH$Rb*aryWIBgw_lHoj!T$i$YyeoQVg!(Jpu_fLDRZINGUP>z^ujvdm2f?*?$cW
zusdnr21u7kXEuM12R}hSY-R_(3zYSkuXpdhV=_DiCdv_@48CN^W6qK3o#A9R9;h=*
z-)WvDLggyz>HehhMaSIHc~%T)dmY@+6H+&P=Pe9
z`J$13i4qwfD1M5Mq{-9wKxxQ+ZC$OzyYpt@=Je=y`~j=;Ok#1sYJO2}t9F>a@6|Gs
zEt6AkD|o?*+Kl=fntrzIYE-JUX94SFiGh3{^^a!?nVAr>>t*LGG{}NQfHc3jxmNZ&EXMTRL)i>777;MZ{?wHmPBp2J5p-*T
zYqqp6eD#V<6BCCGtzJ0vL})%AnX0Lz;KdL%)8p~ertWd!DFq!n!r(c)43IIvjY@`D
z%6$;Meme5H>=kRI`_I0)bK99+NfqZt4)FT8PTj;NqRCg}z{3d9(Uav&oaeq7RY&&m
zm=V(Vqk*9e$l%z$C64EYn$5z6XX6qI=#cDJdzi>&pHu>MK)ttc64I@=$C>o%7T
zQ_I&9i*$P5on5(=efe_e9P9n;Aca5LPCEx3`x34g3&9!aC3aE)^h~Bcum%3rC9M^P9tZ#4qIEL`69I9nu-|a
z{=i0sUh=&Ch@ti767@xc@9{5W&Cp!>Yjhl>3>3OHEW$Pi(G`Pbs&6Oa{Ed9^-c9~w
zK~yq@aZdXLdW@3M?&;`_AX3uHUi%VE4LXMR&;EYWu<8D$dHvLd$$>9$_LQ7^=bu0M
z;Vm_MVdeR>lU`EDh1t2LtJ`Su;!oinue_v7gRhnchCRGPx6fTuCpn4^4mYqGKexPc
z-bpm_{SohI@qs~E|LdQ<^b6Gb{r7DX8;b5s2PB*(Gf@td6`r?R&1kf}sf%|5AO|JN
zYMCT(Vu+NKg5GCHQc7fdh7R@5uWU81;1#kxpru+zB4nCnHh`z87Q^*Pwz3CEH3(J<
z-WDZJ!F~PKu}`z5?S+uZDa`wAsb-JXbj6e(YugLmk#_ImtrZYSPU@PjUh3xXl>y!_(YSv{ky
zuyOjv?R;h(AF)#CLSp=q9AcK7%wmG=uwjlv{W_%j!RqA>bA|VxXAR3N5H;dxa#gi;y^Qe--9An}*W+?pN>2{+-l|^}ihlc)Sxkh$#QT`%nTRNV
zF+VvD=xQC-PIkq0&>a?vbBX6>)N@ll-SJgA5P1m4%gEe7aPZ{1z(}y;A_~svLfavt
z-pj_}Zqcf2EJ?Z&!x{E47N!5S;q8M4tyito^cC@hSPSXTH)1c{+Q24GC}Q0%M*8Kh
zed_I8W|_Vi?ri;jqMTt2SukTlG7JhZJ8-
zabY?3K^%hw`02T}oTA_l;!kdPH%Ul|7W_(64~{FiKe47Tdnj|k%f;zTT}4v(w<@dT
zu2L>*I$}Lfi6|S1%R*wdhG?dbj{UsB`J}A#ibbaYdgh>^cJoXTMqdyFFJLzhei=6S
zqIjz5AU<7FULU&SSaXJ`d<+pP5Ub;#s)(;di
zl%XSdNo}G}ANuvOqTf6HlXnK7TzvBO^NIA3m7u184*r=6-
z*`*bBk3z}HE(m>d!zw7adG^)sOaJBh_m8V8OY|*lbh#N(Zduxm6(@f`win6hACH`U
z@oh>fBj`??Z{cNsQT?CXuVhXwbxpd4B6UkL*o5;J%9q7-#?kNlw(j6bRE_6fU|BvO9;VK{Bo@AZu+DCpaloj`6hZ6YlsJSe}8}zjsE@n
z1MR;hwdVAX0t#yCY8q-Xs;XPdlRGdeS%u?rIyxs$sj2KBbaaA0^sU31<
z&_d<}%Y%>iGrNDKycvTRAF^tfvzA+zU2@ZO`Y|)7C!5pmSVIpo=VR5od%pIE-5ZM=
zlsNAUd_N}|WcF0E2!>@H7jm9+v_M}i)<4JbIl6aK{;`g;ly3$iP&T(38JiC$EMzG(
z)yi#`{-7rzbKymBR=LU?J6zxTtqZNT-;@)SR870O=8GD5zZpG~IT2?TLJWFbNUq)xH?h3GOWJ??#qzXr>3_?k2{JdK>PJ_&);+=`0OD<}5u3DaHq9EwC&548@
zDhlr$!7NiHR1BOsa3o))q?*_-
z3ti+Y-?YmY^V4?=Hm27`i6a$WqB+r=JBH_K#fBfVD0Vb9Gh@)_MT$X$593?$Kz@yQ
z2f4}At|qV|m6d7kf#=0rlDjM^$Ctx_Q%~ky-!oB)6UjC{0!^vp
z2#D>E?9mgi-8n`&ssotW%Zukzv*HGa&b729CzvOS_6t^@ZtY6v
zvD9K>`^c!wBZNk3etIU$0^5FlDE?Sq^?UJbc6|(8lg5e5u)|GHYqeb{L&)KdRCCwE
zE90EhcoMgR{<%Y8{+>u!96I}i^fa-i+b%^@G(5WIS9y3R3CFQ%W5fNcNb%eH#dzmC
z7yaK%V#FRL#VlUv)Z_Se>#2D`VODLH$&8>F!#?Og^f_aZ2@(wL+?cc$cP?9Pd;839
z-|E#1Cdo$!voxnKy#o_bCSm*4TIin7!LyYP4z}$taSf%Lb19e$bTh|~rhy_&e7pQ2
zR-9@Z|M2q%R%S+fB`hMLjaY+;Pa~9OJiXq>CpneRS!^6EDiE03!_zI}{Tbhl)HBc^
zvKgFaWT?6-dtZfNA`2edNFlNhi6R7tK@s6xyr2WS{)EBTGy}p)41Dc*AR#wCf40Gy
ztH5?5c2!tn%hPyLx8uayNdCizPs<#gn7fuGUNBtOZT)^FVJYryMPzTmoG&JGWyy+d
z?4*Ca-vyuMkCDH;<+^mTr$n0^wUx_V6EO4@0EC{-_B0+9JUOJfWXpb9;>e$aZ53U8
z)rKMBVmcUE;fABWS(1key*>5tS)iplB%kAwvW@ha40`M1bt%_tYfp@izj3gbxlu3k
zESAMEgh8gvUx+1_hb~)FiM5E#V8kn)Zso?LGk?4*($Ua*$Awqy
zej6Zazq@UFql`HYX?b^$Ooks9_RgZe>ZUm^O1)KjPvof?3u_N21D5A;FM^F)cU4?9
ztonP8r$nG+eziuX*c9%emNeWc7N|5&s6xfq1S!P
zy98CQnC|i8-2KvV&YB++)oB&|Uu5Uk3WZc*`SaR-+inp;UkhC@JGz+0O
zT!}tBuhYUCrH)Vrm+iYPQTkPYGEeeL?hI*Fe75j8P8bi!H}3^3y+9jSQIO1mj5e4z
z%^yY%Iz8+W>gbJn%LVe6V%I;vs=Mt`E!IocB&x~+N9feY_K5p
zf^o4+@(u6T-4FTeaphW$M{RCOf8aXS*l>8wtE5wXG$1NDg4LQeT%(<{8~W1%M7bTE
z-FiQs_;cIo3@2ObV1hKvKeF|(Ppl4AG&o{$P@1ohrOlt)Ysdxt=BJ-PVBJ^4=;$XQ
z1|ga3%=Ez?2PYnVS$z8AW5~$CiE05(5QHAReUI-mQcJcS$x>ycGL@SWLy5m#@cD^r
zPIm-$>b9MposdqAwUwYDg5qO2@JDBpm-~~*GHXE&KOZr0L6m$h_Sp?)I^{dD@q}X~
z3X+{7zUy`u6t_6n8{skrY58J|WvDz9CuKq6%ZGs4Z%6)A(BU5JF#!ZG%Nm>0JeX$j
zLwUSyI43WW<4cDI3T!ske~YEv#tJj!TN)ub1gAM0RCU|Ay@`xdXXJ!T1P=^yeBj}n=_NicUa&tijxX^Kk*Iu=rvsGfc)?Goj+VXAB?#z<{Pw^1(M
zSXV7OIQs#uCNw`ghBY<~`Ir%QImx(1VjnrQF_VtoogaNH&*^pykH-1L7;QNtLL1>B
zj!uE`c)i)24%WDnPO?SN+(aK?v8WK(-$O*10Djg|t$RHho-9&%4eT97z?DDp`n$Jv
zk+*%W=X*KIzAkxgbe=jhH`6xTeD}MOr^>f>-b*v&B&eZ(d^2UIeaVB#K9&F9enHxIbCgB@S0_I<^g6H
zK$}0(&z8t+9q4U3_5Mnk*Ue%!qWZ(s$X}0st?Z~&)t_Z&%Z!Y?{%ZQ?fq|plUAOFY
Yey0{!Cn_fC>pP{OT2c1{$tlwR1&&?6=l}o!

literal 0
HcmV?d00001

diff --git a/sounds/slap.ogg b/sounds/slap.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..fc4038c3496449a0e46fb079f2efdf3404d1b5d5
GIT binary patch
literal 8502
zcmeHtc~p~0({CpPOb{?(Ga_gb0t5sG5D=7MlCUKa5HQFhDC+}Rctoc_GYl`2L
z(NE~&^cMe_R!A(jqy`1tL&Aggw?zaILPPv^EVWNy5{wKCHX4{4n2Vc3NG=O&(5-eL
z02Kgoq^Hr(rIDl2uhr;+H501z-ssoaG<#gWD!ps(4AEb(ruhP$?=On!O>+feg`m2#
zW6_;;pB_cq-V7Ry4iQs#>Jo>eJH*uMzY2}&AVD-kT%D-0OO2Mg38}z3H$gO|7E-fy
z*T~$C8|5SEBcjKE}u+X(g3)SPvPfNq_HYB(%=pNSgNyX>jBlf>8f|DRac^AJtQF*
z01f3B{6hECN%vF@4}mx{1w~QC)3+*@vi_1f0{|yy#ny1e6-#;n0HC2(Q851!k6&7n
zD$PwOM*JUS0Z%9I^6wg3CTXM+`a2No0
z$Doz-C&(N{rlWvju3aQ-CV1EjSYpb~Fp8a#OoNmQ>(8Qz;E}rgwuR!6EYL9#%b7>H
zWF-Iir^u49q;^!lpi->+j1tl-a1g(x*dz%uW|{wxTgCc9SZ==T$D6|$OS*v%Fr9yve{un*n2Q*TaDxXT$CvKIC!z(EaPjcU}*BFp~27=adPv)W79~
z<*CC(P2ghb%M;Nq#z)F+MiYhde@;_uP$xw@vA>a-HRR5IU55c(wL1AE&CwzG-rz_Pozdb{oj9JveK4a=T3z)w*0(*I5t?R5)JaOHk`t!UgKbuzt#|x*k(-RV{NiD8#mhUNY=Pw%)hn%PQjfS9}W+2gNrhM
zjfs*Phya`kvrHHhq)xc2z&I*%G7116kDRIHfb8!r{BP3#>`)%?kRY7G4@9(A>yE=7
z$#-i&rev=%=MnKG56}LPE@d@Q9Kt}>qzMn_V^Qb&6
zesQUY%p9JdjnnQY^UL6^1@BQ%T|`r#tlFkk}5chrB(cvWk~DMvdZ@
zyxlSpa41l#M#jk%#!7CPfk52GhE90A%BjM@IQc?Gvd*chKJLm@)QiLoNDc*nMtqUt
z_6`G0q7X}JBXi~H8bv|@t5h7fpq02TEDSNj#f7Eeie+I+h0=7)MWG7-qcIZj7w;5&
zhXn|$0LQ>qyDc};kyNPY52F*D3*T@mH-lyP9EKE3?KP3z=2L~jWb>)qNEa5A>xS`{
zn3oItfILT*wgwlf6$$&zr&J-@>?O1d?mS1S)0W_NK>(Rlf9O6@`N27v0L-ovI!lkJi?1QT9)+{g~(PSW)Y_x8mlfU3;Y~
zTHNFi<1D>u_+tN
z$>)?(rBB(YGf#6)U<}LGJ@b@Q=2z?OB$%$cdV!h)_^7~oZr6#c?5xynQ#r8~O
znrBjpK*r^@3A>l@oRY~m+%f<4h=u}%P|g@mz)MS
zNSIW>F?e&;pjulSN8;D0tW{N8x1K;GsgvP0zI@*%Z~zLm9Ob~h1k%eFYHHw;J`i81
z#rzFI(LpUeT@9_HVf&(wq$ZwDzfyO-<;IAi2Vm*5^m{lHYYcb`?f7S
zFo?)_p|u!>zj;Yi!nIeHO5<>V`=Dpb7z_2l(P7NgBxLLHwunVJ@<7P9GC%HF`%Cke
z`M5{Ln?)1dRLu!YP6M^9aQ
z_QQ~t;b-4wy)i)p`+UoIqU=ckGFv;!P)&I9vX;0kEcZY(`W3kAnJRC0SleuCf68}k
z^ghMCyx1N4vl$Yrh<(QA`QA8|7v6HXN}bUXFdR9yvf_^uN)MZJlas$}w89R|%}bWj
z=q@^#wP(m6Z|k+u-GK?n-Ez+adLP}gFE*Z)cx^ps3$7?QilJUk*n(lct8Sa8di4QK
zl?2BuYRFOK8PUP)r=`nRXvAf
z5d5ER1G^qfwl>^(FWq-?9$S8`^VG@omrD7IPl==V1fxx(#6Nojz;#fnx^^^MdC)qQ
zjE*Ci6543IxY5yHp62hcXFD_79LY>g7YSu~rF-TYMJEgf)V@uPzHq+c!zFP8WYQbj
z&u;M>2aj@U=NshC$E-K4=<>W`x-jx&w||kcBb{0rKQ6sL7z0Zazb1imPf>
zG^G_3a9ib+&61eXD;}8wAn66bBXd2{g16z9>5>x%@8s5Yhv4R4^Y8Aq`%qx@y!@P=
zj1`2IL|2ju&<2+nb{NY?kArMHx?
zHnE|%=Gh7CIADw|JV31QT`>>(_AJKyc=jmmYfdi#5bd}TGqKpzLvta*w=@o==V
zMt3@xw6+Z!x2|g)Hkg@}Y4J1Ez*({RZeC=S948juoI)m>iw5e`2fWV~jfI&?OU0co
z0FBZ!X}^R&o*EcVlemDvI++1vI7d>Y5pf$3(Ba_3=E=zhw`Z4`u~T(h$7nAPc66S+
zd}ntCswevF)wurqesFmeavAAd=n8(-(CYFE+wj(Cf2-DZGZk6hD!Tju>@;=WuthVR
zeASuDox$eP+tTsv_@Ky5tEVkhO%gH|HhDMC-B2WYk_)U9+icL(fkv;l@sY|a?%axz
zLO3aLTM&ile5{lBK-$I9Xl>ZA!!oYnns=t)f{?{O=~jMB>SWk&x*E6dh5D>apdpDKblMXDrm+pF9LoD7ZG_Si$%36y4apP3||iBE{I$J#oO{j>`=Q?mJSEwyKNPi^62
zLjBP`FJ>qfTYb*qwPuoyJ*ZTcw9Rpet8=H+TZD2yYiJ&R7u$XdOvU65M^f)+eAEj%$c`#O8qis+%=T{DmWG33#1
zNDRWqb9TySpL;a*Z2q9!j;krh+WeJQZ?$CCd9MEw7HMgGW=n-y{osNuS71`49=9fo
zXlI4UV!bTspt24y4N$vZq!Gc^;9#(DgFV?$=4=%F=1Sv)%bCSW+q)NT_aKjWVs<7<
zVw~;-h(vrWhsaSoTpXgZheu|ICi@F}ldKgeyweA)`SKW5Gkvrw#)ZdGU~P_9NJB{`
zSG~FYL))gJ-8Ulhp&wd%$a4KV((%kjQ^fI3*bm+an@
zNvOfCVv>muq|^zt&k1h#>S!T*t3Tx(efz5LUd0!O-!a=}M4lam=iE(eLGj$TTaH0r
z$Pu6WFUHHCuV@>P$<7+#u428D%!|>H0QK71;b7fO@_X^HQ@^^cO%=!|dC7l$>?*Ld
z`=d-Ry2NUNSIG5
ztSC52y;rp>iL<3Q4#c4HUKTDqJ`?m+_RIQOwRy!jd#w+Pl@Yc1c|`
zmiX92
zClu5(9zNSy`sC5ijZz+R5$qznTS`rEG_+h%ROF*$m)
zyFNB1%xQ$*k|}YFX56?VH{dL}>*RF*z`LY-{c4rxW7@n4d+|>=vD;M)0NS(!V!xs6
z%C;BZzMXXWXYJz(khXE-Xp_M+ynBhEib#i!CZu;Dsn#=Lp<%PKR=Ub%A?*?yCN(Z9D?6g@
zqUrEtvuQy8*4Uy8m-o0I@8b6Ozq$Eb@0RWTnXiT%w4628Kn_V(1@G^a{Sib**+xGb
zTG&WL7W_I$HWH?x8r{oC!ygZ=etaJL$udMo7AZIT@sgS4j1*#;037l}Xj3&Hqk3e{=tY2rY
zeL>|Xi
zIXX^98jqCyG2;E}^w7d|<;pO0st2GVkv@C*BDInVn^2|a$I1JAcZJymgQJXDsV}cJ
z*3#c@+NLFBhX1qrrc_kS7&;@;)d_SR8r+m*;XA4TLmmyT5T0~Zw(Mk28oL}Fzy%8~|0=05
z`y4@C9C9rs4#VC)_67sHT461Q_Y|uoCEpgGnxx4~Ng7LMh!nS5NefsEM{7wYCLbY_
zWqpv_`=tnfW;ge>1)qfyGVrLcx(oL{g~z>{x|-^tls6stA=^W>$&*>|{)+Cd%Ckd3
zKOaEv)kx{@sre;na~zB?zNZV^>KOU58-#8?c7wICzNsyZJx}vXs?{)O5!VQ`NphKi
zJ2C0uZy(;E_c<-F$OatQ8Whgv6x-b7u5EK!IMUz1-SI{yuzb+^CEG&o1p6(e7Lzmk
z^qc*<6+h`j3CB<{!b|vff8*A%!Cob!20HQB*S(kt#Uu^WR|Hd%LP9m`MX)_S;hIC^
z4trNo0VDTQpnF%uqrE%!>vAxiByxCDY5s>_(R;sC%&CNeM|_)?8{YjvakH^q!w+fA
ze}Cr731JzjK&CZUuVu~h2QTplV=gzT#gi3^w237g#54LvM-42KHN)wO6taAaf4sYM
zd4Nu8k|Lq#>65{VBiDY)Og(b*q@pS#KEOU*SaK}Uo^p9R`T@c?94vU~jh}SunlXA)
zu+QI|Y*{Ax?KMB!Tk6DMm~LXreesS1zgCe**5awo`#y(Io^9*$%aKQGyz>U0oICUb
zF!mt;V`(;!Y(!Xk>OEhm^U@309kuzQy>`Kn-`wNQA^OKNmlQgL5!b3RIIv@)3yuyS
zzQ$R7f+6NZ!f_EMc%7S>?~1DY
z+e0z_vIl?ZJ_-FU;eecfD8e2{8Td3_1c?%f5)xDr!iNDeM`q-vAD^+?%Bvrq)vevI
zpnmzj+Hk{$)0x=g6%Rc2xP5EC#9|j2vClf+?Y$m4+@%a7IN+M%tU(7pDfsf+?yb_p
zR}J<23}tJt80zjZQLyr&+SYFu{n?n_dhz9*~gB-w+HdmYxOZl9D_
z^Rve)5T%lpIH;aIS+w7~KfW`4c1-QkzI#=P%<<{+^7Kj{j@wAW3Vcgw@U;HAT^_Oh
z#lOul#_M}E5lKLf0Z^R}7WOL`Dd_J1SA)Zyt|rIA3xJH_XkWW_*so+!l27I(7;%Tc
z9w>TWr!|K7)^h5(?7y^j8Sfumxv810gt%%rJ2UHWCg|fz|EKac_#`rI@zcMOU+2y}
zHmQ&7*wpsN4d;pHpIeiGMW6Djl)T_miMGGKDrXv9VY`A9cea7y!daE=g;jA6C%*k=
z`g=kB-pO@6(G?Rt@1Acqahtj~*a~!iH=qBlzG#cqiIM9Df^m0fN0=Jh_W1elS2>(;
z5v)r^yd9Y_U3l!eL)hza`6Yj_^7Gk-pRdm^R@UTmyN^1!6&`MFI3|+z$n?k<%9b{_
zJ)Wr_q>rtE9}$ubhf3sez^8&HS?)fFU#l?Sq*zurhF2cEI_f#Y4wW>i>UdoAdF7{p
zfrz!Zu6J9n>kA%qB_^xjI{mjeYs*aVY)Kc9>XIszCLw`yO+5>9U61*pSMKc#wBqcy
z+oGE$U;jrFC&Gl=7|gyIm*q{o8?2^y?E?8>#Q&_e3eX;#tL^
zo)O!va^Kn>EL>KwXqdmYIF0IQS|CvX0mE^1Ftj)MI6^}v&Kt3u*I(0M6?-9{HGMrI
zF)1!8&b3mlm3kd#!e*-6{gLWg@&W>bcW8L=$p>xIS9Om*o9+7Pw+~l#!eaLqH3YY<
zzx}OjO3~@bEh|MjKB1&^Yj^{9GQ!4T($mq-ANl*%e_

literal 0
HcmV?d00001

diff --git a/templates/Makefile.am b/templates/Makefile.am
new file mode 100644
index 0000000..2614895
--- /dev/null
+++ b/templates/Makefile.am
@@ -0,0 +1,31 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
+	     stdhead.html stylesheet.html search.html about.html volume.html \
+	     sidebar.html prefs.html help.html choosealpha.html topbar.html \
+	     sidebarend.html topbarend.html error.html \
+	     options options.labels \
+	     options.columns
+static_DATA=disorder.css
+staticdir=${pkgdatadir}/static
+
+EXTRA_DIST=${pkgdata_DATA} $(static_DATA)
+# arch-tag:c04ecae39c46f7a88463e746b857d913
diff --git a/templates/about.html b/templates/about.html
new file mode 100644
index 0000000..5688238
--- /dev/null
+++ b/templates/about.html
@@ -0,0 +1,75 @@
+
+
+
+ 
+@include:stdhead@
+  @label:about.title@
+ 
+ 
+@include{@label{menu}@}@
+   

@label:about.title@

+ +

Copyright

+ +

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

+ +

Copyright © 2003, 2004, 2005, 2006 Richard Kettlewell

+ +

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

+ +

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

+ +

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

+ +

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

+ +

Server Statistics

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

@label:choose.title@

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

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

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

+ @label:choose.directories@ +

+ @choose{directories}{ +

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

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

+ @label:choose.files@ +

+ @choose{files}{ +

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

+ }@ +
+

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

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

@label:choose.title@

+ +

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

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

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

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

@label:error.title@

+ +

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

+ +

@label:error.generic@

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

@label{help.title}@

+ + + +
+ +

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

+ +

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

+ +

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

+ +

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

+ +
+ + + +
+ +

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

+ +

At the top of the screen are the following controls:

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

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

+ +
+ + + +
+ +

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

+ +

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

+ +
+ + + +
+ +

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

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

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

+ +
+ + + +
+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ + +

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

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

context can be anything but standard + values are:

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

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

+ +

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

+ +
+ + + +
+ +

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

+ +

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

+ +

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

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

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

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

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

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

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

+ +
+ + + +
+ +

disorder_config(5) - + configuration

+ +

disorder(1) - command line + client

+ +

disobedience(1) - GTK+ + client

+ +

tkdisorder(1) - GUI + client

+ +

disorderd(8) - server

+ +

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

+ +

disorder(3) - C API

+ +

disorder_protocol(5) - + DisOrder control protocol

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

@label:playing.title@

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

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

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

@label:prefs.title@

+ +
+ + + + @files{ +

Preferences for @arg{@index@_file}@

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

+ +

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

@label:recent.title@

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

@label:search.title@

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

Artist: + @part:artist@

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

Album: + @part:album@

+ @search{title}{ +
+

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

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

@label:volume.title@

+ +
+

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

+
+ +@include{@label{menu}@end}@ + + +@@ + + -- [mdw]