--- /dev/null
+* 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
--- /dev/null
+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
--- /dev/null
+2004-10-04 00:59:42 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: proper track ordering in search.
+
+ * regsub.c, tracks.c: chattier error messages
+
+ * utf8.h: stricter UTF-8 parsing
+
+2004-09-26 10:47:45 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Makefile.am, plugins/Makefile.am: tidy up to build in separate
+ object directory
+
+2004-09-25 17:47:23 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * version 0.11
+
+2004-09-25 17:41:21 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * BUGS: some known bugs
+
+2004-09-25 17:25:49 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: chattier errors
+
+2004-09-25 17:17:25 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * api-client.c, cgi.c, cgimain.c, disorder.c, server.c: EXIT_FAILURE
+ pedantry
+
+2004-09-18 17:28:32 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * client.c: rework handling of commands that get lists back,
+ unbreaking 'stats' in the process.
+
+2004-09-18 16:54:58 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: add a lockfile to prevent concurrent access to databases.
+
+2004-07-31 19:05:38 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * debian/postinst, prerm: hopefuly better upgrade handling
+
+2004-07-31 18:44:52 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * queue.c: don't reverse the queue on restart
+
+ * Document queue/recent ordering a bit better.
+
+2004-07-20 20:22:09 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tkdisorder: primitive queue widget, simplify MonitorStateThread
+
+2004-07-20 19:42:52 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tkdisorder: python + tkinter gui, still under development
+
+2004-07-18 14:21:12 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Extra navigation links in 'Choose' screen allow easier navigation
+ back up the directory tree. It's not quite right yet because
+ track names are relative to the filesystem root rather than the root
+ of their collection.
+
+ New @navigate@, @fullname@, @dirname@, @basename@, @ne@ and @eq@
+ expansions.
+
+2004-07-18 13:22:53 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * use new label sidebar.choosewhich to determine which 'Choose' screen
+ to pick, rather than making operator edit sidebar.html
+
+ * mention config.USERNAME in README
+
+2004-07-18 13:05:45 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * debian/control: more detailed build deps
+
+2004-07-18 13:03:56 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * README: dependency version notes
+
+2004-07-18 12:57:55 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * cope with gcrypt version skew
+
+2004-07-18 02:46:07 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Control connections can go over the net (using anything that
+ getaddrinfo() knows about) using the new 'listen' and 'connect'
+ options.
+
+2004-07-17 19:01:25 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Increase/decrease volume buttons
+
+2004-07-17 16:35:18 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * templates/help.html: document 'Manage'
+
+ * templates/playing.html: extra empty buttons
+
+2004-07-17 16:24:12 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Any track that had a filesystem encoding that differed from itself
+ when converted to UTF-8 track (that is to say, any non-ASCII track in
+ a non-UTF-8 filesystem) would not be played, as only the UTF-8
+ version of the name was passed to the player.
+
+ * The disorder_play_track plugin interface now takes both the path and
+ the track name (the former being the raw bytes from the filesystem,
+ the latter being the UTF-8 version).
+
+ * Document that scratch names must be UTF-8 (or ASCII).
+
+2004-07-17 16:11:21 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * 'move' command in control protocol to move tracks around
+
+ * New 'Manage' page allows tracks to be moved around in the queue.
+
+2004-07-17 14:56:04 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.c: teach 'disorder play' to take multile tracks. Hence for
+ instance you could do:
+ disorder play /path/to/some/album/*.ogg
+
+2004-07-17 14:45:55 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * templates/help.html: mention choosealpha
+
+2004-07-17 14:37:18 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Use regexp-filtered file listings to implement choosealpha.html,
+ which split the top-level 'Choose' page up according to the initial
+ letter of the filenames.
+
+2004-07-17 14:19:33 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * files and directory listings can now be filtered using regexps (you
+ could do this in your client anyway but now the server does it for
+ you, thus causing less data to be transferred from server to
+ client).
+
+ * correct disorder.py's quoting of empty strings
+
+2004-07-17 13:07:51 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.py.in: correct exception-to-string functions
+
+2004-07-17 13:05:06 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.py.in: add another example
+
+2004-07-17 12:57:57 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.py.in: better docs and error handling
+
+2004-07-17 01:06:59 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.py.in: add a full set of methods
+
+2004-07-17 01:06:31 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * plugins/tracklength.c: quieten compiler
+
+2004-07-16 22:31:31 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * rudimentary Python client support
+
+2004-07-15 00:55:08 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * configuration.c: stricter syntax check for 'url'
+
+2004-07-15 00:41:22 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: clarify 'url' syntax
+
+ * templates/help.html: link to DisOrder control protocol page
+
+2004-07-14 19:46:38 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * config.sample.in, debian/disorder.config: play WAV files correctly
+
+ * debian/control: depend on sox
+
+ * scratches are now *.ogg files
+
+2004-07-11 19:45:24 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * plugins/tracklength.c: build fix
+
+2004-07-11 19:39:40 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * add notify_queue to notify plugin
+
+2004-07-11 19:01:26 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: always sync log after replay, so that upgrade works
+
+2004-07-11 18:54:33 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * new prefs.log file records all preferences in ASCII and guarantess
+ to be up to date. So provided you back up this file the rest of the
+ databases can be lost completely.
+
+ * document the use and properties of the various database files
+
+ * new prefsync configuration command controls interval between
+ minimization of prefs.log.
+
+ * event.c, event.h: timeouts get an associated handle which can be
+ used to cancel them.
+
+ * don't remove the search database at the drop of a hat as that would
+ require it to be rebuilt more frequently than is sensible.
+
+2004-07-11 18:38:52 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.c: missing 'break', oops!
+
+2004-07-10 19:43:42 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * plugins/tracklength.c: binary search over extensions
+ round up WAV duration
+
+2004-07-10 19:22:50 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * plugins/tracklength.c: tracklength plugin now knows about WAV files
+ remember to unmap files after parsing them!
+
+ * sounds/slap.wav: correct broken encoding (was two WAVs concatenated,
+ is now a single WAV; it worked with 'cat > /dev/audio' but doesn't
+ work with things that expect a single correctly formatted WAV file.)
+
+2004-07-10 18:11:26 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * 'disorder --length' allows command-line access to tracklength plugin
+
+2004-07-10 14:47:41 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * server.c: use libgcrypt for random numbers
+
+ * configure.ac: don't need dev/[u]random any more
+
+2004-07-10 13:33:16 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * add log command to stream logs to clients
+
+ * disorder_protocol.5.in: new man page documenting internal
+ communications protocol
+
+2004-07-10 11:51:54 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * use URL-encoding in dumps because it's more convenient to duck
+ encoding issues that way. It also looks more consistent with the
+ use of URL-encoding elsewhere in DisOrder.
+
+ * change logging interface so that messages are formatted only once.
+
+2004-07-10 11:40:13 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * printf.c: correct number bases!
+
+2004-07-10 00:00:33 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * send back our URL in Refresh HTTP header to work around weird Apache
+ behaviour
+
+ * log to (in theory) multiple outputs
+
+ * '#define NO_MEMORY_ALLOCATION' allows us to enforce rules about
+ files that shouldn't perform memory allocation
+
+ * split *printf frontends into separate files since some need memory
+ allocation and some do not
+
+2004-06-12 11:37:37 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Use a sink to handle writing to an ev_writer
+
+ * disorder-dump --dump now works against a running server, so it is
+ not necessary to take the server down to back up the preference
+ data.
+
+ * printf.c: correct handling of flags (which were completely broken)
+
+2004-05-24 15:56:30 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * new help page, plus HTML-ized versions of man pages
+
+ * de-dupe code in Makefile
+
+ * template names can't have / in and can't be dot-files
+ (previously they weren't allowed . in)
+
+ * @ is @ not (
+
+2004-05-24 14:00:38 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * templates/playing.html: put track title in <TITLE> element
+
+2004-05-24 13:58:29 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: remove dead code
+
+2004-05-23 19:29:16 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Makefile.am: distribute missing files
+
+ * dcgi.c: quiten compiler
+
+ * printf.c: missing base and padding settings
+
+2004-05-23 19:08:40 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.3: update for disorder_snprintf
+
+2004-05-23 19:01:31 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * New *printf implementation, which is guaranteed to behave reliable
+ in the face of strange encoding games (standard *printf insist on
+ MBC strings in the current encoding, which isn't great for us).
+ We don't support floating point yet.
+
+ * Log output is always ASCII (non-ASCII characters are escaped) so we
+ don't have to rely on the encoding of stderr or syslog.
+
+ * No longer depend on the target providing a working snprintf.
+
+ * Corrected a few broken *printf calls
+
+2004-05-22 16:55:02 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * reach preferences edit from 'Recent'
+
+2004-05-22 16:30:56 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * @search@ converted to new template infrastructure
+
+2004-05-21 21:55:55 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * optionally restrict scratch and/or remove to submitting user
+
+ * export 'become' command to command-line client
+
+ * determine username from UID, not environment
+
+2004-05-21 21:13:30 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: minor improvements
+
+2004-05-21 21:00:05 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: rearrange into alpha order
+
+2004-05-18 23:14:11 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.c: add {enable,disable}-random as synonyms for
+ random-{enable,disable}
+
+2004-05-18 20:18:02 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: prevent rapid web refresh is all playing is disabled
+
+2004-05-18 00:30:28 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * support libdb 4.2 as well as 3.2. Versions inbetween might well work
+ but this hasn't been tested.
+
+2004-05-16 23:43:34 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * debian/control: list build depends
+
+2004-05-16 23:34:40 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * #include <db.h> when checking libdb, to cope with variants that
+ redefine all the symbols.
+
+2004-05-16 23:28:55 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * new disorder-dump program to read/write prefs in text format
+ (e.g. for backup, database upgrade, replication)
+
+2004-05-16 10:37:10 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Makefile.am: clean generated files
+
+ * debian/autorules.m4: fix clean target
+
+ * debian/rules.m4: man pages in /usr/share/man
+
+2004-04-26 23:28:18 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * eliminate dependency on <inttypes.h>
+
+2004-04-26 20:59:20 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * convert UTF-8 -> UCS-4 directly, rather than relying on iconv (which
+ doesn't always know how to do it).
+
+2004-04-26 20:47:12 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Do not expand dcgi-generated txet in expansion argument values (as
+ it'll get expanded properly later on anyway).
+
+ * Add @urlquote@ expansion.
+
+2004-04-25 20:47:26 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * arg:directory, not arg:dir
+ Include containing directory name in choose.html
+
+2004-04-25 20:11:49 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: minor docs improvements.
+
+2004-04-25 19:53:19 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * unify transform- web options, and make them available via the
+ @transform@ expansion.
+
+2004-04-25 19:52:13 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: setting a trackname_ pref would cause a crash.
+
+2004-04-25 18:51:56 +0100 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Bring documentation up to date a bit.
+
+2004-04-25 18:29:49 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * @choose@ converted to new template infrastructure.
+
+2004-04-25 17:14:56 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * @prefs@ now takes a template argument, shifting the preferences
+ table furniture into the template file.
+
+2004-04-25 15:44:38 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Portability hacks of various degrees of nastiness in the interest of
+ building on FreeBSD.
+
+2004-04-18 19:00:52 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: add some missing bits.
+
+2004-04-18 18:05:17 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * @playing@ and @recent@ now take a template argument, allowing the
+ template file to contain the table furniture rather than having it
+ generated from inside dcgi. Templates updated accordingly.
+ Documentation updated.
+
+ * Moved the calculation of expected start times into the server.
+
+2004-04-18 13:45:19 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: booleans for the expansion language
+
+2004-04-18 13:13:46 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Expansions can now take multiple parameters using a new quoting
+ syntax. Parameters are (for all current expansions, but not for
+ some future one) recursively expanded before use.
+
+ * sink.c: interface for things that accept output (current
+ implementations being stdio and dynstrs)
+
+ * vacopy.h: find a va_copy somewhere.
+
+2004-04-17 16:48:00 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * version 0.10
+
+2004-04-17 16:25:59 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Typo fixes.
+
+2004-04-17 16:03:45 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * README: recommend basic authentication instead of digest auth, as
+ the latter seems too poorly supported.
+
+ * configure.ac: don't check for things that are very standard
+
+ * debian/control: add Section and Priority fields
+
+2004-04-04 17:45:34 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Build GNU getopt for systems where libc doesn't have it
+
+2004-04-04 15:11:49 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * configure.ac: check that various things are available and work at
+ configure time, rather than build or (worse still!) run time. More
+ library checks.
+
+ * charset.c: <wchar.h> no longer needed
+
+2004-04-04 12:06:16 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * server.c: attempt to play tracks when they are added to the queue.
+ Otherwise they wouldn't be played at all nothing was already
+ playing.
+
+2004-04-03 13:39:30 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Use libiconv if libc doesn't have iconv
+
+2004-04-03 12:42:05 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * configure.ac: check for both libdb and libdb3
+ report all missing libs at once
+
+2004-04-02 23:42:13 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Only use __attribute__ syntax under GNU C.
+
+2004-04-02 23:16:41 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * configure.ac: use whatever libdb the system defaults to.
+
+2004-04-02 23:06:50 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.c: add a --help-commands option giving a one-line summary
+ of each command.
+
+ * Updated documentation a bit.
+
+2004-04-02 20:20:47 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Ship another scratch sound.
+
+2004-04-02 20:10:10 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Debianization fixes:
+ + distribute postrm
+ + call Libtool correctly
+ + fix shared library dependencies
+
+ * Quieten compiler when optimization turned on.
+
+2004-04-02 19:27:59 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Tidy up appearance of preferences screen
+
+2004-03-28 20:01:40 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Track prefs editor in web interface. Rather rough-edged for now.
+
+2004-03-28 17:41:43 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: add @shell@ expansion
+
+2004-03-28 15:02:02 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: handle empty search result lists correctly.
+
+2004-03-28 14:38:01 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * split.c: quote empty strings properly
+
+2004-03-28 14:35:39 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * README.streams: how to play streams with DisOrder
+
+2004-03-28 14:10:51 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * play.c: unpick wait status of failed players
+
+2004-03-28 14:07:49 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * play.c: put players in their own process group and send SIGKILL
+ rather than SIGTERM (the latter because ogg123 doesn't seem to honor
+ SIGTERM when playing streams).
+
+2004-03-28 13:57:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Handle directories that contain both files and directories better
+ + dcgi.c: headings for directory/track lists
+ correct output for choose.playall label
+ + server.c: support file listing in the root
+ + Associated label and style sheet additions
+
+2004-03-28 12:35:20 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * New 'shell' player plugin executes a shell command with an environment
+ variable identifying the track. Optionally, the user can control what
+ shell is used.
+
+ * The sample config file knows how to play .wav files now.
+
+ * Ship a default scratch sound.
+
+2004-03-27 20:48:13 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Play tracks via a plugin. 'exec' plugin module provides previous
+ behaviour.
+
+2004-03-27 20:28:57 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * log interesting events if a username is attached (see previous
+ change)
+
+2004-03-27 19:30:44 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * server.c: don't log auth data any more - with the Refresh: HTTP
+ header causing disorder.cgi to run every few seconds you end up with
+ vast amounts of useless information. It would be more sensible to
+ log actions instead.
+
+2004-03-27 18:41:22 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Add a sidebar to the web interface, making everything available from
+ everywhere. Tidy up the default stylesheet.
+
+2004-03-27 18:19:40 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: eliminate a spurious </div>
+
+2004-03-27 17:17:16 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * When updating the track list, only process one track per select().
+ This makes the server much more responsive to clients during
+ rescans.
+
+2004-03-27 16:20:30 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Don't need file extensions in stopword list by default any more.
+
+2004-03-27 16:07:53 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Common code now lives in a shared library.
+ This library is not intended for other programs to link against,
+ so no ABI guarantees exist for it.
+
+ * The disorder(3) API is now implemented by shim functions rather than
+ aliases. These are always part of the executable that imports the
+ plugin, never the shared library. Varargs functions are a bit messy
+ as you can't just write a forwarding function for them.
+
+2004-03-27 14:22:22 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Volume control support.
+ + new 'mixer' and 'channel' configuration commands
+ + 'volume' server command
+ + disorder_set_volume and disorder_get_volume client functions
+ + get-volume and set-volume command-line client commands
+ + @volume:SPEAKER@ expansion to get current volumes
+ + 'volume' action to set volume
+ + volume.html template (a bit primitive still)
+
+2004-03-26 19:15:09 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Bring back 'Play all' button in @choose@, and fix track selection
+
+ * Scratch by ID, so that late scratches don't get the wrong track
+
+2004-03-25 00:40:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: report top 10 search words in server stats
+
+2004-03-24 23:24:45 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: de-dupe track/directory code in @choose@
+
+2004-03-22 23:31:55 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * dcgi.c: fine-tune sort order. Now we try the 'sort' transformation
+ or part context first - once case-folded, once raw; then we try the
+ 'display' version, again case-folded then raw; and finally we use
+ the unprocessed track name as as tie-breaker.
+
+ Repeated calls to casefold aren't very efficient, this should be
+ fixed.
+
+ * templates/options.transform: correct transform-track replacement
+ string
+
+2004-03-22 19:58:42 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Global and case-independent regexp replacement.
+
+ * Strip out punctuation for sorting directories in @choose@.
+
+2004-03-21 19:30:39 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: remove search database in track_sync(). It'll be removed
+ when we re-open anyway, and there's no point occupying the disk
+ space longer than necessary.
+
+ By the same token, we don't bother syncing it in track_sync().
+
+2004-03-21 18:47:31 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Sort strings are now case-folded before comparison.
+
+ * words.c: casefold now returns the original string if it was
+ malformed, rather than a null pointer.
+
+2004-03-21 17:04:48 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.h: new function disorder_track_set_data allows plugins to
+ set preferences as well as query them.
+
+ * disorder.3: document the above and add some general notes
+
+ * plugins/notify.c: record time and count tracks are played at
+
+ * plugins/pick.c: don't pick recently played tracks
+
+2004-03-21 16:06:21 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: server stats had the database types all wrong, leading to
+ a crash. Fixed.
+
+2004-03-21 15:14:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Separate sorting and display with new context arguments in
+ configuration and trackname_ preferences.
+
+ * By default track numbers take part in sorting, but are not displayed
+
+ * By default "The" is display unmodified but moved to the end for
+ sorting
+
+2004-03-21 14:37:36 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Group search results according to 'columns search' configuration.
+
+2004-03-21 14:25:35 Richard Kettlewell <rjk@greenend.org.uk>
+
+ cvs -Q up -j mergepoint-0-9-bugfixes-1 -j mergepoint-0-9-bugfixes-2
+
+2004-03-21 14:21:34 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * templates/search.html: fix broken HTML.
+
+2004-03-20 21:11:30 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Include trackname_ preferences in searches, and rebuild search
+ database from scratch on startup (to completely eliminate any
+ contamination due to prior configuration).
+
+2004-03-20 18:50:36 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * words.c: more characters are separators
+
+2004-03-20 18:44:45 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Split preferences into a separate database, and automatically
+ convert the old one.
+
+2004-03-18 00:11:26 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: correct scanning for directories, to correctly handle
+ cases where you have <path>/<prefix> and <path>/<prefix><suffix>.
+
+2004-03-17 00:20:55 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * config.sample.in: prefer mpg321 to mpg123
+
+2004-03-16 23:45:54 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Server statistics support:
+ o track_stats() reports stats from databases
+ o disorder_stats() reports stats in client
+ o stats command to command line interface
+ o @stats@ expansion in web interface
+
+ * about.html: provides information about DisOrder
+
+ * client.c: correct list parsing in client interface
+
+ * disorder_config.5.in: add missing docs for @arg@ and @search@
+
+2004-03-16 23:16:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Do all database writes synchronously and add some missing
+ transaction IDs. In fact the missing transaction IDs probably
+ ameliorated problems caused by the asynchronous writes (which leave
+ the database actually unusable, rather than merely with bits
+ missing).
+
+2004-03-16 20:30:00 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Improved track search:
+ o Instead of encoding all track names into a single db item, we use
+ sorted duplicate data items. libdb does all the work for us.
+ o Strip collection root and extensions from filenames
+ o Delete stopwords from search db at startup (in case new ones have
+ been added)
+ o Rename search db to search2.db so it doesn't conflict with the old
+ one
+ o Always update search db even if we've seen the track, so that
+ database format/name changes don't trash searching
+
+ * Add a few missing transaction ID arguments.
+
+2004-03-16 20:24:25 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * rescan.c: rescan would crash on shut down
+
+2004-03-16 12:43:54 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * quieten compiler when optimization turned on
+
+2004-03-16 12:12:40 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder_config.5.in: complete renaming of trackinfo.* to heading.*
+
+2004-03-15 23:18:02 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * version 0.9
+
+2004-03-15 19:56:50 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Define table columns with a new 'columns' web option, rather than by
+ relying on the ordering of trackinfo.* labels. trackinfo.* labels
+ are thus only used as headings and are renamed accordingly. The
+ button column is no longer magical.
+
+ * Always search the config directory and data directory for templates
+ and web options (after any explicitly configured directories).
+
+2004-03-15 19:17:02 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * make table classes more consistent
+
+2004-03-15 18:13:39 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: log closing databases. Checkpoint database at shutdown.
+
+2004-03-15 18:03:35 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * tracks.c: issue log messages from database checkpointing, since we
+ don't know how long they'll take.
+
+2004-03-15 17:31:35 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * disorder.init.in: make 'stop' work even if server not running.
+
+2004-03-15 14:37:59 Richard Kettlewell <rjk@greenend.org.uk>
+
+ Added debianization files.
+
+2004-03-15 13:51:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * cgi.c: avoid UB if template path is empty
+
+2004-03-14 16:33:12 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Makefile.am: correct dependencies on version script
+ distribute version script
+
+2004-03-14 16:30:56 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * eliminate zombie rescans.
+
+2004-03-14 16:25:37 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * eliminate clash with glibc error().
+
+2004-03-14 16:19:55 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * Use a version script to limit the exported symbols. There's a glibc
+ naming clash with error() which should work around. Also this
+ should be conditional on the linker actually supporting this
+ feature.
+
+2004-03-14 15:37:56 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * configure.ac: correct wrong handling of <gc.h>
+
+2004-03-14 15:04:44 Richard Kettlewell <rjk@greenend.org.uk>
+
+ * DisOrder has been rewritten in C.
+
+arch-tag:b64f2a539d1621207b46b5bd202244e9
--- /dev/null
+# 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
+
+
--- /dev/null
+#
+# 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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+* 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
--- /dev/null
+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
--- /dev/null
+* 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
--- /dev/null
+-*-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
--- /dev/null
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/* 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+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
--- /dev/null
+#! /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
--- /dev/null
+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
--- /dev/null
+/etc/disorder/config
+/etc/disorder/options
+/etc/init.d/disorder
+/usr/lib/cgi-bin/disorder/.htaccess
--- /dev/null
+#!/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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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
--- /dev/null
+Require valid-user
+AuthType basic
+AuthName jukebox
+AuthUserFile /etc/disorder/http.users
+# arch-tag:2c90f33e2ed8bd26f2a0091113abfe0b
--- /dev/null
+# 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
--- /dev/null
+#! /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
--- /dev/null
+#! /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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+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.
+
--- /dev/null
+#
+# 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
--- /dev/null
+properties
+ make return be the same as OK
+search
+ select tracks by tag
+
+# arch-tag:MyZQrifgPbO8ip4FqZo1eg
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#
+# 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
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#
+# 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
--- /dev/null
+* 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+.\"
+.\" 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
--- /dev/null
+#
+# 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
--- /dev/null
+
+/*
+ * 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 */
--- /dev/null
+#
+# 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
--- /dev/null
+# 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
--- /dev/null
+#! /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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#
+# 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
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/* 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+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
--- /dev/null
+#! /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
--- /dev/null
+#! /bin/sh
+#
+# This file is part of DisOrder
+# Copyright (C) 2004, 2005 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+set -e
+
+title=$(basename $1)
+
+cat <<EOF
+<html>
+ <head>
+@include{stdhead}@
+ <title>$title</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+EOF
+printf " <pre class=manpage>"
+# this is kind of painful using only BREs
+nroff -man "$1" | sed 's/&/\&/g;
+ s/</\</g;
+ s/>/\>/g;
+ s/@/\@/g;
+ s!\(.\)\b\1!<b>\1</b>!g;
+ s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
+ s!_\b\(.\)!<i>\1</i>!g;
+ s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
+ s!</\([bi]\)><\1>!!g'
+cat <<EOF
+</pre>
+@include{@label{menu}@end}@
+ </body>
+</html>
+EOF
+# arch-tag:c0096f33b8a8f7d88236043ed970ae83
--- /dev/null
+#! /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
--- /dev/null
+#! /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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+#! /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
--- /dev/null
+#
+# 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
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <locale.h>
+
+#include "client.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+
+static disorder_client *c;
+
+disorder_client *disorder_get_client(void) {
+ if(!c)
+ if(!(c = disorder_new(0))) exit(EXIT_FAILURE);
+ return c;
+}
+
+int disorder_track_exists(const char *track) {
+ int result;
+
+ return disorder_exists(c, track, &result) ? 0 : result;
+}
+
+const char *disorder_track_get_data(const char *track, const char *key) {
+ char *value;
+
+ if(disorder_get(c, track, key, &value)) return 0;
+ return value;
+}
+
+int disorder_track_set_data(const char *track,
+ const char *key,
+ const char *value) {
+ if(value)
+ return disorder_set(c, track, key, value);
+ else
+ return disorder_unset(c, track, key);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:3c00ffad971b689e5af1d9a9ff45a120 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <pcre.h>
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "event.h"
+#include "trackdb.h"
+
+int disorder_track_exists(const char *track) {
+ return trackdb_exists(track);
+}
+
+const char *disorder_track_get_data(const char *track, const char *key) {
+ return trackdb_get(track, key);
+}
+
+int disorder_track_set_data(const char *track,
+ const char *key, const char *value) {
+ return trackdb_set(track, key, value);
+}
+
+const char *disorder_track_random(void) {
+ return trackdb_random(16);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:b39fbeb5a45190fa30e7f3ac20cd6c34 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <syslog.h>
+
+#include "log.h"
+#include "mem.h"
+#include "disorder.h"
+#include "printf.h"
+
+/* shared implementation of vararg functions */
+#include "log-impl.h"
+#include "mem-impl.h"
+
+void *disorder_malloc(size_t n) {
+ return xmalloc(n);
+}
+
+void *disorder_realloc(void *p, size_t n) {
+ return xrealloc(p, n);
+}
+
+void *disorder_malloc_noptr(size_t n) {
+ return xmalloc_noptr(n);
+}
+
+void *disorder_realloc_noptr(void *p, size_t n) {
+ return xrealloc_noptr(p, n);
+}
+
+char *disorder_strdup(const char *p) {
+ return xstrdup(p);
+}
+
+char *disorder_strndup(const char *p, size_t n) {
+ return xstrndup(p, n);
+}
+
+int disorder_snprintf(char buffer[], size_t bufsize, const char *fmt, ...) {
+ int n;
+ va_list ap;
+
+ va_start(ap, fmt);
+ n = byte_vsnprintf(buffer, bufsize, fmt, ap);
+ va_end(ap);
+ return n;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a033645978e202c94260fc21959e59a7 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <stddef.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <limits.h>
+#include <fnmatch.h>
+#include <ctype.h>
+
+#include "mem.h"
+#include "log.h"
+#include "hex.h"
+#include "charset.h"
+#include "configuration.h"
+#include "table.h"
+#include "syscalls.h"
+#include "kvp.h"
+#include "vector.h"
+#include "split.h"
+#include "inputline.h"
+#include "regsub.h"
+#include "defs.h"
+#include "sink.h"
+#include "cgi.h"
+#include "printf.h"
+#include "mime.h"
+#include "utf8.h"
+
+struct kvp *cgi_args;
+
+/* options */
+struct column {
+ struct column *next;
+ char *name;
+ int ncolumns;
+ char **columns;
+};
+
+#define RELIST(x) struct re *x, **x##_tail = &x
+
+static int have_read_options;
+static struct kvp *labels;
+static struct column *columns;
+
+static void include_options(const char *name);
+
+static void cgi_parse_get(void) {
+ const char *q;
+
+ if(!(q = getenv("QUERY_STRING"))) fatal(0, "QUERY_STRING not set");
+ cgi_args = kvp_urldecode(q, strlen(q));
+}
+
+static void cgi_input(char **ptrp, size_t *np) {
+ const char *cl;
+ char *q;
+ size_t n, m = 0;
+ int r;
+
+ if(!(cl = getenv("CONTENT_LENGTH"))) fatal(0, "CONTENT_LENGTH not set");
+ n = atol(cl);
+ q = xmalloc_noptr(n + 1);
+ while(m < n) {
+ r = read(0, q + m, n - m);
+ if(r > 0)
+ m += r;
+ else if(r == 0)
+ fatal(0, "unexpected end of file reading request body");
+ else switch(errno) {
+ case EINTR: break;
+ default: fatal(errno, "error reading request body");
+ }
+ }
+ if(memchr(q, 0, n)) fatal(0, "null character in request body");
+ q[n + 1] = 0;
+ *ptrp = q;
+ if(np) *np = n;
+}
+
+static int cgi_field_callback(const char *name, const char *value,
+ void *u) {
+ char *disposition, *pname, *pvalue;
+ char **namep = u;
+
+ if(!strcmp(name, "content-disposition")) {
+ if(mime_rfc2388_content_disposition(value,
+ &disposition,
+ &pname,
+ &pvalue))
+ fatal(0, "error parsing Content-Disposition field");
+ if(!strcmp(disposition, "form-data")
+ && pname
+ && !strcmp(pname, "name")) {
+ if(*namep)
+ fatal(0, "duplicate Content-Disposition field");
+ *namep = pvalue;
+ }
+ }
+ return 0;
+}
+
+static int cgi_part_callback(const char *s,
+ void attribute((unused)) *u) {
+ char *name = 0;
+ struct kvp *k;
+
+ if(!(s = mime_parse(s, cgi_field_callback, &name)))
+ fatal(0, "error parsing part header");
+ if(!name) fatal(0, "no name found");
+ k = xmalloc(sizeof *k);
+ k->next = cgi_args;
+ k->name = name;
+ k->value = s;
+ cgi_args = k;
+ return 0;
+}
+
+static void cgi_parse_multipart(const char *boundary) {
+ char *q;
+
+ cgi_input(&q, 0);
+ if(mime_multipart(q, cgi_part_callback, boundary, 0))
+ fatal(0, "invalid multipart object");
+}
+
+static void cgi_parse_post(void) {
+ const char *ct;
+ char *q, *type, *pname, *pvalue;
+ size_t n;
+
+ if(!(ct = getenv("CONTENT_TYPE")))
+ ct = "application/x-www-form-urlencoded";
+ if(mime_content_type(ct, &type, &pname, &pvalue))
+ fatal(0, "invalid content type '%s'", ct);
+ if(!strcmp(type, "application/x-www-form-urlencoded")) {
+ cgi_input(&q, &n);
+ cgi_args = kvp_urldecode(q, n);
+ return;
+ }
+ if(!strcmp(type, "multipart/form-data")) {
+ if(!pname || strcmp(pname, "boundary"))
+ fatal(0, "expected a boundary parameter, found %s",
+ pname ? pname : "nothing");
+ cgi_parse_multipart(pvalue);
+ return;
+ }
+ fatal(0, "unrecognized content type '%s'", type);
+}
+
+void cgi_parse(void) {
+ const char *p;
+ struct kvp *k;
+
+ if(!(p = getenv("REQUEST_METHOD"))) fatal(0, "REQUEST_METHOD not set");
+ if(!strcmp(p, "GET"))
+ cgi_parse_get();
+ else if(!strcmp(p, "POST"))
+ cgi_parse_post();
+ else
+ fatal(0, "unknown request method %s", p);
+ for(k = cgi_args; k; k = k->next)
+ if(!validutf8(k->name)
+ || !validutf8(k->value))
+ fatal(0, "invalid UTF-8 sequence in cgi argument");
+}
+
+const char *cgi_get(const char *name) {
+ return kvp_get(cgi_args, name);
+}
+
+void cgi_output(cgi_sink *output, const char *fmt, ...) {
+ va_list ap;
+ int n;
+ char *r;
+
+ va_start(ap, fmt);
+ n = byte_vasprintf(&r, fmt, ap);
+ if(n < 0)
+ fatal(errno, "error calling byte_vasprintf");
+ if(output->quote)
+ r = cgi_sgmlquote(r, 0);
+ output->sink->write(output->sink, r, strlen(r));
+ va_end(ap);
+}
+
+void cgi_header(struct sink *output, const char *name, const char *value) {
+ sink_printf(output, "%s: %s\r\n", name, value);
+}
+
+void cgi_body(struct sink *output) {
+ sink_printf(output, "\r\n");
+}
+
+char *cgi_sgmlquote(const char *s, int raw) {
+ uint32_t *ucs, *p, c;
+ char *b, *bp;
+ int n;
+
+ if(!raw) {
+ if(!(ucs = utf82ucs4(s))) exit(EXIT_FAILURE);
+ } else {
+ ucs = xmalloc_noptr((strlen(s) + 1) * sizeof(uint32_t));
+ for(n = 0; s[n]; ++n)
+ ucs[n] = (unsigned char)s[n];
+ ucs[n] = 0;
+ }
+
+ n = 1;
+ /* estimate the length we'll need */
+ for(p = ucs; (c = *p); ++p) {
+ switch(c) {
+ default:
+ if(c > 127 || c < 32) {
+ case '"':
+ case '&':
+ case '<':
+ case '>':
+ n += 12;
+ break;
+ } else
+ n++;
+ }
+ }
+ /* format the string */
+ b = bp = xmalloc_noptr(n);
+ for(p = ucs; (c = *p); ++p) {
+ switch(c) {
+ default:
+ if(*p > 127 || *p < 32) {
+ case '"':
+ case '&':
+ case '<':
+ case '>':
+ bp += sprintf(bp, "&#%lu;", (unsigned long)c);
+ break;
+ } else
+ *bp++ = c;
+ }
+ }
+ *bp = 0;
+ return b;
+}
+
+void cgi_attr(struct sink *output, const char *name, const char *value) {
+ if(!value[strspn(value, "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789")])
+ sink_printf(output, "%s=%s", name, value);
+ else
+ sink_printf(output, "%s=\"%s\"", name, cgi_sgmlquote(value, 0));
+}
+
+void cgi_opentag(struct sink *output, const char *name, ...) {
+ va_list ap;
+ const char *n, *v;
+
+ sink_printf(output, "<%s", name);
+ va_start(ap, name);
+ while((n = va_arg(ap, const char *))) {
+ sink_printf(output, " ");
+ v = va_arg(ap, const char *);
+ if(v)
+ cgi_attr(output, n, v);
+ else
+ sink_printf(output, n);
+ }
+ sink_printf(output, ">");
+}
+
+void cgi_closetag(struct sink *output, const char *name) {
+ sink_printf(output, "</%s>", name);
+}
+
+static int template_open(const char *name,
+ const char *ext,
+ const char **filenamep) {
+ const char *dirs[2];
+ int fd = -1, n;
+ char *fullpath;
+
+ dirs[0] = pkgconfdir;
+ dirs[1] = pkgdatadir;
+ if(name[0] == '/') {
+ if((fd = open(name, O_RDONLY)) < 0) fatal(0, "cannot open %s", name);
+ *filenamep = name;
+ } else {
+ for(n = 0; n < config->templates.n + (int)(sizeof dirs / sizeof *dirs); ++n) {
+ byte_xasprintf(&fullpath, "%s/%s%s",
+ n < config->templates.n ? config->templates.s[n]
+ : dirs[n - config->templates.n],
+ name, ext);
+ if((fd = open(fullpath, O_RDONLY)) >= 0) break;
+ }
+ if(fd < 0) error(0, "cannot find %s%s in template path", name, ext);
+ *filenamep = fullpath;
+ }
+ return fd;
+}
+
+static int valid_template_name(const char *name) {
+ if(strchr(name, '/') || name[0] == '.')
+ return 0;
+ return 1;
+}
+
+void cgi_expand(const char *template,
+ const struct cgi_expansion *expansions,
+ size_t nexpansions,
+ cgi_sink *output,
+ void *u) {
+ int fd = -1;
+ int n;
+ off_t m;
+ char *b;
+ struct stat sb;
+
+ if(!valid_template_name(template))
+ fatal(0, "invalid template name '%s'", template);
+ if((fd = template_open(template, ".html", &template)) < 0)
+ exitfn(EXIT_FAILURE);
+ if(fstat(fd, &sb) < 0) fatal(errno, "cannot stat %s", template);
+ m = 0;
+ b = xmalloc_noptr(sb.st_size + 1);
+ while(m < sb.st_size) {
+ n = read(fd, b + m, sb.st_size - m);
+ if(n > 0) m += n;
+ else if(n == 0) fatal(0, "unexpected EOF reading %s", template);
+ else if(errno != EINTR) fatal(errno, "error reading %s", template);
+ }
+ b[sb.st_size] = 0;
+ xclose(fd);
+ cgi_expand_string(template, b, expansions, nexpansions, output, u);
+}
+
+void cgi_expand_string(const char *name,
+ const char *template,
+ const struct cgi_expansion *expansions,
+ size_t nexpansions,
+ cgi_sink *output,
+ void *u) {
+ int braces, n, m, line = 1, sline;
+ char *argname;
+ const char *p;
+ struct vector v;
+ struct dynstr d;
+ cgi_sink parameter_output;
+
+ while(*template) {
+ if(*template != '@') {
+ p = template;
+ while(*p && *p != '@') {
+ if(*p == '\n') ++line;
+ ++p;
+ }
+ output->sink->write(output->sink, template, p - template);
+ template = p;
+ continue;
+ }
+ vector_init(&v);
+ braces = 0;
+ ++template;
+ sline = line;
+ while(*template != '@') {
+ dynstr_init(&d);
+ if(*template == '{') {
+ /* bracketed arg */
+ ++template;
+ while(*template && (*template != '}' || braces > 0)) {
+ switch(*template) {
+ case '{': ++braces; break;
+ case '}': --braces; break;
+ case '\n': ++line; break;
+ }
+ dynstr_append(&d, *template++);
+ }
+ if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+ ++template;
+ /* skip whitespace after closing bracket */
+ while(isspace((unsigned char)*template))
+ ++template;
+ } else {
+ /* unbracketed arg */
+ /* leading whitespace is not significant in unquoted args */
+ while(isspace((unsigned char)*template))
+ ++template;
+ while(*template
+ && *template != '@' && *template != '{' && *template != ':') {
+ if(*template == '\n') ++line;
+ dynstr_append(&d, *template++);
+ }
+ if(*template == ':')
+ ++template;
+ if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
+ /* trailing whitespace is not significant in unquoted args */
+ while(d.nvec && (isspace((unsigned char)d.vec[d.nvec - 1])))
+ --d.nvec;
+ }
+ dynstr_terminate(&d);
+ vector_append(&v, d.vec);
+ }
+ ++template;
+ vector_terminate(&v);
+ /* @@ terminates this file */
+ if(v.nvec == 0)
+ break;
+ if((n = table_find(expansions,
+ offsetof(struct cgi_expansion, name),
+ sizeof (struct cgi_expansion),
+ nexpansions,
+ v.vec[0])) < 0)
+ fatal(0, "%s:%d: unknown expansion '%s'", name, line, v.vec[0]);
+ if(v.nvec - 1 < expansions[n].minargs)
+ fatal(0, "%s:%d: insufficient arguments to @%s@ (min %d, got %d)",
+ name, line, v.vec[0], expansions[n].minargs, v.nvec - 1);
+ if(v.nvec - 1 > expansions[n].maxargs)
+ fatal(0, "%s:%d: too many arguments to @%s@ (max %d, got %d)",
+ name, line, v.vec[0], expansions[n].maxargs, v.nvec - 1);
+ /* for ordinary expansions, recursively expand the arguments */
+ if(!(expansions[n].flags & EXP_MAGIC)) {
+ for(m = 1; m < v.nvec; ++m) {
+ dynstr_init(&d);
+ byte_xasprintf(&argname, "<%s:%d arg #%d>", name, sline, m);
+ parameter_output.quote = 0;
+ parameter_output.sink = sink_dynstr(&d);
+ cgi_expand_string(argname, v.vec[m],
+ expansions, nexpansions,
+ ¶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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <locale.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "client.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "disorder.h"
+#include "api-client.h"
+
+int main(int argc, char **argv) {
+ const char *user, *conf;
+ dcgi_global g;
+ dcgi_state s;
+ cgi_sink output;
+
+ mem_init(0);
+ if(argc > 0) progname = argv[0];
+ cgi_parse();
+ if((conf = getenv("DISORDER_CONFIG"))) configfile = xstrdup(conf);
+ if(getenv("DISORDER_DEBUG")) debugging = 1;
+ if(config_read()) exit(EXIT_FAILURE);
+ memset(&g, 0, sizeof g);
+ memset(&s, 0, sizeof s);
+ s.g = &g;
+ g.client = disorder_get_client();
+ output.quote = 1;
+ output.sink = sink_stdio("stdout", stdout);
+ if(!(user = getenv("REMOTE_USER"))) fatal(0, "REMOTE_USER is not set");
+ if(disorder_connect(g.client)) {
+ disorder_cgi_error(&output, &s, "connect");
+ return 0;
+ }
+ if(disorder_become(g.client, user)) {
+ disorder_cgi_error(&output, &s, "become");
+ return 0;
+ }
+ disorder_cgi(&output, &s);
+ if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:1c8016a19d7f117427c184c8aadf1bba */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <syslog.h>
+
+#include "daemonize.h"
+#include "syscalls.h"
+#include "log.h"
+
+void daemonize(const char *tag, int fac, const char *pidfile) {
+ pid_t pid, r;
+ int w, dn;
+ FILE *fp;
+
+ D(("daemonize tag=%s fac=%d pidfile=%s",
+ tag ? tag : "NULL", fac, pidfile ? pidfile : "NULL"));
+ /* make sure that FDs 0, 1, 2 all at least exist (and get a
+ * /dev/null) */
+ do {
+ if((dn = open("/dev/null", O_RDWR, 0)) < 0)
+ fatal(errno, "error opening /dev/null");
+ } while(dn < 3);
+ pid = xfork();
+ if(pid) {
+ /* Parent process. Wait for the first child to finish, then
+ * return to the caller. */
+ exitfn = _exit;
+ while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+ ;
+ if(r < 0) fatal(errno, "error calling waitpid");
+ if(w) error(0, "subprocess exited with wait status %#x", (unsigned)w);
+ _exit(0);
+ }
+ /* First child process. This will be the session leader, and will
+ * be transient. */
+ D(("first child pid=%lu", (unsigned long)getpid()));
+ if(setsid() < 0) fatal(errno, "error calling setsid");
+ /* we'll log to syslog */
+ openlog(tag, LOG_PID, fac);
+ log_default = &log_syslog;
+ /* stdin/out/err we lose */
+ xdup2(dn, 0);
+ xdup2(dn, 1);
+ xdup2(dn, 2);
+ xclose(dn);
+ pid = xfork();
+ if(pid)
+ _exit(0);
+ /* second child. Write a pidfile if someone wanted it. */
+ D(("second child pid=%lu", (unsigned long)getpid()));
+ if(pidfile) {
+ if(!(fp = fopen(pidfile, "w"))
+ || fprintf(fp, "%lu\n", (unsigned long)getpid()) < 0
+ || fclose(fp) < 0)
+ fatal(errno, "error creating %s", pidfile);
+ }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:dc74d9007ddce97d782d3dacf9aa2ed6 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <pcre.h>
+#include <assert.h>
+
+#include "client.h"
+#include "mem.h"
+#include "vector.h"
+#include "sink.h"
+#include "cgi.h"
+#include "dcgi.h"
+#include "log.h"
+#include "configuration.h"
+#include "table.h"
+#include "queue.h"
+#include "plugin.h"
+#include "split.h"
+#include "words.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "syscalls.h"
+#include "printf.h"
+#include "regsub.h"
+#include "defs.h"
+#include "trackname.h"
+
+static void expand(cgi_sink *output,
+ const char *template,
+ dcgi_state *ds);
+static void expandstring(cgi_sink *output,
+ const char *string,
+ dcgi_state *ds);
+
+struct entry {
+ const char *path;
+ const char *sort;
+ const char *display;
+};
+
+static const char *nonce(void) {
+ static unsigned long count;
+ char *s;
+
+ byte_xasprintf(&s, "%lx%lx%lx",
+ (unsigned long)time(0),
+ (unsigned long)getpid(),
+ count++);
+ return s;
+}
+
+static int compare_entry(const void *a, const void *b) {
+ const struct entry *ea = a, *eb = b;
+
+ return compare_tracks(ea->sort, eb->sort,
+ ea->display, eb->display,
+ ea->path, eb->path);
+}
+
+static const char *front_url(void) {
+ char *url;
+ const char *mgmt;
+
+ /* preserve management interface visibility */
+ if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
+ byte_xasprintf(&url, "%s?mgmt=true", config->url);
+ return url;
+ }
+ return config->url;
+}
+
+static void redirect(struct sink *output) {
+ const char *back;
+
+ cgi_header(output, "Location",
+ (back = cgi_get("back")) ? back : front_url());
+ cgi_body(output);
+}
+
+static void lookups(dcgi_state *ds, unsigned want) {
+ unsigned need;
+ struct queue_entry *r, *rnext;
+ const char *dir, *re;
+
+ if(ds->g->client && (need = want ^ (ds->g->flags & want)) != 0) {
+ if(need & DC_QUEUE)
+ disorder_queue(ds->g->client, &ds->g->queue);
+ if(need & DC_PLAYING)
+ disorder_playing(ds->g->client, &ds->g->playing);
+ if(need & DC_RECENT) {
+ /* we need to reverse the order of the list */
+ disorder_recent(ds->g->client, &r);
+ while(r) {
+ rnext = r->next;
+ r->next = ds->g->recent;
+ ds->g->recent = r;
+ r = rnext;
+ }
+ }
+ if(need & DC_VOLUME)
+ disorder_get_volume(ds->g->client,
+ &ds->g->volume_left, &ds->g->volume_right);
+ if(need & (DC_FILES|DC_DIRS)) {
+ if(!(dir = cgi_get("directory")))
+ dir = "";
+ re = cgi_get("regexp");
+ if(need & DC_DIRS)
+ if(disorder_directories(ds->g->client, dir, re,
+ &ds->g->dirs, &ds->g->ndirs))
+ ds->g->ndirs = 0;
+ if(need & DC_FILES)
+ if(disorder_files(ds->g->client, dir, re,
+ &ds->g->files, &ds->g->nfiles))
+ ds->g->nfiles = 0;
+ }
+ ds->g->flags |= need;
+ }
+}
+
+/* actions ********************************************************************/
+
+static void act_disable(cgi_sink *output,
+ dcgi_state *ds) {
+ if(ds->g->client)
+ disorder_disable(ds->g->client);
+ redirect(output->sink);
+}
+
+static void act_enable(cgi_sink *output,
+ dcgi_state *ds) {
+ if(ds->g->client)
+ disorder_enable(ds->g->client);
+ redirect(output->sink);
+}
+
+static void act_random_disable(cgi_sink *output,
+ dcgi_state *ds) {
+ if(ds->g->client)
+ disorder_random_disable(ds->g->client);
+ redirect(output->sink);
+}
+
+static void act_random_enable(cgi_sink *output,
+ dcgi_state *ds) {
+ if(ds->g->client)
+ disorder_random_enable(ds->g->client);
+ redirect(output->sink);
+}
+
+static void act_remove(cgi_sink *output,
+ dcgi_state *ds) {
+ const char *id;
+
+ if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+ if(ds->g->client)
+ disorder_remove(ds->g->client, id);
+ redirect(output->sink);
+}
+
+static void act_move(cgi_sink *output,
+ dcgi_state *ds) {
+ const char *id, *delta;
+
+ if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
+ if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
+ if(ds->g->client)
+ disorder_move(ds->g->client, id, atoi(delta));
+ redirect(output->sink);
+}
+
+static void act_scratch(cgi_sink *output,
+ dcgi_state *ds) {
+ if(ds->g->client)
+ disorder_scratch(ds->g->client, cgi_get("id"));
+ redirect(output->sink);
+}
+
+static void act_playing(cgi_sink *output, dcgi_state *ds) {
+ char r[1024];
+ long refresh = config->refresh, length;
+ time_t now, fin;
+ int random_enabled = 0;
+ int enabled = 0;
+
+ lookups(ds, DC_PLAYING|DC_QUEUE);
+ cgi_header(output->sink, "Content-Type", "text/html");
+ disorder_random_enabled(ds->g->client, &random_enabled);
+ disorder_enabled(ds->g->client, &enabled);
+ if(ds->g->playing
+ && ds->g->playing->state == playing_started /* i.e. not paused */
+ && !disorder_length(ds->g->client, ds->g->playing->track, &length)
+ && length
+ && ds->g->playing->sofar >= 0) {
+ /* Try to put the next refresh at the start of the next track. */
+ time(&now);
+ fin = now + length - ds->g->playing->sofar + config->gap;
+ if(now + refresh > fin)
+ refresh = fin - now;
+ }
+ if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
+ /* next track is a scratch, don't leave more than the inter-track gap */
+ if(refresh > config->gap)
+ refresh = config->gap;
+ }
+ if(!ds->g->playing && ((ds->g->queue
+ && ds->g->queue->state != playing_random)
+ || random_enabled) && enabled) {
+ /* no track playing but playing is enabled and there is something coming
+ * up, must be in a gap */
+ if(refresh > config->gap)
+ refresh = config->gap;
+ }
+ byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
+ front_url());
+ cgi_header(output->sink, "Refresh", r);
+ cgi_body(output->sink);
+ expand(output, "playing", ds);
+}
+
+static void act_play(cgi_sink *output,
+ dcgi_state *ds) {
+ const char *track, *dir;
+ char **tracks;
+ int ntracks, n;
+ struct entry *e;
+
+ if((track = cgi_get("file"))) {
+ disorder_play(ds->g->client, track);
+ } else if((dir = cgi_get("directory"))) {
+ if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
+ if(ntracks) {
+ e = xmalloc(ntracks * sizeof (struct entry));
+ for(n = 0; n < ntracks; ++n) {
+ e[n].path = tracks[n];
+ e[n].sort = trackname_transform("track", tracks[n], "sort");
+ e[n].display = trackname_transform("track", tracks[n], "display");
+ }
+ qsort(e, ntracks, sizeof (struct entry), compare_entry);
+ for(n = 0; n < ntracks; ++n)
+ disorder_play(ds->g->client, e[n].path);
+ }
+ }
+ /* XXX error handling */
+ redirect(output->sink);
+}
+
+static int clamp(int n, int min, int max) {
+ if(n < min)
+ return min;
+ if(n > max)
+ return max;
+ return n;
+}
+
+static const char *volume_url(void) {
+ char *url;
+
+ byte_xasprintf(&url, "%s?action=volume", config->url);
+ return url;
+}
+
+static void act_volume(cgi_sink *output, dcgi_state *ds) {
+ const char *l, *r, *d, *back;
+ int nd, changed = 0;;
+
+ if((d = cgi_get("delta"))) {
+ lookups(ds, DC_VOLUME);
+ nd = clamp(atoi(d), -255, 255);
+ disorder_set_volume(ds->g->client,
+ clamp(ds->g->volume_left + nd, 0, 255),
+ clamp(ds->g->volume_right + nd, 0, 255));
+ changed = 1;
+ } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
+ disorder_set_volume(ds->g->client, atoi(l), atoi(r));
+ changed = 1;
+ }
+ if(changed) {
+ /* redirect back to ourselves (but without the volume-changing bits in the
+ * URL) */
+ cgi_header(output->sink, "Location",
+ (back = cgi_get("back")) ? back : volume_url());
+ cgi_body(output->sink);
+ } else {
+ cgi_header(output->sink, "Content-Type", "text/html");
+ cgi_body(output->sink);
+ expand(output, "volume", ds);
+ }
+}
+
+static void act_prefs_errors(const char *msg,
+ void attribute((unused)) *u) {
+ fatal(0, "error splitting parts list: %s", msg);
+}
+
+static const char *numbered_arg(const char *argname, int numfile) {
+ char *fullname;
+
+ byte_xasprintf(&fullname, "%d_%s", numfile, argname);
+ return cgi_get(fullname);
+}
+
+static void process_prefs(dcgi_state *ds, int numfile) {
+ const char *file, *name, *value, *part, *parts, *current, *context;
+ char **partslist;
+
+ if(!(file = numbered_arg("file", numfile)))
+ /* The first file doesn't need numbering. */
+ if(numfile > 0 || !(file = cgi_get("file")))
+ return;
+ if((parts = numbered_arg("parts", numfile))
+ || (parts = cgi_get("parts"))) {
+ /* Default context is display. Other contexts not actually tested. */
+ if(!(context = numbered_arg("context", numfile))) context = "display";
+ partslist = split(parts, 0, 0, act_prefs_errors, 0);
+ while((part = *partslist++)) {
+ if(!(value = numbered_arg(part, numfile)))
+ continue;
+ /* If it's already right (whether regexps or db) don't change anything,
+ * so we don't fill the database up with rubbish. */
+ if(disorder_part(ds->g->client, (char **)¤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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "kvp.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { "no-debug", no_argument, 0, 'D' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorder-deadlock [OPTIONS]\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n"
+ "\n"
+ "Deadlock manager for DisOrder. Not intended to be run\n"
+ "directly.\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+ xprintf("disorder-deadlock version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+int main(int argc, char **argv) {
+ int n, err, aborted;
+
+ set_progname(argv);
+ mem_init(0);
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ case 'D': debugging = 0; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ /* if stderr is a TTY then log there, otherwise to syslog */
+ if(!isatty(2)) {
+ openlog(progname, LOG_PID, LOG_DAEMON);
+ log_default = &log_syslog;
+ }
+ if(config_read()) fatal(0, "cannot read configuration");
+ info("started");
+ trackdb_init(0);
+ while(getppid() != 1) {
+ if((err = trackdb_env->lock_detect(trackdb_env,
+ 0,
+ DB_LOCK_DEFAULT,
+ &aborted)))
+ fatal(0, "trackdb_env->lock_detect: %s", db_strerror(err));
+ if(aborted)
+ D(("aborted %d lock requests", aborted));
+ sleep(1);
+ }
+ /* if our parent goes away, it's time to stop */
+ info("stopped (parent terminated)");
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:8VzpRqqo2qifzXWK1GxF5A */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <getopt.h>
+#include <pwd.h>
+#include <grp.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <locale.h>
+#include <syslog.h>
+#include <sys/time.h>
+#include <pcre.h>
+#include <fcntl.h>
+
+#include "daemonize.h"
+#include "event.h"
+#include "log.h"
+#include "configuration.h"
+#include "trackdb.h"
+#include "queue.h"
+#include "mem.h"
+#include "play.h"
+#include "server.h"
+#include "state.h"
+#include "syscalls.h"
+#include "defs.h"
+#include "user.h"
+#include "mixer.h"
+#include "eventlog.h"
+
+static ev_source *ev;
+
+static void rescan_after(long offset);
+static void dbgc_after(long offset);
+static void volumecheck_after(long offset);
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { "foreground", no_argument, 0, 'f' },
+ { "log", required_argument, 0, 'l' },
+ { "pidfile", required_argument, 0, 'P' },
+ { "no-initial-rescan", no_argument, 0, 'N' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorderd [OPTIONS]\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n"
+ " --foreground, -f Do not become a daemon\n"
+ " --pidfile PATH, -P PATH Leave a pidfile\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+ xprintf("disorderd version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+/* SIGHUP callback */
+static int handle_sighup(ev_source attribute((unused)) *ev_,
+ int attribute((unused)) sig,
+ void attribute((unused)) *u) {
+ info("received SIGHUP");
+ reconfigure(ev, 1);
+ return 0;
+}
+
+/* fatal signals */
+
+static int handle_sigint(ev_source attribute((unused)) *ev_,
+ int attribute((unused)) sig,
+ void attribute((unused)) *u) {
+ info("received SIGINT");
+ quit(ev);
+}
+
+static int handle_sigterm(ev_source attribute((unused)) *ev_,
+ int attribute((unused)) sig,
+ void attribute((unused)) *u) {
+ info("received SIGTERM");
+ quit(ev);
+}
+
+static int rescan_again(ev_source *ev_,
+ const struct timeval attribute((unused)) *now,
+ void attribute((unused)) *u) {
+ trackdb_rescan(ev_);
+ rescan_after(86400);
+ return 0;
+}
+
+static void rescan_after(long offset) {
+ struct timeval w;
+
+ gettimeofday(&w, 0);
+ w.tv_sec += offset;
+ ev_timeout(ev, 0, &w, rescan_again, 0);
+}
+
+static int dbgc_again(ev_source attribute((unused)) *ev_,
+ const struct timeval attribute((unused)) *now,
+ void attribute((unused)) *u) {
+ trackdb_gc();
+ dbgc_after(60);
+ return 0;
+}
+
+static void dbgc_after(long offset) {
+ struct timeval w;
+
+ gettimeofday(&w, 0);
+ w.tv_sec += offset;
+ ev_timeout(ev, 0, &w, dbgc_again, 0);
+}
+
+static int volumecheck_again(ev_source attribute((unused)) *ev_,
+ const struct timeval attribute((unused)) *now,
+ void attribute((unused)) *u) {
+ int l, r;
+ char lb[32], rb[32];
+
+ if(!mixer_control(&l, &r, 0)) {
+ if(l != volume_left || r != volume_right) {
+ volume_left = l;
+ volume_right = r;
+ snprintf(lb, sizeof lb, "%d", l);
+ snprintf(rb, sizeof rb, "%d", r);
+ eventlog("volume", lb, rb, (char *)0);
+ }
+ }
+ volumecheck_after(60);
+ return 0;
+}
+
+static void volumecheck_after(long offset) {
+ struct timeval w;
+
+ gettimeofday(&w, 0);
+ w.tv_sec += offset;
+ ev_timeout(ev, 0, &w, volumecheck_again, 0);
+}
+
+ int main(int argc, char **argv) {
+ int n, background = 1;
+ const char *pidfile = 0;
+ int initial_rescan = 1;
+
+ set_progname(argv);
+ mem_init(1);
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ /* garbage-collect PCRE's memory */
+ pcre_malloc = xmalloc;
+ pcre_free = xfree;
+ while((n = getopt_long(argc, argv, "hVc:dfP:N", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ case 'f': background = 0; break;
+ case 'P': pidfile = optarg; break;
+ case 'N': initial_rescan = 0; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ /* go into background if necessary */
+ if(background)
+ daemonize(progname, LOG_DAEMON, pidfile);
+ info("process ID %lu", (unsigned long)getpid());
+ srand(time(0)); /* don't start the same every time */
+ /* create event loop */
+ ev = ev_new();
+ if(ev_child_setup(ev)) fatal(0, "ev_child_setup failed");
+ /* read config */
+ if(config_read())
+ fatal(0, "cannot read configuration");
+ /* Start the speaker process (as root! - so it can choose its nice value) */
+ speaker_setup(ev);
+ /* set server nice value _after_ starting the speaker, so that they
+ * are independently niceable */
+ xnice(config->nice_server);
+ /* change user */
+ become_mortal();
+ /* make sure we're not root, whatever the config says */
+ if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+ /* open a lockfile - we only want one copy of the server to run at once. */
+ if(config->lock) {
+ const char *lockfile;
+ int lockfd;
+ struct flock lock;
+
+ lockfile = config_get_file("lock");
+ if((lockfd = open(lockfile, O_RDWR|O_CREAT, 0600)) < 0)
+ fatal(errno, "error opening %s", lockfile);
+ cloexec(lockfd);
+ memset(&lock, 0, sizeof lock);
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ if(fcntl(lockfd, F_SETLK, &lock) < 0)
+ fatal(errno, "error locking %s", lockfile);
+ }
+ /* initialize database environment */
+ trackdb_init(TRACKDB_NORMAL_RECOVER);
+ trackdb_master(ev);
+ /* install new config */
+ reconfigure(ev, 0);
+ /* re-read config if we receive a SIGHUP */
+ if(ev_signal(ev, SIGHUP, handle_sighup, 0)) fatal(0, "ev_signal failed");
+ /* exit on SIGINT/SIGTERM */
+ if(ev_signal(ev, SIGINT, handle_sigint, 0)) fatal(0, "ev_signal failed");
+ if(ev_signal(ev, SIGTERM, handle_sigterm, 0)) fatal(0, "ev_signal failed");
+ /* ignore SIGPIPE */
+ signal(SIGPIPE, SIG_IGN);
+ /* start a rescan straight away */
+ if(initial_rescan)
+ trackdb_rescan(ev);
+ rescan_after(86400);
+ /* periodically tidy up the database */
+ dbgc_after(60);
+ /* periodically check the volume */
+ volumecheck_after(60);
+ /* set initial state */
+ add_random_track();
+ play(ev);
+ /* enter the event loop */
+ n = ev_run(ev);
+ /* if we exit the event loop, something must have gone wrong */
+ fatal(errno, "ev_run returned %d", n);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:eaf909880c8fd3d1fef94ca8f12efe78 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <pcre.h>
+#include <unistd.h>
+#include <db.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "client.h"
+#include "sink.h"
+#include "mem.h"
+#include "defs.h"
+#include "printf.h"
+#include "kvp.h"
+#include "vector.h"
+#include "inputline.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "charset.h"
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "dump", no_argument, 0, 'd' },
+ { "undump", no_argument, 0, 'u' },
+ { "debug", no_argument, 0, 'D' },
+ { "recover", no_argument, 0, 'r' },
+ { "recover-fatal", no_argument, 0, 'R' },
+ { "trackdb", no_argument, 0, 't' },
+ { "searchdb", no_argument, 0, 's' },
+ { "recompute-aliases", no_argument, 0, 'a' },
+ { "remove-pathless", no_argument, 0, 'P' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorder-dump [OPTIONS] --dump|--undump PATH\n"
+ " disorder-dump [OPTIONS] --recompute-aliases\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --dump, -d Dump state to PATH\n"
+ " --undump, -u Restore state from PATH\n"
+ " --recover, -r Run database recovery\n"
+ " --recompute-aliases, -a Recompute aliases\n"
+ " --remove-pathless, -P Remove pathless tracks\n"
+ " --debug Debug mode\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+ xprintf("disorder-dump version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+/* dump prefs to FP, return nonzero on error */
+static void do_dump(FILE *fp, const char *tag,
+ int tracksdb, int searchdb) {
+ DBC *cursor = 0;
+ DB_TXN *tid;
+ struct sink *s = sink_stdio(tag, fp);
+ int err;
+ DBT k, d;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(fseek(fp, 0, SEEK_SET) < 0)
+ fatal(errno, "error calling fseek");
+ if(fflush(fp) < 0)
+ fatal(errno, "error calling fflush");
+ if(ftruncate(fileno(fp), 0) < 0)
+ fatal(errno, "error calling ftruncate");
+ if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
+ fatal(errno, "error writing to %s", tag);
+ cursor = trackdb_opencursor(trackdb_prefsdb, tid);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST);
+ while(err == 0) {
+ if(fputc('P', fp) < 0
+ || urlencode(s, k.data, k.size)
+ || fputc('\n', fp) < 0
+ || urlencode(s, d.data, d.size)
+ || fputc('\n', fp) < 0)
+ fatal(errno, "error writing to %s", tag);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+ cursor = 0;
+
+ if(tracksdb) {
+ cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST);
+ while(err == 0) {
+ if(fputc('T', fp) < 0
+ || urlencode(s, k.data, k.size)
+ || fputc('\n', fp) < 0
+ || urlencode(s, d.data, d.size)
+ || fputc('\n', fp) < 0)
+ fatal(errno, "error writing to %s", tag);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+ cursor = 0;
+ }
+
+ if(searchdb) {
+ cursor = trackdb_opencursor(trackdb_searchdb, tid);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST);
+ while(err == 0) {
+ if(fputc('S', fp) < 0
+ || urlencode(s, k.data, k.size)
+ || fputc('\n', fp) < 0
+ || urlencode(s, d.data, d.size)
+ || fputc('\n', fp) < 0)
+ fatal(errno, "error writing to %s", tag);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } cursor = 0;
+ }
+
+ if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag);
+ if(err == DB_LOCK_DEADLOCK) {
+ error(0, "c->c_get: %s", db_strerror(err));
+ goto fail;
+ }
+ if(err && err != DB_NOTFOUND)
+ fatal(0, "cursor->c_get: %s", db_strerror(err));
+ if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+ break;
+fail:
+ trackdb_closecursor(cursor);
+ cursor = 0;
+ info("aborting transaction and retrying dump");
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ if(fflush(fp) < 0) fatal(errno, "error writing to %s", tag);
+ /* caller might not be paranoid so we are paranoid on their behalf */
+ if(fsync(fileno(fp)) < 0) fatal(errno, "error syncing %s", tag);
+}
+
+/* delete all aliases prefs, return 0 or DB_LOCK_DEADLOCK */
+static int remove_aliases(DB_TXN *tid, int remove_pathless) {
+ DBC *cursor;
+ int err;
+ DBT k, d;
+ struct kvp *data;
+ int alias, pathless;
+
+ info("removing aliases");
+ cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+ if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST)) == DB_LOCK_DEADLOCK) {
+ error(0, "cursor->c_get: %s", db_strerror(err));
+ goto done;
+ }
+ while(err == 0) {
+ data = kvp_urldecode(d.data, d.size);
+ alias = !!kvp_get(data, "_alias_for");
+ pathless = !kvp_get(data, "_path");
+ if(pathless && !remove_pathless)
+ info("no _path for %s", utf82mb(xstrndup(k.data, k.size)));
+ if(alias || (remove_pathless && pathless)) {
+ switch(err = cursor->c_del(cursor, 0)) {
+ case 0: break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "cursor->c_get: %s", db_strerror(err));
+ goto done;
+ default:
+ fatal(0, "cursor->c_del: %s", db_strerror(err));
+ }
+ }
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), DB_NEXT);
+ }
+ if(err == DB_LOCK_DEADLOCK) {
+ error(0, "cursor operation: %s", db_strerror(err));
+ goto done;
+ }
+ if(err != DB_NOTFOUND) fatal(0, "cursor->c_get: %s", db_strerror(err));
+ err = 0;
+done:
+ if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+ return err;
+}
+
+/* truncate (i.e. empty) a database, return 0 or DB_LOCK_DEADLOCK */
+static int truncdb(DB_TXN *tid, DB *db) {
+ int err;
+ u_int32_t count;
+
+ switch(err = db->truncate(db, tid, &count, 0)) {
+ case 0: break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "db->truncate: %s", db_strerror(err));
+ break;
+ default:
+ fatal(0, "db->truncate: %s", db_strerror(err));
+ }
+ return err;
+}
+
+/* read a DBT from FP, return 0 on success or -1 on input error */
+static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
+ char *s;
+ struct dynstr d;
+
+ if(inputline(tag, fp, &s, '\n')) return -1;
+ dynstr_init(&d);
+ if(urldecode(sink_dynstr(&d), s, strlen(s)))
+ fatal(0, "invalid URL-encoded data in %s", tag);
+ dbt->data = d.vec;
+ dbt->size = d.nvec;
+ return 0;
+}
+
+/* undump from FP, return 0 or DB_LOCK_DEADLOCK */
+static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
+ int err, c;
+ DBT k, d;
+
+ info("undumping");
+ if(fseek(fp, 0, SEEK_SET) < 0)
+ fatal(errno, "error calling fseek on %s", tag);
+ if((err = truncdb(tid, trackdb_prefsdb))) return err;
+ if((err = truncdb(tid, trackdb_searchdb))) return err;
+ c = getc(fp);
+ while(!ferror(fp) && !feof(fp)) {
+ switch(c) {
+ case 'V':
+ c = getc(fp);
+ if(c != '0')
+ fatal(0, "unknown version '%c'", c);
+ break;
+ case 'E':
+ return 0;
+ case 'P':
+ if(undump_dbt(fp, tag, prepare_data(&k))
+ || undump_dbt(fp, tag, prepare_data(&d)))
+ break;
+ switch(err = trackdb_prefsdb->put(trackdb_prefsdb, tid, &k, &d, 0)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating prefs.db: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating prefs.db: %s", db_strerror(err));
+ }
+ break;
+ case 'T':
+ case 'S':
+ if(undump_dbt(fp, tag, prepare_data(&k))
+ || undump_dbt(fp, tag, prepare_data(&d)))
+ break;
+ /* We don't restore the tracks.db or search.db entries, instead
+ * we recompute them */
+ break;
+ case '\n':
+ break;
+ }
+ c = getc(fp);
+ }
+ if(ferror(fp))
+ fatal(errno, "error reading %s", tag);
+ else
+ fatal(0, "unexpected EOF reading %s", tag);
+ return 0;
+}
+
+/* recompute aliases and search database from prefs, return 0 or
+ * DB_LOCK_DEADLOCK */
+static int recompute_aliases(DB_TXN *tid) {
+ DBC *cursor;
+ DBT k, d;
+ int err;
+ struct kvp *data;
+ const char *path, *track;
+
+ info("recomputing aliases");
+ cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+ if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST)) == DB_LOCK_DEADLOCK) goto done;
+ while(err == 0) {
+ data = kvp_urldecode(d.data, d.size);
+ track = xstrndup(k.data, k.size);
+ if(!kvp_get(data, "_alias_for")) {
+ if(!(path = kvp_get(data, "_path")))
+ error(0, "%s is not an alias but has no path", utf82mb(track));
+ else
+ if((err = trackdb_notice_tid(track, path, tid)) == DB_LOCK_DEADLOCK)
+ goto done;
+ }
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ switch(err) {
+ case 0:
+ break;
+ case DB_NOTFOUND:
+ err = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ break;
+ default:
+ fatal(0, "cursor->c_get: %s", db_strerror(err));
+ }
+done:
+ if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
+ return err;
+}
+
+/* restore prefs from FP */
+static void do_undump(FILE *fp, const char *tag, int remove_pathless) {
+ DB_TXN *tid;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(remove_aliases(tid, remove_pathless)
+ || undump_from_fp(tid, fp, tag)
+ || recompute_aliases(tid)) goto fail;
+ break;
+fail:
+ info("aborting transaction and retrying undump");
+ trackdb_abort_transaction(tid);
+ }
+ info("committing undump");
+ trackdb_commit_transaction(tid);
+}
+
+/* just recompute alisaes */
+static void do_recompute(int remove_pathless) {
+ DB_TXN *tid;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(remove_aliases(tid, remove_pathless)
+ || recompute_aliases(tid)) goto fail;
+ break;
+fail:
+ info("aborting transaction and retrying recomputation");
+ trackdb_abort_transaction(tid);
+ }
+ info("committing recomputed aliases");
+ trackdb_commit_transaction(tid);
+}
+
+int main(int argc, char **argv) {
+ int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
+ int tracksdb = 0, searchdb = 0, remove_pathless = 0;
+ const char *path;
+ char *tmp;
+ FILE *fp;
+
+ mem_init(1);
+ while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': dump = 1; break;
+ case 'u': undump = 1; break;
+ case 'D': debugging = 1; break;
+ case 't': tracksdb = 1; break;
+ case 's': searchdb = 1; break;
+ case 'r': recover = TRACKDB_NORMAL_RECOVER;
+ case 'R': recover = TRACKDB_FATAL_RECOVER;
+ case 'a': recompute = 1; break;
+ case 'P': remove_pathless = 1; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ if(dump + undump + recompute != 1)
+ fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
+ if((undump || recompute) && (tracksdb || searchdb))
+ fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases");
+ if(recompute) {
+ if(optind != argc)
+ fatal(0, "--recompute-aliases does not take a filename");
+ path = 0;
+ } else {
+ if(optind >= argc)
+ fatal(0, "missing dump file name");
+ if(optind + 1 < argc)
+ fatal(0, "specify only a dump file name");
+ path = argv[optind];
+ }
+ if(config_read()) fatal(0, "cannot read configuration");
+ trackdb_init(recover);
+ trackdb_open();
+ if(dump) {
+ /* we write to a temporary file and rename into place */
+ byte_xasprintf(&tmp, "%s.%lx.tmp", path, (unsigned long)getpid());
+ if(!(fp = fopen(tmp, "w"))) fatal(errno, "error opening %s", tmp);
+ do_dump(fp, tmp, tracksdb, searchdb);
+ if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
+ if(rename(tmp, path) < 0)
+ fatal(errno, "error renaming %s to %s", tmp, path);
+ } else if(undump) {
+ /* the databases or logfiles might end up with wrong permissions
+ * if new ones are created */
+ if(getuid() == 0) info("you might need to chown database files");
+ if(!(fp = fopen(path, "r"))) fatal(errno, "error opening %s", path);
+ do_undump(fp, path, remove_pathless);
+ xfclose(fp);
+ } else if(recompute) {
+ do_recompute(remove_pathless);
+ }
+ trackdb_close();
+ trackdb_deinit();
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:a446d6d9fcfbece4e3042e29c148a1cc */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <time.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <sys/socket.h>
+#include <string.h>
+#include <stdio.h>
+#include <pcre.h>
+#include <ao/ao.h>
+
+#include "event.h"
+#include "log.h"
+#include "mem.h"
+#include "configuration.h"
+#include "queue.h"
+#include "trackdb.h"
+#include "play.h"
+#include "plugin.h"
+#include "wstat.h"
+#include "eventlog.h"
+#include "logfd.h"
+#include "syscalls.h"
+#include "speaker.h"
+#include "disorder.h"
+#include "signame.h"
+#include "hash.h"
+
+#define SPEAKER "disorder-speaker"
+
+struct queue_entry *playing;
+int paused;
+
+static void finished(ev_source *ev);
+
+static int speaker_fd = -1;
+static hash *player_pids;
+static int shutting_down;
+
+static void store_player_pid(const char *id, pid_t pid) {
+ if(!player_pids) player_pids = hash_new(sizeof (pid_t));
+ hash_add(player_pids, id, &pid, HASH_INSERT_OR_REPLACE);
+}
+
+static pid_t find_player_pid(const char *id) {
+ pid_t *pidp;
+
+ if(player_pids && (pidp = hash_find(player_pids, id))) return *pidp;
+ return -1;
+}
+
+static void forget_player_pid(const char *id) {
+ if(player_pids) hash_remove(player_pids, id);
+}
+
+/* called when speaker process terminates */
+static int speaker_terminated(ev_source attribute((unused)) *ev,
+ pid_t attribute((unused)) pid,
+ int attribute((unused)) status,
+ const struct rusage attribute((unused)) *rusage,
+ void attribute((unused)) *u) {
+ if(status)
+ error(0, "speaker subprocess terminated with status %s",
+ wstat(status));
+ return 0;
+}
+
+/* called when speaker process has something to say */
+static int speaker_readable(ev_source *ev, int fd,
+ void attribute((unused)) *u) {
+ struct speaker_message sm;
+ int ret = speaker_recv(fd, &sm, 0);
+
+ if(ret < 0) return 0; /* EAGAIN */
+ if(!ret) { /* EOF */
+ ev_fd_cancel(ev, ev_read, fd);
+ return 0;
+ }
+ switch(sm.type) {
+ case SM_PAUSED:
+ /* track ID is paused, DATA seconds played */
+ D(("SM_PAUSED %s %ld", sm.id, sm.data));
+ playing->sofar = sm.data;
+ break;
+ case SM_FINISHED:
+ /* the playing track finished */
+ D(("SM_FINISHED %s", sm.id));
+ finished(ev);
+ break;
+ case SM_PLAYING:
+ /* track ID is playing, DATA seconds played */
+ D(("SM_PLAYING %s %ld", sm.id, sm.data));
+ playing->sofar = sm.data;
+ break;
+ default:
+ error(0, "unknown message type %d", sm.type);
+ }
+ return 0;
+}
+
+void speaker_setup(ev_source *ev) {
+ int sp[2], lfd;
+ pid_t pid;
+
+ if(socketpair(PF_UNIX, SOCK_DGRAM, 0, sp) < 0)
+ fatal(errno, "error calling socketpair");
+ if(!isatty(2))
+ lfd = logfd(ev, SPEAKER);
+ else
+ lfd = -1;
+ if(!(pid = xfork())) {
+ exitfn = _exit;
+ ev_signal_atfork(ev);
+ xdup2(sp[0], 0);
+ xdup2(sp[0], 1);
+ xclose(sp[0]);
+ xclose(sp[1]);
+ if(lfd != -1) {
+ xdup2(lfd, 2);
+ xclose(lfd);
+ }
+ signal(SIGPIPE, SIG_DFL);
+#if 0
+ execlp("valgrind", "valgrind", SPEAKER, "--config", configfile,
+ debugging ? "--debug" : "--no-debug", (char *)0);
+#else
+ execlp(SPEAKER, SPEAKER, "--config", configfile,
+ debugging ? "--debug" : "--no-debug", (char *)0);
+#endif
+ fatal(errno, "error invoking %s", SPEAKER);
+ }
+ ev_child(ev, pid, 0, speaker_terminated, 0);
+ speaker_fd = sp[1];
+ xclose(sp[0]);
+ cloexec(speaker_fd);
+ /* Don't need to make speaker_fd nonblocking because speaker_recv() uses
+ * MSG_DONTWAIT. */
+ ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0);
+}
+
+void speaker_reload(void) {
+ struct speaker_message sm;
+
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_RELOAD;
+ speaker_send(speaker_fd, &sm, -1);
+}
+
+/* timeout for play retry */
+static int play_again(ev_source *ev,
+ const struct timeval attribute((unused)) *now,
+ void attribute((unused)) *u) {
+ D(("play_again"));
+ play(ev);
+ return 0;
+}
+
+/* try calling play() again after @offset@ seconds */
+static void retry_play(ev_source *ev, int offset) {
+ struct timeval w;
+
+ D(("retry_play(%d)", offset));
+ gettimeofday(&w, 0);
+ w.tv_sec += offset;
+ ev_timeout(ev, 0, &w, play_again, 0);
+}
+
+/* Called when the currently playing track finishes playing. This
+ * might be because the player finished or because the speaker process
+ * told us so. */
+static void finished(ev_source *ev) {
+ D(("finished playing=%p", (void *)playing));
+ if(!playing)
+ return;
+ if(playing->state != playing_scratched)
+ notify_not_scratched(playing->track, playing->submitter);
+ switch(playing->state) {
+ case playing_ok:
+ eventlog("completed", playing->track, (char *)0);
+ break;
+ case playing_scratched:
+ eventlog("scratched", playing->track, playing->scratched, (char *)0);
+ break;
+ case playing_failed:
+ eventlog("failed", playing->track, wstat(playing->wstat), (char *)0);
+ break;
+ default:
+ break;
+ }
+ queue_played(playing);
+ recent_write();
+ forget_player_pid(playing->id);
+ playing = 0;
+ if(ev) retry_play(ev, config->gap);
+}
+
+/* Called when a player terminates. */
+static int player_finished(ev_source *ev,
+ pid_t pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void *u) {
+ struct queue_entry *q = u;
+
+ D(("player_finished pid=%lu status=%#x",
+ (unsigned long)pid, (unsigned)status));
+ /* Record that this PID is dead. If we killed the track we might know this
+ * already, but also it might have exited or crashed. Either way we don't
+ * want to end up signalling it. */
+ if(pid == find_player_pid(q->id))
+ forget_player_pid(q->id);
+ switch(q->state) {
+ case playing_unplayed:
+ case playing_random:
+ /* If this was an SM_PREPARE track then either it failed or we deliberately
+ * stopped it because it was removed from the queue or moved down it. So
+ * leave it state alone for future use. */
+ break;
+ default:
+ /* We actually started playing this track. */
+ if(status) {
+ if(q->state != playing_scratched)
+ q->state = playing_failed;
+ } else
+ q->state = playing_ok;
+ break;
+ }
+ /* Regardless we always report and record the status and do cleanup for
+ * prefork calls. */
+ if(status)
+ error(0, "player for %s %s", q->track, wstat(status));
+ if(q->type & DISORDER_PLAYER_PREFORK)
+ play_cleanup(q->pl, q->data);
+ q->wstat = status;
+ /* If this actually was the current track, and does not use the speaker
+ * process, then it must have finished. For raw-output players we will get a
+ * separate notification from the speaker process. */
+ if(q == playing
+ && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+ finished(ev);
+ return 0;
+}
+
+/* Find the player for Q */
+static int find_player(const struct queue_entry *q) {
+ int n;
+
+ for(n = 0; n < config->player.n; ++n)
+ if(fnmatch(config->player.s[n].s[0], q->track, 0) == 0)
+ break;
+ if(n >= config->player.n)
+ return -1;
+ else
+ return n;
+}
+
+/* Return values from start() */
+#define START_OK 0 /* Succeeded. */
+#define START_HARDFAIL 1 /* Track is broken. */
+#define START_SOFTFAIL 2 /* Track OK, system (temporarily?) broken */
+
+/* Play or prepare Q */
+static int start(ev_source *ev,
+ struct queue_entry *q,
+ int smop) {
+ int n, lfd;
+ const char *p;
+ int sp[2];
+ struct speaker_message sm;
+ char buffer[64];
+ int optc;
+ ao_sample_format format;
+ ao_device *device;
+ int retries;
+ struct timespec ts;
+ const char *waitdevice = 0;
+ const char *const *optv;
+ pid_t pid;
+
+ memset(&sm, 0, sizeof sm);
+ if(find_player_pid(q->id) > 0) {
+ if(smop == SM_PREPARE) return START_OK;
+ /* We have already sent an SM_PREPARE for this track so we just need to
+ * tell the speaker process to start actually playing the queued up audio
+ * data */
+ strcpy(sm.id, q->id);
+ sm.type = SM_PLAY;
+ speaker_send(speaker_fd, &sm, -1);
+ return START_OK;
+ }
+ /* Find the player plugin. */
+ if((n = find_player(q)) < 0) return START_HARDFAIL;
+ if(!(q->pl = open_plugin(config->player.s[n].s[1], 0)))
+ return START_HARDFAIL;
+ q->type = play_get_type(q->pl);
+ /* Can't prepare non-raw tracks. */
+ if(smop == SM_PREPARE
+ && (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+ return START_OK;
+ /* Call the prefork function. */
+ p = trackdb_rawpath(q->track);
+ if(q->type & DISORDER_PLAYER_PREFORK)
+ if(!(q->data = play_prefork(q->pl, p))) {
+ error(0, "prefork function for %s failed", q->track);
+ return START_HARDFAIL;
+ }
+ /* Use the second arg as the tag if available (it's probably a command name),
+ * otherwise the module name. */
+ lfd = logfd(ev, (config->player.s[n].s[2]
+ ? config->player.s[n].s[2] : config->player.s[n].s[1]));
+ optc = config->player.s[n].n - 2;
+ optv = (void *)&config->player.s[n].s[2];
+ while(optc > 0 && optv[0][0] == '-') {
+ if(!strcmp(optv[0], "--")) {
+ ++optv;
+ --optc;
+ break;
+ }
+ if(!strcmp(optv[0], "--wait-for-device")
+ || !strncmp(optv[0], "--wait-for-device=", 18)) {
+ if((waitdevice = strchr(optv[0], '='))) {
+ ++waitdevice;
+ } else
+ waitdevice = ""; /* use default */
+ ++optv;
+ --optc;
+ } else {
+ error(0, "unknown option %s", optv[0]);
+ return START_HARDFAIL;
+ }
+ }
+ switch(pid = fork()) {
+ case 0: /* child */
+ exitfn = _exit;
+ ev_signal_atfork(ev);
+ signal(SIGPIPE, SIG_DFL);
+ xdup2(lfd, 1);
+ xdup2(lfd, 2);
+ xclose(lfd); /* tidy up */
+ setpgid(0, 0);
+ if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+ /* Raw format players write down a pipe (in fact a socket) to
+ * the speaker process. */
+ sm.type = smop;
+ strcpy(sm.id, q->id);
+ if(socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0)
+ fatal(errno, "error calling socketpair");
+ xshutdown(sp[0], SHUT_WR);
+ xshutdown(sp[1], SHUT_RD);
+ speaker_send(speaker_fd, &sm, sp[0]);
+ /* Pass the file descriptor to the driver in an environment
+ * variable. */
+ snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", sp[1]);
+ if(putenv(buffer) < 0)
+ fatal(errno, "error calling putenv");
+ xclose(sp[0]);
+ }
+ if(waitdevice) {
+ ao_initialize();
+ if(*waitdevice) {
+ n = ao_driver_id(waitdevice);
+ if(n == -1)
+ fatal(0, "invalid libao driver: %s", optv[0]);
+ } else
+ n = ao_default_driver_id();
+ /* Make up a format. */
+ memset(&format, 0, sizeof format);
+ format.bits = 8;
+ format.rate = 44100;
+ format.channels = 1;
+ format.byte_format = AO_FMT_NATIVE;
+ retries = 20;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 100000000; /* 0.1s */
+ while((device = ao_open_live(n, &format, 0)) == 0 && retries-- > 0)
+ nanosleep(&ts, 0);
+ if(device)
+ ao_close(device);
+ }
+ play_track(q->pl,
+ optv, optc,
+ p,
+ q->track);
+ _exit(0);
+ case -1: /* error */
+ error(errno, "error calling fork");
+ if(q->type & DISORDER_PLAYER_PREFORK)
+ play_cleanup(q->pl, q->data); /* else would leak */
+ xclose(lfd);
+ return START_SOFTFAIL;
+ }
+ store_player_pid(q->id, pid);
+ xclose(lfd);
+ setpgid(pid, pid);
+ ev_child(ev, pid, 0, player_finished, q);
+ D(("player subprocess ID %lu", (unsigned long)pid));
+ return START_OK;
+}
+
+int prepare(ev_source *ev,
+ struct queue_entry *q) {
+ int n;
+
+ /* Find the player plugin */
+ if(find_player_pid(q->id) > 0) return 0; /* Already going. */
+ if((n = find_player(q)) < 0) return -1; /* No player */
+ q->pl = open_plugin(config->player.s[n].s[1], 0); /* No player */
+ q->type = play_get_type(q->pl);
+ if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+ return 0; /* Not a raw player */
+ return start(ev, q, SM_PREPARE); /* Prepare it */
+}
+
+void abandon(ev_source attribute((unused)) *ev,
+ struct queue_entry *q) {
+ struct speaker_message sm;
+ pid_t pid = find_player_pid(q->id);
+
+ if(pid < 0) return; /* Not prepared. */
+ if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
+ return; /* Not a raw player. */
+ /* Terminate the player. */
+ kill(-pid, config->signal);
+ forget_player_pid(q->id);
+ /* Cancel the track. */
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_CANCEL;
+ strcpy(sm.id, q->id);
+ speaker_send(speaker_fd, &sm, -1);
+}
+
+int add_random_track(void) {
+ struct queue_entry *q;
+ const char *p;
+
+ /* If random play is not enabled then do nothing. */
+ if(shutting_down || !random_is_enabled())
+ return 0;
+ /* If there is already a random track, do nothing. */
+ for(q = qhead.next; q != &qhead; q = q->next)
+ if(q->state == playing_random)
+ return 0;
+ /* Try to pick a random track */
+ if(!(p = trackdb_random(16)))
+ return -1;
+ /* Add it to the end of the queue. */
+ q = queue_add(p, 0, WHERE_END);
+ q->state = playing_random;
+ /* Commit the queue */
+ queue_write();
+ D(("picked %p (%s) at random", (void *)q, q->track));
+ return 0;
+}
+
+/* try to play a track */
+void play(ev_source *ev) {
+ struct queue_entry *q;
+ int random_enabled = random_is_enabled();
+
+ D(("play playing=%p", (void *)playing));
+ if(shutting_down || playing || !playing_is_enabled()) return;
+ /* If the queue is empty then add a random track. */
+ if(qhead.next == &qhead) {
+ if(!random_enabled)
+ return;
+ if(add_random_track()) {
+ /* On error, try again in 10s. */
+ retry_play(ev, 10);
+ return;
+ }
+ /* Now there must be at least one track in the queue. */
+ }
+ q = qhead.next;
+ /* If random play is disabled but the track is a random one then don't play
+ * it. play() will be called again when random play is re-enabled. */
+ if(!random_enabled && q->state == playing_random)
+ return;
+ D(("taken %p (%s) from queue", (void *)q, q->track));
+ /* Try to start playing. */
+ switch(start(ev, q, SM_PLAY)) {
+ case START_HARDFAIL:
+ if(q == qhead.next) {
+ queue_remove(q, 0); /* Abandon this track. */
+ queue_played(q);
+ recent_write();
+ }
+ if(qhead.next == &qhead)
+ /* Queue is empty, wait a bit before trying something else (so we don't
+ * sit there looping madly in the presence of persistent problem). Note
+ * that we might not reliably get a random track lookahead in this case,
+ * but if we get here then really there are bigger problems. */
+ retry_play(ev, 1);
+ else
+ /* More in queue, try again now. */
+ play(ev);
+ break;
+ case START_SOFTFAIL:
+ /* Try same track again in a bit. */
+ retry_play(ev, 10);
+ break;
+ case START_OK:
+ if(q == qhead.next) {
+ queue_remove(q, 0);
+ queue_write();
+ }
+ playing = q;
+ time(&playing->played);
+ playing->state = playing_started;
+ notify_play(playing->track, playing->submitter);
+ eventlog("playing", playing->track,
+ playing->submitter ? playing->submitter : (const char *)0,
+ (const char *)0);
+ /* Maybe add a random track. */
+ add_random_track();
+ /* If there is another track in the queue prepare it now. This could
+ * potentially be a just-added random track. */
+ if(qhead.next != &qhead)
+ prepare(ev, qhead.next);
+ break;
+ }
+}
+
+int playing_is_enabled(void) {
+ const char *s = trackdb_get_global("playing");
+
+ return !s || !strcmp(s, "yes");
+}
+
+void enable_playing(const char *who, ev_source *ev) {
+ trackdb_set_global("playing", "yes", who);
+ /* Add a random track if necessary. */
+ add_random_track();
+ play(ev);
+}
+
+void disable_playing(const char *who) {
+ trackdb_set_global("playing", "no", who);
+}
+
+int random_is_enabled(void) {
+ const char *s = trackdb_get_global("random-play");
+
+ return !s || !strcmp(s, "yes");
+}
+
+void enable_random(const char *who, ev_source *ev) {
+ trackdb_set_global("random-play", "yes", who);
+ add_random_track();
+ play(ev);
+}
+
+void disable_random(const char *who) {
+ trackdb_set_global("random-play", "no", who);
+}
+
+void scratch(const char *who, const char *id) {
+ struct queue_entry *q;
+ struct speaker_message sm;
+ pid_t pid;
+
+ D(("scratch playing=%p state=%d id=%s playing->id=%s",
+ (void *)playing,
+ playing ? playing->state : 0,
+ id ? id : "(none)",
+ playing ? playing->id : "(none)"));
+ if(playing
+ && (playing->state == playing_started
+ || playing->state == playing_paused)
+ && (!id
+ || !strcmp(id, playing->id))) {
+ playing->state = playing_scratched;
+ playing->scratched = who ? xstrdup(who) : 0;
+ if((pid = find_player_pid(playing->id)) > 0) {
+ D(("kill -%d %lu", config->signal, (unsigned long)pid));
+ kill(-pid, config->signal);
+ forget_player_pid(playing->id);
+ } else
+ error(0, "could not find PID for %s", playing->id);
+ if((playing->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_CANCEL;
+ strcpy(sm.id, playing->id);
+ speaker_send(speaker_fd, &sm, -1);
+ D(("sending SM_CANCEL for %s", playing->id));
+ }
+ /* put a scratch track onto the front of the queue (but don't
+ * bother if playing is disabled) */
+ if(playing_is_enabled() && config->scratch.n) {
+ int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+ q = queue_add(config->scratch.s[r], who, WHERE_START);
+ q->state = playing_isscratch;
+ }
+ notify_scratch(playing->track, playing->submitter, who,
+ time(0) - playing->played);
+ }
+}
+
+void quitting(ev_source *ev) {
+ struct queue_entry *q;
+ pid_t pid;
+
+ /* Don't start anything new */
+ shutting_down = 1;
+ /* Shut down the current player */
+ if(playing) {
+ if((pid = find_player_pid(playing->id)) > 0) {
+ kill(-pid, config->signal);
+ forget_player_pid(playing->id);
+ } else
+ error(0, "could not find PID for %s", playing->id);
+ playing->state = playing_quitting;
+ finished(0);
+ }
+ /* Zap any other players */
+ for(q = qhead.next; q != &qhead; q = q->next)
+ if((pid = find_player_pid(q->id)) > 0) {
+ D(("kill -%d %lu", config->signal, (unsigned long)pid));
+ kill(-pid, config->signal);
+ forget_player_pid(q->id);
+ } else
+ error(0, "could not find PID for %s", q->id);
+ /* Don't need the speaker any more */
+ ev_fd_cancel(ev, ev_read, speaker_fd);
+ xclose(speaker_fd);
+}
+
+int pause_playing(const char *who) {
+ struct speaker_message sm;
+ long played;
+
+ /* Can't pause if already paused or if nothing playing. */
+ if(!playing || paused) return 0;
+ switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+ case DISORDER_PLAYER_STANDALONE:
+ if(!(playing->type & DISORDER_PLAYER_PAUSES)) {
+ default:
+ error(0, "cannot pause because player is not powerful enough");
+ return -1;
+ }
+ if(play_pause(playing->pl, &played, playing->data)) {
+ error(0, "player indicates it cannot pause");
+ return -1;
+ }
+ time(&playing->lastpaused);
+ playing->uptopause = played;
+ playing->lastresumed = 0;
+ break;
+ case DISORDER_PLAYER_RAW:
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_PAUSE;
+ speaker_send(speaker_fd, &sm, -1);
+ break;
+ }
+ if(who) info("paused by %s", who);
+ notify_pause(playing->track, who);
+ paused = 1;
+ if(playing->state == playing_started)
+ playing->state = playing_paused;
+ eventlog("state", "pause", (char *)0);
+ return 0;
+}
+
+void resume_playing(const char *who) {
+ struct speaker_message sm;
+
+ if(!paused) return;
+ paused = 0;
+ if(!playing) return;
+ switch(playing->type & DISORDER_PLAYER_TYPEMASK) {
+ case DISORDER_PLAYER_STANDALONE:
+ if(!playing->type & DISORDER_PLAYER_PAUSES) {
+ default:
+ /* Shouldn't happen */
+ return;
+ }
+ play_resume(playing->pl, playing->data);
+ time(&playing->lastresumed);
+ break;
+ case DISORDER_PLAYER_RAW:
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_RESUME;
+ speaker_send(speaker_fd, &sm, -1);
+ break;
+ }
+ if(who) info("resumed by %s", who);
+ notify_resume(playing->track, who);
+ if(playing->state == playing_paused)
+ playing->state = playing_started;
+ eventlog("state", "resume", (char *)0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:17f4e83cce4cbaa60a122475379e63f1 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <db.h>
+#include <locale.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <pcre.h>
+#include <fnmatch.h>
+#include <sys/wait.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "plugin.h"
+#include "inputline.h"
+#include "charset.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "printf.h"
+#include "trackdb.h"
+#include "trackdb-int.h"
+
+static DB_TXN *global_tid;
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { "no-debug", no_argument, 0, 'D' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorder-rescan [OPTIONS] [PATH...]\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n"
+ "\n"
+ "Rescanner for DisOrder. Not intended to be run\n"
+ "directly.\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+ xprintf("disorder-rescan version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+static volatile sig_atomic_t signalled;
+
+static void signal_handler(int sig) {
+ if(sig == 0) _exit(-1); /* "Cannot happen" */
+ signalled = sig;
+}
+
+static int aborted(void) {
+ return signalled || getppid() == 1;
+}
+
+/* Exit if our parent has gone away or we have been told to stop. */
+static void checkabort(void) {
+ if(getppid() == 1) {
+ info("parent has terminated");
+ trackdb_abort_transaction(global_tid);
+ exit(0);
+ }
+ if(signalled) {
+ info("received signal %d", signalled);
+ trackdb_abort_transaction(global_tid);
+ exit(0);
+ }
+}
+
+/* rescan a collection */
+static void rescan_collection(const struct collection *c) {
+ pid_t pid, r;
+ int p[2], n, w;
+ FILE *fp = 0;
+ char *path, *track;
+ long ntracks = 0, nnew = 0;
+
+ checkabort();
+ info("rescanning %s with %s", c->root, c->module);
+ /* plugin runs in a subprocess */
+ xpipe(p);
+ if(!(pid = xfork())) {
+ exitfn = _exit;
+ xclose(p[0]);
+ xdup2(p[1], 1);
+ xclose(p[1]);
+ scan(c->module, c->root);
+ if(fflush(stdout) < 0)
+ fatal(errno, "error writing to scanner pipe");
+ _exit(0);
+ }
+ xclose(p[1]);
+ if(!(fp = fdopen(p[0], "r")))
+ fatal(errno, "error calling fdopen");
+ /* read tracks from the plugin */
+ while(!inputline("rescanner", fp, &path, 0)) {
+ checkabort();
+ /* actually we can cope relatively well within the server, but they'll go
+ * wrong in track listings */
+ if(strchr(path, '\n')) {
+ error(0, "cannot cope with tracks with newlines in the name");
+ continue;
+ }
+ if(!(track = any2utf8(c->encoding, path))) {
+ error(0, "cannot convert track path to UTF-8: %s", path);
+ continue;
+ }
+ D(("track %s", track));
+ /* only tracks with a known player are admitted */
+ for(n = 0; (n < config->player.n
+ && fnmatch(config->player.s[n].s[0], track, 0) != 0); ++n)
+ ;
+ if(n < config->player.n) {
+ nnew += !!trackdb_notice(track, path);
+ ++ntracks;
+ }
+ }
+ /* tidy up */
+ if(ferror(fp)) {
+ error(errno, "error reading from scanner pipe");
+ goto done;
+ }
+ xfclose(fp);
+ fp = 0;
+ while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+ ;
+ if(r < 0) fatal(errno, "error calling waitpid");
+ pid = 0;
+ if(w) {
+ error(0, "scanner subprocess: %s", wstat(w));
+ goto done;
+ }
+ info("rescanned %s, %ld tracks, %ld new", c->root, ntracks, nnew);
+done:
+ if(fp)
+ xfclose(fp);
+ if(pid)
+ while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+ ;
+}
+
+struct recheck_state {
+ const struct collection *c;
+ long nobsolete, nlength;
+};
+
+/* called for each non-alias track */
+static int recheck_callback(const char *track,
+ struct kvp *data,
+ void *u,
+ DB_TXN *tid) {
+ struct recheck_state *cs = u;
+ const struct collection *c = cs->c;
+ const char *path = kvp_get(data, "_path");
+ char buffer[20];
+ int err;
+ long n;
+
+ if(aborted()) return EINTR;
+ D(("rechecking %s", track));
+ /* see if the track has evaporated */
+ if(check(c->module, c->root, path) == 0) {
+ D(("obsoleting %s", track));
+ if((err = trackdb_obsolete(track, tid))) return err;
+ ++cs->nobsolete;
+ return 0;
+ }
+ /* make sure we know the length */
+ if(!kvp_get(data, "_length")) {
+ D(("recalculating length of %s", track));
+ n = tracklength(track, path);
+ if(n > 0) {
+ byte_snprintf(buffer, sizeof buffer, "%ld", n);
+ kvp_set(&data, "_length", buffer);
+ if((err = trackdb_putdata(trackdb_tracksdb, track, data, tid, 0)))
+ return err;
+ ++cs->nlength;
+ }
+ }
+ return 0;
+}
+
+/* recheck a collection */
+static void recheck_collection(const struct collection *c) {
+ struct recheck_state cs;
+
+ info("rechecking %s", c->root);
+ for(;;) {
+ checkabort();
+ global_tid = trackdb_begin_transaction();
+ memset(&cs, 0, sizeof cs);
+ cs.c = c;
+ if(trackdb_scan(c->root, recheck_callback, &cs, global_tid)) goto fail;
+ break;
+ fail:
+ /* Maybe we need to shut down */
+ checkabort();
+ /* Abort the transaction and try again in a bit. */
+ trackdb_abort_transaction(global_tid);
+ global_tid = 0;
+ /* Let anything else that is going on get out of the way. */
+ sleep(10);
+ checkabort();
+ info("resuming recheck of %s", c->root);
+ }
+ trackdb_commit_transaction(global_tid);
+ global_tid = 0;
+ info("rechecked %s, %ld obsoleted, %ld lengths calculated",
+ c->root, cs.nobsolete, cs.nlength);
+}
+
+/* rescan/recheck a collection by name */
+static void do_directory(const char *s,
+ void (*fn)(const struct collection *c)) {
+ int n;
+
+ for(n = 0; (n < config->collection.n
+ && strcmp(config->collection.s[n].root, s)); ++n)
+ ;
+ if(n < config->collection.n)
+ fn(&config->collection.s[n]);
+ else
+ error(0, "no collection has root '%s'", s);
+}
+
+/* rescan/recheck all collections */
+static void do_all(void (*fn)(const struct collection *c)) {
+ int n;
+
+ for(n = 0; n < config->collection.n; ++n)
+ fn(&config->collection.s[n]);
+}
+
+int main(int argc, char **argv) {
+ int n;
+ struct sigaction sa;
+
+ set_progname(argv);
+ mem_init(1);
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ case 'D': debugging = 0; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ /* If stderr is a TTY then log there, otherwise to syslog. */
+ if(!isatty(2)) {
+ openlog(progname, LOG_PID, LOG_DAEMON);
+ log_default = &log_syslog;
+ }
+ if(config_read()) fatal(0, "cannot read configuration");
+ xnice(config->nice_rescan);
+ sa.sa_handler = signal_handler;
+ sa.sa_flags = SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ xsigaction(SIGTERM, &sa, 0);
+ xsigaction(SIGINT, &sa, 0);
+ info("started");
+ trackdb_init(0);
+ trackdb_open();
+ if(optind == argc) {
+ do_all(rescan_collection);
+ do_all(recheck_collection);
+ }
+ else {
+ for(n = optind; n < argc; ++n)
+ do_directory(argv[n], rescan_collection);
+ for(n = optind; n < argc; ++n)
+ do_directory(argv[n], recheck_collection);
+ }
+ trackdb_close();
+ trackdb_deinit();
+ info("completed");
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:yBYnk5wX3mDtVI1MI3LlWg */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <gcrypt.h>
+#include <stddef.h>
+#include <time.h>
+#include <limits.h>
+#include <pcre.h>
+#include <netdb.h>
+#include <netinet/in.h>
+
+#include "event.h"
+#include "server.h"
+#include "syscalls.h"
+#include "queue.h"
+#include "play.h"
+#include "log.h"
+#include "mem.h"
+#include "state.h"
+#include "charset.h"
+#include "split.h"
+#include "configuration.h"
+#include "hex.h"
+#include "trackdb.h"
+#include "table.h"
+#include "kvp.h"
+#include "mixer.h"
+#include "sink.h"
+#include "authhash.h"
+#include "plugin.h"
+#include "printf.h"
+#include "trackname.h"
+#include "eventlog.h"
+#include "defs.h"
+#include "cache.h"
+
+#ifndef NONCE_SIZE
+# define NONCE_SIZE 16
+#endif
+
+int volume_left, volume_right; /* last known volume */
+
+struct listener {
+ const char *name;
+ int pf;
+};
+
+struct conn {
+ ev_reader *r;
+ ev_writer *w;
+ int fd;
+ unsigned tag;
+ char *who;
+ ev_source *ev;
+ unsigned char nonce[NONCE_SIZE];
+ ev_reader_callback *reader;
+ struct eventlog_output *lo;
+ const struct listener *l;
+};
+
+static int reader_callback(ev_source *ev,
+ ev_reader *reader,
+ int fd,
+ void *ptr,
+ size_t bytes,
+ int eof,
+ void *u);
+
+static const char *noyes[] = { "no", "yes" };
+
+static int writer_error(ev_source attribute((unused)) *ev,
+ int fd,
+ int errno_value,
+ void *u) {
+ struct conn *c = u;
+
+ D(("server writer_error %d %d", fd, errno_value));
+ if(errno_value == 0) {
+ /* writer is done */
+ c->w = 0;
+ if(c->r == 0) {
+ D(("server writer_error closes %d", fd));
+ xclose(fd); /* reader is done too, close */
+ } else {
+ D(("server writer_error shutdown %d SHUT_WR", fd));
+ xshutdown(fd, SHUT_WR); /* reader is not done yet */
+ }
+ } else {
+ if(errno_value != EPIPE)
+ error(errno_value, "S%x write error on socket", c->tag);
+ if(c->r)
+ ev_reader_cancel(c->r);
+ xclose(fd);
+ }
+ return 0;
+}
+
+static int reader_error(ev_source attribute((unused)) *ev,
+ int fd,
+ int errno_value,
+ void *u) {
+ struct conn *c = u;
+
+ D(("server reader_error %d %d", fd, errno_value));
+ error(errno, "S%x read error on socket", c->tag);
+ ev_writer_cancel(c->w);
+ xclose(fd);
+ return 0;
+}
+
+/* return true if we are talking to a trusted user */
+static int trusted(struct conn *c) {
+ int n;
+
+ for(n = 0; (n < config->trust.n
+ && strcmp(config->trust.s[n], c->who)); ++n)
+ ;
+ return n < config->trust.n;
+}
+
+static int c_disable(struct conn *c, char **vec, int nvec) {
+ if(nvec == 0)
+ disable_playing(c->who);
+ else if(nvec == 1 && !strcmp(vec[0], "now"))
+ disable_playing(c->who);
+ else {
+ sink_writes(ev_writer_sink(c->w), "550 invalid argument\n");
+ return 1; /* completed */
+ }
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1; /* completed */
+}
+
+static int c_enable(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ enable_playing(c->who, c->ev);
+ /* Enable implicitly unpauses if there is nothing playing */
+ if(paused && !playing) resume_playing(c->who);
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1; /* completed */
+}
+
+static int c_enabled(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[playing_is_enabled()]);
+ return 1; /* completed */
+}
+
+static int c_play(struct conn *c, char **vec,
+ int attribute((unused)) nvec) {
+ const char *track;
+ struct queue_entry *q;
+
+ if(!trackdb_exists(vec[0])) {
+ sink_writes(ev_writer_sink(c->w), "550 track is not in database\n");
+ return 1;
+ }
+ if(!(track = trackdb_resolve(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+ return 1;
+ }
+ q = queue_add(track, c->who, WHERE_BEFORE_RANDOM);
+ queue_write();
+ /* If we added the first track, and something is playing, then prepare the
+ * new track. If nothing is playing then we don't bother as it wouldn't gain
+ * anything. */
+ if(q == qhead.next && playing)
+ prepare(c->ev, q);
+ sink_writes(ev_writer_sink(c->w), "250 queued\n");
+ /* If the queue was empty but we are for some reason paused then
+ * unpause. */
+ if(!playing) resume_playing(0);
+ play(c->ev);
+ return 1; /* completed */
+}
+
+static int c_remove(struct conn *c, char **vec,
+ int attribute((unused)) nvec) {
+ struct queue_entry *q;
+
+ if(!(q = queue_find(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+ return 1;
+ }
+ if(config->restrictions & RESTRICT_REMOVE) {
+ /* can only remove tracks that you submitted */
+ if(!q->submitter || strcmp(q->submitter, c->who)) {
+ sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+ return 1;
+ }
+ }
+ queue_remove(q, c->who);
+ /* De-prepare the track. */
+ abandon(c->ev, q);
+ /* If we removed the random track then add another one. */
+ if(q->state == playing_random)
+ add_random_track();
+ /* Prepare whatever the next head track is. */
+ if(qhead.next != &qhead)
+ prepare(c->ev, qhead.next);
+ queue_write();
+ sink_writes(ev_writer_sink(c->w), "250 removed\n");
+ return 1; /* completed */
+}
+
+static int c_scratch(struct conn *c,
+ char **vec,
+ int nvec) {
+ if(!playing) {
+ sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+ return 1; /* completed */
+ }
+ if(config->restrictions & RESTRICT_SCRATCH) {
+ /* can only scratch tracks you submitted and randomly selected ones */
+ if(playing->submitter && strcmp(playing->submitter, c->who)) {
+ sink_writes(ev_writer_sink(c->w), "550 you didn't submit that track!\n");
+ return 1;
+ }
+ }
+ scratch(c->who, nvec == 1 ? vec[0] : 0);
+ /* If you scratch an unpaused track then it is automatically unpaused */
+ resume_playing(0);
+ sink_writes(ev_writer_sink(c->w), "250 scratched\n");
+ return 1; /* completed */
+}
+
+static int c_pause(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(!playing) {
+ sink_writes(ev_writer_sink(c->w), "250 nothing is playing\n");
+ return 1; /* completed */
+ }
+ if(paused) {
+ sink_writes(ev_writer_sink(c->w), "250 already paused\n");
+ return 1; /* completed */
+ }
+ if(pause_playing(c->who) < 0)
+ sink_writes(ev_writer_sink(c->w), "550 cannot pause this track\n");
+ else
+ sink_writes(ev_writer_sink(c->w), "250 paused\n");
+ return 1;
+}
+
+static int c_resume(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(!paused) {
+ sink_writes(ev_writer_sink(c->w), "250 not paused\n");
+ return 1; /* completed */
+ }
+ resume_playing(c->who);
+ sink_writes(ev_writer_sink(c->w), "250 paused\n");
+ return 1;
+}
+
+static int c_shutdown(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ info("S%x shut down by %s", c->tag, c->who);
+ sink_writes(ev_writer_sink(c->w), "250 shutting down\n");
+ ev_writer_flush(c->w);
+ quit(c->ev);
+}
+
+static int c_reconfigure(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ info("S%x reconfigure by %s", c->tag, c->who);
+ if(reconfigure(c->ev, 1))
+ sink_writes(ev_writer_sink(c->w), "550 error reading new config\n");
+ else
+ sink_writes(ev_writer_sink(c->w), "250 installed new config\n");
+ return 1; /* completed */
+}
+
+static int c_rescan(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ info("S%x rescan by %s", c->tag, c->who);
+ trackdb_rescan(c->ev);
+ sink_writes(ev_writer_sink(c->w), "250 initiated rescan\n");
+ return 1; /* completed */
+}
+
+static int c_version(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ /* VERSION had better only use the basic character set */
+ sink_printf(ev_writer_sink(c->w), "251 %s\n", disorder_version_string);
+ return 1; /* completed */
+}
+
+static int c_playing(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(playing) {
+ queue_fix_sofar(playing);
+ playing->expected = 0;
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", queue_marshall(playing));
+ } else
+ sink_printf(ev_writer_sink(c->w), "259 nothing playing\n");
+ return 1; /* completed */
+}
+
+static int c_become(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ c->who = vec[0];
+ sink_writes(ev_writer_sink(c->w), "230 OK\n");
+ return 1;
+}
+
+static int c_user(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int n;
+ const char *res;
+ union {
+ struct sockaddr sa;
+ struct sockaddr_in in;
+ struct sockaddr_in6 in6;
+ } u;
+ socklen_t l;
+ char host[1024];
+
+ if(c->who) {
+ sink_writes(ev_writer_sink(c->w), "530 already authenticated\n");
+ return 1;
+ }
+ /* get connection data */
+ l = sizeof u;
+ if(getpeername(c->fd, &u.sa, &l) < 0) {
+ error(errno, "S%x error calling getpeername", c->tag);
+ sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+ return 1;
+ }
+ if(c->l->pf != PF_UNIX) {
+ if((n = getnameinfo(&u.sa, l,
+ host, sizeof host, 0, 0, NI_NUMERICHOST))) {
+ error(0, "S%x error calling getnameinfo: %s", c->tag, gai_strerror(n));
+ sink_writes(ev_writer_sink(c->w), "530 authentication failure\n");
+ return 1;
+ }
+ }
+ /* find the user */
+ for(n = 0; n < config->allow.n
+ && strcmp(config->allow.s[n].s[0], vec[0]); ++n)
+ ;
+ /* if it's a real user check whether the response is right */
+ if(n < config->allow.n) {
+ res = authhash(c->nonce, sizeof c->nonce, config->allow.s[n].s[1]);
+ if(res && !strcmp(res, vec[1])) {
+ c->who = vec[0];
+ /* currently we only bother logging remote connections */
+ if(c->l->pf != PF_UNIX)
+ info("S%x %s connected from %s", c->tag, vec[0], host);
+ sink_writes(ev_writer_sink(c->w), "230 OK\n");
+ return 1;
+ }
+ }
+ /* oops, response was wrong */
+ if(c->l->pf != PF_UNIX)
+ info("S%x authentication failure for %s from %s", c->tag, vec[0], host);
+ else
+ info("S%x authentication failure for %s", c->tag, vec[0]);
+ sink_writes(ev_writer_sink(c->w), "530 authentication failed\n");
+ return 1;
+}
+
+static int c_recent(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ const struct queue_entry *q;
+
+ sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+ for(q = phead.next; q != &phead; q = q->next)
+ sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1; /* completed */
+}
+
+static int c_queue(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ struct queue_entry *q;
+ time_t when = 0;
+ const char *l;
+ long length;
+
+ sink_writes(ev_writer_sink(c->w), "253 Tracks follow\n");
+ if(playing_is_enabled() && !paused) {
+ if(playing) {
+ queue_fix_sofar(playing);
+ if((l = trackdb_get(playing->track, "_length"))
+ && (length = atol(l))) {
+ time(&when);
+ when += length - playing->sofar + config->gap;
+ }
+ } else
+ /* Nothing is playing but playing is enabled, so whatever is
+ * first in the queue can be expected to start immediately. */
+ time(&when);
+ }
+ for(q = qhead.next; q != &qhead; q = q->next) {
+ /* fill in estimated start time */
+ q->expected = when;
+ sink_printf(ev_writer_sink(c->w), " %s\n", queue_marshall(q));
+ /* update for next track */
+ if(when) {
+ if((l = trackdb_get(q->track, "_length"))
+ && (length = atol(l)))
+ when += length + config->gap;
+ else
+ when = 0;
+ }
+ }
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1; /* completed */
+}
+
+static int output_list(struct conn *c, char **vec) {
+ while(*vec)
+ sink_printf(ev_writer_sink(c->w), "%s\n", *vec++);
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1;
+}
+
+static int files_dirs(struct conn *c,
+ char **vec,
+ int nvec,
+ enum trackdb_listable what) {
+ const char *dir, *re, *errstr;
+ int erroffset;
+ pcre *rec;
+ char **fvec, *key;
+
+ switch(nvec) {
+ case 0: dir = 0; re = 0; break;
+ case 1: dir = vec[0]; re = 0; break;
+ case 2: dir = vec[0]; re = vec[1]; break;
+ default: abort();
+ }
+ /* A bit of a bodge to make sure the args don't trample on cache keys */
+ if(dir && strchr(dir, '\n')) {
+ sink_writes(ev_writer_sink(c->w), "550 invalid directory name\n");
+ return 1;
+ }
+ if(re && strchr(re, '\n')) {
+ sink_writes(ev_writer_sink(c->w), "550 invalid regexp\n");
+ return 1;
+ }
+ /* We bother eliminating "" because the web interface is relatively
+ * likely to send it */
+ if(re && *re) {
+ byte_xasprintf(&key, "%d\n%s\n%s", (int)what, dir ? dir : "", re);
+ fvec = (char **)cache_get(&cache_files_type, key);
+ if(fvec) {
+ /* Got a cache hit, don't store the answer in the cache */
+ key = 0;
+ ++cache_files_hits;
+ rec = 0; /* quieten compiler */
+ } else {
+ /* Cache miss, we'll do the lookup and key != 0 so we'll store the answer
+ * in the cache. */
+ if(!(rec = pcre_compile(re, PCRE_CASELESS|PCRE_UTF8,
+ &errstr, &erroffset, 0))) {
+ sink_printf(ev_writer_sink(c->w), "550 Error compiling regexp: %s\n",
+ errstr);
+ return 1;
+ }
+ /* It only counts as a miss if the regexp was valid. */
+ ++cache_files_misses;
+ }
+ } else {
+ /* No regexp, don't bother caching the result */
+ rec = 0;
+ key = 0;
+ fvec = 0;
+ }
+ if(!fvec) {
+ /* No cache hit (either because a miss, or because we did not look) so do
+ * the lookup */
+ if(dir && *dir)
+ fvec = trackdb_list(dir, 0, what, rec);
+ else
+ fvec = trackdb_list(0, 0, what, rec);
+ }
+ if(key)
+ /* Put the answer in the cache */
+ cache_put(&cache_files_type, key, fvec);
+ sink_writes(ev_writer_sink(c->w), "253 Listing follow\n");
+ return output_list(c, fvec);
+}
+
+static int c_files(struct conn *c,
+ char **vec,
+ int nvec) {
+ return files_dirs(c, vec, nvec, trackdb_files);
+}
+
+static int c_dirs(struct conn *c,
+ char **vec,
+ int nvec) {
+ return files_dirs(c, vec, nvec, trackdb_directories);
+}
+
+static int c_allfiles(struct conn *c,
+ char **vec,
+ int nvec) {
+ return files_dirs(c, vec, nvec, trackdb_directories|trackdb_files);
+}
+
+static int c_get(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ const char *v;
+
+ if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+ else
+ sink_writes(ev_writer_sink(c->w), "550 not found\n");
+ return 1;
+}
+
+static int c_length(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ const char *track, *v;
+
+ if(!(track = trackdb_resolve(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+ return 1;
+ }
+ if((v = trackdb_get(track, "_length")))
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
+ else
+ sink_writes(ev_writer_sink(c->w), "550 not found\n");
+ return 1;
+}
+
+static int c_set(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ if(vec[1][0] != '_' && !trackdb_set(vec[0], vec[1], vec[2]))
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ else
+ sink_writes(ev_writer_sink(c->w), "550 not found\n");
+ return 1;
+}
+
+static int c_prefs(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct kvp *k;
+
+ k = trackdb_get_all(vec[0]);
+ sink_writes(ev_writer_sink(c->w), "253 prefs follow\n");
+ for(; k; k = k->next)
+ if(k->name[0] != '_') /* omit internal values */
+ sink_printf(ev_writer_sink(c->w),
+ " %s %s\n", quoteutf8(k->name), quoteutf8(k->value));
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1;
+}
+
+static int c_exists(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[trackdb_exists(vec[0])]);
+ return 1;
+}
+
+static void search_parse_error(const char *msg, void *u) {
+ *(const char **)u = msg;
+}
+
+static int c_search(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char **terms, **results;
+ int nterms, nresults, n;
+ const char *e = "unknown error";
+
+ /* This is a bit of a bodge. Initially it's there to make the eclient
+ * interface a bit more convenient to add searching to, but it has the more
+ * compelling advantage that if everything uses it, then interpretation of
+ * user-supplied search strings will be the same everywhere. */
+ if(!(terms = split(vec[0], &nterms, SPLIT_QUOTES, search_parse_error, &e))) {
+ sink_printf(ev_writer_sink(c->w), "550 %s\n", e);
+ } else {
+ results = trackdb_search(terms, nterms, &nresults);
+ sink_printf(ev_writer_sink(c->w), "253 %d matches\n", nresults);
+ for(n = 0; n < nresults; ++n)
+ sink_printf(ev_writer_sink(c->w), "%s\n", results[n]);
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ }
+ return 1;
+}
+
+static int c_random_enable(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ enable_random(c->who, c->ev);
+ /* Enable implicitly unpauses if there is nothing playing */
+ if(paused && !playing) resume_playing(c->who);
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1; /* completed */
+}
+
+static int c_random_disable(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ disable_random(c->who);
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1; /* completed */
+}
+
+static int c_random_enabled(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[random_is_enabled()]);
+ return 1; /* completed */
+}
+
+static int c_stats(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ char **v;
+ int nv, n;
+
+ v = trackdb_stats(&nv);
+ sink_printf(ev_writer_sink(c->w), "253 stats\n");
+ for(n = 0; n < nv; ++n) {
+ if(v[n][0] == '.')
+ sink_writes(ev_writer_sink(c->w), ".");
+ sink_printf(ev_writer_sink(c->w), "%s\n", v[n]);
+ }
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1;
+}
+
+static int c_volume(struct conn *c,
+ char **vec,
+ int nvec) {
+ int l, r, set;
+ char lb[32], rb[32];
+
+ switch(nvec) {
+ case 0:
+ set = 0;
+ break;
+ case 1:
+ l = r = atoi(vec[0]);
+ set = 1;
+ break;
+ case 2:
+ l = atoi(vec[0]);
+ r = atoi(vec[1]);
+ set = 1;
+ break;
+ default:
+ abort();
+ }
+ if(mixer_control(&l, &r, set))
+ sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n");
+ else {
+ sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
+ if(l != volume_left || r != volume_right) {
+ volume_left = l;
+ volume_right = r;
+ snprintf(lb, sizeof lb, "%d", l);
+ snprintf(rb, sizeof rb, "%d", r);
+ eventlog("volume", lb, rb, (char *)0);
+ }
+ }
+ return 1;
+}
+
+/* we are logging, and some data is available to read */
+static int logging_reader_callback(ev_source *ev,
+ ev_reader *reader,
+ int fd,
+ void *ptr,
+ size_t bytes,
+ int eof,
+ void *u) {
+ struct conn *c = u;
+
+ /* don't log to this conn any more */
+ eventlog_remove(c->lo);
+ /* terminate the log output */
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ /* restore the reader callback */
+ c->reader = reader_callback;
+ /* ...and exit via it */
+ return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+static void logclient(const char *msg, void *user) {
+ struct conn *c = user;
+
+ sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n",
+ (uintmax_t)time(0), msg);
+}
+
+static int c_log(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ time_t now;
+
+ sink_writes(ev_writer_sink(c->w), "254 OK\n");
+ /* pump out initial state */
+ time(&now);
+ sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+ (uintmax_t)now,
+ playing_is_enabled() ? "enable_play" : "disable_play");
+ sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+ (uintmax_t)now,
+ random_is_enabled() ? "enable_random" : "disable_random");
+ sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n",
+ (uintmax_t)now,
+ paused ? "pause" : "resume");
+ c->lo = xmalloc(sizeof *c->lo);
+ c->lo->fn = logclient;
+ c->lo->user = c;
+ eventlog_add(c->lo);
+ c->reader = logging_reader_callback;
+ return 0;
+}
+
+static void post_move_cleanup(void) {
+ struct queue_entry *q;
+
+ /* If we have caused the random track to not be at the end then we make it no
+ * longer be random. */
+ for(q = qhead.next; q != &qhead; q = q->next)
+ if(q->state == playing_random && q->next != &qhead)
+ q->state = playing_unplayed;
+ /* That might mean we need to add a new random track. */
+ add_random_track();
+ queue_write();
+}
+
+static int c_move(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct queue_entry *q;
+ int n;
+
+ if(config->restrictions & RESTRICT_MOVE) {
+ if(!trusted(c)) {
+ sink_writes(ev_writer_sink(c->w),
+ "550 only trusted users can move tracks\n");
+ return 1;
+ }
+ }
+ if(!(q = queue_find(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+ return 1;
+ }
+ n = queue_move(q, atoi(vec[1]), c->who);
+ post_move_cleanup();
+ sink_printf(ev_writer_sink(c->w), "252 %d\n", n);
+ /* If we've moved to the head of the queue then prepare the track. */
+ if(q == qhead.next)
+ prepare(c->ev, q);
+ return 1;
+}
+
+static int c_moveafter(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct queue_entry *q, **qs;
+ int n;
+
+ if(config->restrictions & RESTRICT_MOVE) {
+ if(!trusted(c)) {
+ sink_writes(ev_writer_sink(c->w),
+ "550 only trusted users can move tracks\n");
+ return 1;
+ }
+ }
+ if(vec[0][0]) {
+ if(!(q = queue_find(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+ return 1;
+ }
+ } else
+ q = 0;
+ ++vec;
+ --nvec;
+ qs = xcalloc(nvec, sizeof *qs);
+ for(n = 0; n < nvec; ++n)
+ if(!(qs[n] = queue_find(vec[n]))) {
+ sink_writes(ev_writer_sink(c->w), "550 no such track on the queue\n");
+ return 1;
+ }
+ queue_moveafter(q, nvec, qs, c->who);
+ post_move_cleanup();
+ sink_printf(ev_writer_sink(c->w), "250 Moved tracks\n");
+ /* If we've moved to the head of the queue then prepare the track. */
+ if(q == qhead.next)
+ prepare(c->ev, q);
+ return 1;
+}
+
+static int c_part(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n",
+ trackdb_getpart(vec[0], vec[1], vec[2]));
+ return 1;
+}
+
+static int c_resolve(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ const char *track;
+
+ if(!(track = trackdb_resolve(vec[0]))) {
+ sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+ return 1;
+ }
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", track);
+ return 1;
+}
+
+static int c_tags(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ char **tags = trackdb_alltags();
+
+ sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n");
+ while(*tags) {
+ sink_printf(ev_writer_sink(c->w), "%s%s\n",
+ **tags == '.' ? "." : "", *tags);
+ ++tags;
+ }
+ sink_writes(ev_writer_sink(c->w), ".\n");
+ return 1; /* completed */
+
+}
+
+static int c_set_global(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ trackdb_set_global(vec[0], vec[1], c->who);
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+}
+
+static int c_get_global(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ const char *s = trackdb_get_global(vec[0]);
+
+ if(s)
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", s);
+ else
+ sink_writes(ev_writer_sink(c->w), "550 not found\n");
+ return 1;
+}
+
+#define C_AUTH 0001 /* must be authenticated */
+#define C_TRUSTED 0002 /* must be trusted user */
+
+static const struct command {
+ const char *name;
+ int minargs, maxargs;
+ int (*fn)(struct conn *, char **, int);
+ unsigned flags;
+} commands[] = {
+ { "allfiles", 0, 2, c_allfiles, C_AUTH },
+ { "become", 1, 1, c_become, C_AUTH|C_TRUSTED },
+ { "dirs", 0, 2, c_dirs, C_AUTH },
+ { "disable", 0, 1, c_disable, C_AUTH },
+ { "enable", 0, 0, c_enable, C_AUTH },
+ { "enabled", 0, 0, c_enabled, C_AUTH },
+ { "exists", 1, 1, c_exists, C_AUTH },
+ { "files", 0, 2, c_files, C_AUTH },
+ { "get", 2, 2, c_get, C_AUTH },
+ { "get-global", 1, 1, c_get_global, C_AUTH },
+ { "length", 1, 1, c_length, C_AUTH },
+ { "log", 0, 0, c_log, C_AUTH },
+ { "move", 2, 2, c_move, C_AUTH },
+ { "moveafter", 1, INT_MAX, c_moveafter, C_AUTH },
+ { "part", 3, 3, c_part, C_AUTH },
+ { "pause", 0, 0, c_pause, C_AUTH },
+ { "play", 1, 1, c_play, C_AUTH },
+ { "playing", 0, 0, c_playing, C_AUTH },
+ { "prefs", 1, 1, c_prefs, C_AUTH },
+ { "queue", 0, 0, c_queue, C_AUTH },
+ { "random-disable", 0, 0, c_random_disable, C_AUTH },
+ { "random-enable", 0, 0, c_random_enable, C_AUTH },
+ { "random-enabled", 0, 0, c_random_enabled, C_AUTH },
+ { "recent", 0, 0, c_recent, C_AUTH },
+ { "reconfigure", 0, 0, c_reconfigure, C_AUTH|C_TRUSTED },
+ { "remove", 1, 1, c_remove, C_AUTH },
+ { "rescan", 0, 0, c_rescan, C_AUTH|C_TRUSTED },
+ { "resolve", 1, 1, c_resolve, C_AUTH },
+ { "resume", 0, 0, c_resume, C_AUTH },
+ { "scratch", 0, 1, c_scratch, C_AUTH },
+ { "search", 1, 1, c_search, C_AUTH },
+ { "set", 3, 3, c_set, C_AUTH, },
+ { "set-global", 2, 2, c_set_global, C_AUTH },
+ { "shutdown", 0, 0, c_shutdown, C_AUTH|C_TRUSTED },
+ { "stats", 0, 0, c_stats, C_AUTH },
+ { "tags", 0, 0, c_tags, C_AUTH },
+ { "unset", 2, 2, c_set, C_AUTH },
+ { "unset-global", 1, 1, c_set_global, C_AUTH },
+ { "user", 2, 2, c_user, 0 },
+ { "version", 0, 0, c_version, C_AUTH },
+ { "volume", 0, 2, c_volume, C_AUTH }
+};
+
+static void command_error(const char *msg, void *u) {
+ struct conn *c = u;
+
+ sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg);
+}
+
+/* process a command. Return 1 if complete, 0 if incomplete. */
+static int command(struct conn *c, char *line) {
+ char **vec;
+ int nvec, n;
+
+ D(("server command %s", line));
+ if(!(vec = split(line, &nvec, SPLIT_QUOTES, command_error, c))) {
+ sink_writes(ev_writer_sink(c->w), "500 cannot parse command\n");
+ return 1;
+ }
+ if(nvec == 0) {
+ sink_writes(ev_writer_sink(c->w), "500 do what?\n");
+ return 1;
+ }
+ if((n = TABLE_FIND(commands, struct command, name, vec[0])) < 0)
+ sink_writes(ev_writer_sink(c->w), "500 unknown command\n");
+ else {
+ if((commands[n].flags & C_AUTH) && !c->who) {
+ sink_writes(ev_writer_sink(c->w), "530 not authenticated\n");
+ return 1;
+ }
+ if((commands[n].flags & C_TRUSTED) && !trusted(c)) {
+ sink_writes(ev_writer_sink(c->w), "530 insufficient privilege\n");
+ return 1;
+ }
+ ++vec;
+ --nvec;
+ if(nvec < commands[n].minargs) {
+ sink_writes(ev_writer_sink(c->w), "500 missing argument(s)\n");
+ return 1;
+ }
+ if(nvec > commands[n].maxargs) {
+ sink_writes(ev_writer_sink(c->w), "500 too many arguments\n");
+ return 1;
+ }
+ return commands[n].fn(c, vec, nvec);
+ }
+ return 1; /* completed */
+}
+
+/* redirect to the right reader callback for our current state */
+static int redirect_reader_callback(ev_source *ev,
+ ev_reader *reader,
+ int fd,
+ void *ptr,
+ size_t bytes,
+ int eof,
+ void *u) {
+ struct conn *c = u;
+
+ return c->reader(ev, reader, fd, ptr, bytes, eof, u);
+}
+
+/* the main command reader */
+static int reader_callback(ev_source attribute((unused)) *ev,
+ ev_reader *reader,
+ int attribute((unused)) fd,
+ void *ptr,
+ size_t bytes,
+ int eof,
+ void *u) {
+ struct conn *c = u;
+ char *eol;
+ int complete;
+
+ D(("server reader_callback"));
+ while((eol = memchr(ptr, '\n', bytes))) {
+ *eol++ = 0;
+ ev_reader_consume(reader, eol - (char *)ptr);
+ complete = command(c, ptr);
+ bytes -= (eol - (char *)ptr);
+ ptr = eol;
+ if(!complete) {
+ /* the command had better have set a new reader callback */
+ if(bytes || eof)
+ /* there are further bytes to read, or we are at eof; arrange for the
+ * command's reader callback to handle them */
+ return ev_reader_incomplete(reader);
+ /* nothing's going on right now */
+ return 0;
+ }
+ /* command completed, we can go around and handle the next one */
+ }
+ if(eof) {
+ if(bytes)
+ error(0, "S%x unterminated line", c->tag);
+ c->r = 0;
+ return ev_writer_close(c->w);
+ }
+ return 0;
+}
+
+static int listen_callback(ev_source *ev,
+ int fd,
+ const struct sockaddr attribute((unused)) *remote,
+ socklen_t attribute((unused)) rlen,
+ void *u) {
+ const struct listener *l = u;
+ struct conn *c = xmalloc(sizeof *c);
+ static unsigned tags;
+
+ D(("server listen_callback fd %d (%s)", fd, l->name));
+ nonblock(fd);
+ cloexec(fd);
+ c->tag = tags++;
+ c->ev = ev;
+ c->w = ev_writer_new(ev, fd, writer_error, c);
+ c->r = ev_reader_new(ev, fd, redirect_reader_callback, reader_error, c);
+ c->fd = fd;
+ c->reader = reader_callback;
+ c->l = l;
+ gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
+ sink_printf(ev_writer_sink(c->w), "231 %s\n", hex(c->nonce, sizeof c->nonce));
+ return 0;
+}
+
+int server_start(ev_source *ev, int pf,
+ size_t socklen, const struct sockaddr *sa,
+ const char *name) {
+ int fd;
+ struct listener *l = xmalloc(sizeof *l);
+ static const int one = 1;
+
+ D(("server_init socket %s", name));
+ fd = xsocket(pf, SOCK_STREAM, 0);
+ xsetsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
+ if(bind(fd, sa, socklen) < 0) {
+ error(errno, "error binding to %s", name);
+ return -1;
+ }
+ xlisten(fd, 128);
+ nonblock(fd);
+ cloexec(fd);
+ l->name = name;
+ l->pf = pf;
+ if(ev_listen(ev, fd, listen_callback, l)) exit(EXIT_FAILURE);
+ return fd;
+}
+
+int server_stop(ev_source *ev, int fd) {
+ xclose(fd);
+ return ev_listen_cancel(ev, fd);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
+/* arch-tag:eb9b30c87008880f3f53535101356ab5 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/* This program deliberately does not use the garbage collector even though it
+ * might be convenient to do so. This is for two reasons. Firstly some libao
+ * drivers are implemented using threads and we do not want to have to deal
+ * with potential interactions between threading and garbage collection.
+ * Secondly this process needs to be able to respond quickly and this is not
+ * compatible with the collector hanging the program even relatively
+ * briefly. */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <errno.h>
+#include <ao/ao.h>
+#include <string.h>
+#include <assert.h>
+#include <sys/select.h>
+#include <time.h>
+#include <alsa/asoundlib.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "defs.h"
+#include "mem.h"
+#include "speaker.h"
+#include "user.h"
+
+#define BUFFER_SECONDS 5 /* How many seconds of input to
+ * buffer. */
+
+#define FRAMES 4096 /* Frame batch size */
+
+#define NFDS 256 /* Max FDs to poll for */
+
+/* Known tracks are kept in a linked list. We don't normally to have
+ * more than two - maybe three at the outside. */
+static struct track {
+ struct track *next; /* next track */
+ int fd; /* input FD */
+ char id[24]; /* ID */
+ size_t start, used; /* start + bytes used */
+ int eof; /* input is at EOF */
+ int got_format; /* got format yet? */
+ ao_sample_format format; /* sample format */
+ unsigned long long played; /* number of frames played */
+ char *buffer; /* sample buffer */
+ size_t size; /* sample buffer size */
+ int slot; /* poll array slot */
+} *tracks, *playing; /* all tracks + playing track */
+
+static time_t last_report; /* when we last reported */
+static int paused; /* pause status */
+static snd_pcm_t *pcm; /* current pcm handle */
+static ao_sample_format pcm_format; /* current format if aodev != 0 */
+static size_t bpf; /* bytes per frame */
+static struct pollfd fds[NFDS]; /* if we need more than that */
+static int fdno; /* fd number */
+static snd_pcm_uframes_t pcm_bufsize; /* buffer size */
+static int forceplay; /* frames to force play */
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { "no-debug", no_argument, 0, 'D' },
+ { 0, 0, 0, 0 }
+};
+
+/* Display usage message and terminate. */
+static void help(void) {
+ xprintf("Usage:\n"
+ " disorder-speaker [OPTIONS]\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n"
+ "\n"
+ "Speaker process for DisOrder. Not intended to be run\n"
+ "directly.\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* Display version number and terminate. */
+static void version(void) {
+ xprintf("disorder-speaker version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+/* Return the number of bytes per frame in FORMAT. */
+static size_t bytes_per_frame(const ao_sample_format *format) {
+ return format->channels * format->bits / 8;
+}
+
+/* Find track ID, maybe creating it if not found. */
+static struct track *findtrack(const char *id, int create) {
+ struct track *t;
+
+ D(("findtrack %s %d", id, create));
+ for(t = tracks; t && strcmp(id, t->id); t = t->next)
+ ;
+ if(!t && create) {
+ t = xmalloc(sizeof *t);
+ t->next = tracks;
+ strcpy(t->id, id);
+ t->fd = -1;
+ tracks = t;
+ /* The initial input buffer will be the sample format. */
+ t->buffer = (void *)&t->format;
+ t->size = sizeof t->format;
+ }
+ return t;
+}
+
+/* Remove track ID (but do not destroy it). */
+static struct track *removetrack(const char *id) {
+ struct track *t, **tt;
+
+ D(("removetrack %s", id));
+ for(tt = &tracks; (t = *tt) && strcmp(id, t->id); tt = &t->next)
+ ;
+ if(t)
+ *tt = t->next;
+ return t;
+}
+
+/* Destroy a track. */
+static void destroy(struct track *t) {
+ D(("destroy %s", t->id));
+ if(t->fd != -1) xclose(t->fd);
+ if(t->buffer != (void *)&t->format) free(t->buffer);
+ free(t);
+}
+
+/* Notice a new FD. */
+static void acquire(struct track *t, int fd) {
+ D(("acquire %s %d", t->id, fd));
+ if(t->fd != -1)
+ xclose(t->fd);
+ t->fd = fd;
+ nonblock(fd);
+}
+
+/* Read data into a sample buffer. Return 0 on success, -1 on EOF. */
+static int fill(struct track *t) {
+ size_t where, left;
+ int n;
+
+ D(("fill %s: eof=%d used=%zu size=%zu got_format=%d",
+ t->id, t->eof, t->used, t->size, t->got_format));
+ if(t->eof) return -1;
+ if(t->used < t->size) {
+ /* there is room left in the buffer */
+ where = (t->start + t->used) % t->size;
+ if(t->got_format) {
+ /* We are reading audio data, get as much as we can */
+ if(where >= t->start) left = t->size - where;
+ else left = t->start - where;
+ } else
+ /* We are still waiting for the format, only get that */
+ left = sizeof (ao_sample_format) - t->used;
+ do {
+ n = read(t->fd, t->buffer + where, left);
+ } while(n < 0 && errno == EINTR);
+ if(n < 0) {
+ if(errno != EAGAIN) fatal(errno, "error reading sample stream");
+ return 0;
+ }
+ if(n == 0) {
+ D(("fill %s: eof detected", t->id));
+ t->eof = 1;
+ return -1;
+ }
+ t->used += n;
+ if(!t->got_format && t->used >= sizeof (ao_sample_format)) {
+ assert(t->used == sizeof (ao_sample_format));
+ /* Check that our assumptions are met. */
+ if(t->format.bits & 7)
+ fatal(0, "bits per sample not a multiple of 8");
+ /* Make a new buffer for audio data. */
+ t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS;
+ t->buffer = xmalloc(t->size);
+ t->used = 0;
+ t->got_format = 1;
+ D(("got format for %s", t->id));
+ }
+ }
+ return 0;
+}
+
+/* Return true if A and B denote identical libao formats, else false. */
+static int formats_equal(const ao_sample_format *a,
+ const ao_sample_format *b) {
+ return (a->bits == b->bits
+ && a->rate == b->rate
+ && a->channels == b->channels
+ && a->byte_format == b->byte_format);
+}
+
+/* Close the sound device. */
+static void idle(void) {
+ int err;
+
+ D(("idle"));
+ if(pcm) {
+ if((err = snd_pcm_nonblock(pcm, 0)) < 0)
+ fatal(0, "error calling snd_pcm_nonblock: %d", err);
+ D(("draining pcm"));
+ snd_pcm_drain(pcm);
+ D(("closing pcm"));
+ snd_pcm_close(pcm);
+ pcm = 0;
+ forceplay = 0;
+ D(("released audio device"));
+ }
+}
+
+/* Abandon the current track */
+static void abandon(void) {
+ struct speaker_message sm;
+
+ D(("abandon"));
+ memset(&sm, 0, sizeof sm);
+ sm.type = SM_FINISHED;
+ strcpy(sm.id, playing->id);
+ speaker_send(1, &sm, 0);
+ removetrack(playing->id);
+ destroy(playing);
+ playing = 0;
+ forceplay = 0;
+}
+
+/* Make sure the sound device is open and has the right sample format. Return
+ * 0 on success and -1 on error. */
+static int activate(void) {
+ int err;
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_sw_params_t *swparams;
+ int sample_format = 0;
+ unsigned rate;
+
+ /* If we don't know the format yet we cannot start. */
+ if(!playing->got_format) {
+ D((" - not got format for %s", playing->id));
+ return -1;
+ }
+ /* If we need to change format then close the current device. */
+ if(pcm && !formats_equal(&playing->format, &pcm_format))
+ idle();
+ if(!pcm) {
+ D(("snd_pcm_open"));
+ if((err = snd_pcm_open(&pcm,
+ config->device,
+ SND_PCM_STREAM_PLAYBACK,
+ SND_PCM_NONBLOCK))) {
+ error(0, "error from snd_pcm_open: %d", err);
+ goto error;
+ }
+ snd_pcm_hw_params_alloca(&hwparams);
+ D(("set up hw params"));
+ if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+ fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+ if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
+ SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+ fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
+ switch(playing->format.bits) {
+ case 8:
+ sample_format = SND_PCM_FORMAT_S8;
+ break;
+ case 16:
+ switch(playing->format.byte_format) {
+ case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break;
+ case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
+ case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
+ error(0, "unrecognized byte format %d", playing->format.byte_format);
+ goto fatal;
+ }
+ break;
+ default:
+ error(0, "unsupported sample size %d", playing->format.bits);
+ goto fatal;
+ }
+ if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+ sample_format)) < 0) {
+ error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+ sample_format, err);
+ goto fatal;
+ }
+ rate = playing->format.rate;
+ if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) {
+ error(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
+ playing->format.rate, err);
+ goto fatal;
+ }
+ if(rate != (unsigned)playing->format.rate)
+ info("want rate %d, got %u", playing->format.rate, rate);
+ if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
+ playing->format.channels)) < 0) {
+ error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+ playing->format.channels, err);
+ goto fatal;
+ }
+ pcm_bufsize = 3 * FRAMES;
+ if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+ &pcm_bufsize)) < 0)
+ fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+ 3 * FRAMES, err);
+ if(pcm_bufsize != 3 * FRAMES)
+ info("asked for PCM buffer of %d frames, got %d",
+ 3 * FRAMES, (int)pcm_bufsize);
+ if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+ fatal(0, "error calling snd_pcm_hw_params: %d", err);
+ D(("set up sw params"));
+ snd_pcm_sw_params_alloca(&swparams);
+ if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+ fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+ if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
+ fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+ FRAMES, err);
+ if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+ fatal(0, "error calling snd_pcm_sw_params: %d", err);
+ pcm_format = playing->format;
+ bpf = bytes_per_frame(&pcm_format);
+ D(("acquired audio device"));
+ }
+ return 0;
+fatal:
+ abandon();
+error:
+ /* We assume the error is temporary and that we'll retry in a bit. */
+ if(pcm) {
+ snd_pcm_close(pcm);
+ pcm = 0;
+ }
+ return -1;
+}
+
+/* Check to see whether the current track has finished playing */
+static void maybe_finished(void) {
+ if(playing
+ && playing->eof
+ && (!playing->got_format
+ || playing->used < bytes_per_frame(&playing->format)))
+ abandon();
+}
+
+static void play(size_t frames) {
+ snd_pcm_sframes_t written_frames;
+ size_t avail_bytes, avail_frames, written_bytes;
+ int err;
+
+ if(activate()) {
+ if(playing)
+ forceplay = frames;
+ else
+ forceplay = 0; /* Must have called abandon() */
+ return;
+ }
+ D(("play: play %zu/%zu%s %dHz %db %dc", frames, playing->used / bpf,
+ playing->eof ? " EOF" : "",
+ playing->format.rate,
+ playing->format.bits,
+ playing->format.channels));
+ /* If we haven't got enough bytes yet wait until we have. Exception: when
+ * we are at eof. */
+ if(playing->used < frames * bpf && !playing->eof) {
+ forceplay = frames;
+ return;
+ }
+ /* We have got enough data so don't force play again */
+ forceplay = 0;
+ /* Figure out how many frames there are available to write */
+ if(playing->start + playing->used > playing->size)
+ avail_bytes = playing->size - playing->start;
+ else
+ avail_bytes = playing->used;
+ avail_frames = avail_bytes / bpf;
+ if(avail_frames > frames)
+ avail_frames = frames;
+ if(!avail_frames)
+ return;
+ written_frames = snd_pcm_writei(pcm,
+ playing->buffer + playing->start,
+ avail_frames);
+ D(("actually play %zu frames, wrote %d",
+ avail_frames, (int)written_frames));
+ if(written_frames < 0) {
+ switch(written_frames) {
+ case -EPIPE: /* underrun */
+ error(0, "snd_pcm_writei reports underrun");
+ if((err = snd_pcm_prepare(pcm)) < 0)
+ fatal(0, "error calling snd_pcm_prepare: %d", err);
+ return;
+ case -EAGAIN:
+ return;
+ default:
+ fatal(0, "error calling snd_pcm_writei: %d", (int)written_frames);
+ }
+ }
+ written_bytes = written_frames * bpf;
+ playing->start += written_bytes;
+ playing->used -= written_bytes;
+ playing->played += written_frames;
+ /* If the pointer is at the end of the buffer (or the buffer is completely
+ * empty) wrap it back to the start. */
+ if(!playing->used || playing->start == playing->size)
+ playing->start = 0;
+ frames -= written_frames;
+}
+
+/* Notify the server what we're up to. */
+static void report(void) {
+ struct speaker_message sm;
+
+ if(playing && playing->buffer != (void *)&playing->format) {
+ memset(&sm, 0, sizeof sm);
+ sm.type = paused ? SM_PAUSED : SM_PLAYING;
+ strcpy(sm.id, playing->id);
+ sm.data = playing->played / playing->format.rate;
+ speaker_send(1, &sm, 0);
+ }
+ time(&last_report);
+}
+
+static int addfd(int fd, int events) {
+ if(fdno < NFDS) {
+ fds[fdno].fd = fd;
+ fds[fdno].events = events;
+ return fdno++;
+ } else
+ return -1;
+}
+
+int main(int argc, char **argv) {
+ int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, err;
+ unsigned short alsa_revents;
+ struct track *t;
+ struct speaker_message sm;
+
+ set_progname(argv);
+ mem_init(0);
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ while((n = getopt_long(argc, argv, "hVc:dD", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ case 'D': debugging = 0; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ if(getenv("DISORDER_DEBUG_SPEAKER")) debugging = 1;
+ /* If stderr is a TTY then log there, otherwise to syslog. */
+ if(!isatty(2)) {
+ openlog(progname, LOG_PID, LOG_DAEMON);
+ log_default = &log_syslog;
+ }
+ if(config_read()) fatal(0, "cannot read configuration");
+ /* ignore SIGPIPE */
+ signal(SIGPIPE, SIG_IGN);
+ /* set nice value */
+ xnice(config->nice_speaker);
+ /* change user */
+ become_mortal();
+ /* make sure we're not root, whatever the config says */
+ if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
+ info("started");
+ while(getppid() != 1) {
+ fdno = 0;
+ /* Always ready for commands from the main server. */
+ stdin_slot = addfd(0, POLLIN);
+ /* Try to read sample data for the currently playing track if there is
+ * buffer space. */
+ if(playing && !playing->eof && playing->used < playing->size) {
+ playing->slot = addfd(playing->fd, POLLIN);
+ } else if(playing)
+ playing->slot = -1;
+ /* If forceplay is set then wait until it succeeds before waiting on the
+ * sound device. */
+ if(pcm && !forceplay) {
+ alsa_slots = fdno;
+ alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno);
+ fdno += alsa_nslots;
+ } else
+ alsa_slots = -1;
+ /* If any other tracks don't have a full buffer, try to read sample data
+ * from them. */
+ for(t = tracks; t; t = t->next)
+ if(t != playing) {
+ if(!t->eof && t->used < t->size) {
+ t->slot = addfd(t->fd, POLLIN);
+ } else
+ t->slot = -1;
+ }
+ /* Wait up to a second before thinking about current state */
+ n = poll(fds, fdno, 1000);
+ if(n < 0) {
+ if(errno == EINTR) continue;
+ fatal(errno, "error calling poll");
+ }
+ /* Play some sound before doing anything else */
+ if(alsa_slots != -1) {
+ if((err = snd_pcm_poll_descriptors_revents(pcm,
+ &fds[alsa_slots],
+ alsa_nslots,
+ &alsa_revents)) < 0)
+ fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+ if(alsa_revents & POLLOUT)
+ play(3 * FRAMES);
+ } else {
+ /* Some attempt to play must have failed */
+ if(playing && !paused)
+ play(forceplay);
+ else
+ forceplay = 0; /* just in case */
+ }
+ /* Perhaps we have a command to process */
+ if(fds[stdin_slot].revents & POLLIN) {
+ n = speaker_recv(0, &sm, &fd);
+ if(n > 0)
+ switch(sm.type) {
+ case SM_PREPARE:
+ D(("SM_PREPARE %s %d", sm.id, fd));
+ if(fd == -1) fatal(0, "got SM_PREPARE but no file descriptor");
+ t = findtrack(sm.id, 1);
+ acquire(t, fd);
+ break;
+ case SM_PLAY:
+ D(("SM_PLAY %s %d", sm.id, fd));
+ if(playing) fatal(0, "got SM_PLAY but already playing something");
+ t = findtrack(sm.id, 1);
+ if(fd != -1) acquire(t, fd);
+ playing = t;
+ play(pcm_bufsize);
+ report();
+ break;
+ case SM_PAUSE:
+ D(("SM_PAUSE"));
+ paused = 1;
+ report();
+ break;
+ case SM_RESUME:
+ D(("SM_RESUME"));
+ if(paused) {
+ paused = 0;
+ if(playing)
+ play(pcm_bufsize);
+ }
+ report();
+ break;
+ case SM_CANCEL:
+ D(("SM_CANCEL %s", sm.id));
+ t = removetrack(sm.id);
+ if(t) {
+ if(t == playing) {
+ sm.type = SM_FINISHED;
+ strcpy(sm.id, playing->id);
+ speaker_send(1, &sm, 0);
+ playing = 0;
+ }
+ destroy(t);
+ } else
+ error(0, "SM_CANCEL for unknown track %s", sm.id);
+ report();
+ break;
+ case SM_RELOAD:
+ D(("SM_RELOAD"));
+ if(config_read()) error(0, "cannot read configuration");
+ info("reloaded configuration");
+ break;
+ default:
+ error(0, "unknown message type %d", sm.type);
+ }
+ }
+ /* Read in any buffered data */
+ for(t = tracks; t; t = t->next)
+ if(t->slot != -1 && (fds[t->slot].revents & POLLIN))
+ fill(t);
+ /* We might be able to play now */
+ if(pcm && forceplay && playing && !paused)
+ play(forceplay);
+ /* Maybe we finished playing a track somewhere in the above */
+ maybe_finished();
+ /* If we don't need the sound device for now then close it for the benefit
+ * of anyone else who wants it. */
+ if((!playing || paused) && pcm)
+ idle();
+ /* If we've not reported out state for a second do so now. */
+ if(time(0) > last_report)
+ report();
+ }
+ info("stopped (parent terminated)");
+ exit(0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:HQ4ayCGCjeBF97RuRnvcyg */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <locale.h>
+#include <stdio.h>
+#include <pcre.h>
+#include <netdb.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+
+#include "event.h"
+#include "play.h"
+#include "trackdb.h"
+#include "state.h"
+#include "configuration.h"
+#include "log.h"
+#include "queue.h"
+#include "server.h"
+#include "printf.h"
+#include "addr.h"
+
+static const char *current_unix;
+static int current_unix_fd;
+
+static struct addrinfo *current_listen_addrinfo;
+static int current_listen_fd;
+
+void quit(ev_source *ev) {
+ quitting(ev);
+ trackdb_close();
+ trackdb_deinit();
+ info("terminating");
+ _exit(0);
+}
+
+static void reset_socket(ev_source *ev) {
+ const char *new_unix;
+ struct addrinfo *res;
+ struct sockaddr_un sun;
+ char *name;
+
+ static const struct addrinfo pref = {
+ AI_PASSIVE,
+ PF_INET,
+ SOCK_STREAM,
+ IPPROTO_TCP,
+ 0,
+ 0,
+ 0,
+ 0
+ };
+
+ /* unix first */
+ new_unix = config_get_file("socket");
+ if(!current_unix || strcmp(current_unix, new_unix)) {
+ /* either there was no socket, or there was but a different path */
+ if(current_unix) {
+ /* stop the old one and remove it from the filesystem */
+ server_stop(ev, current_unix_fd);
+ if(unlink(current_unix) < 0)
+ fatal(errno, "unlink %s", current_unix);
+ }
+ /* start the new one */
+ if(strlen(new_unix) >= sizeof sun.sun_path)
+ fatal(0, "socket path %s is too long", new_unix);
+ memset(&sun, 0, sizeof sun);
+ sun.sun_family = AF_UNIX;
+ strcpy(sun.sun_path, new_unix);
+ if(unlink(new_unix) < 0 && errno != ENOENT)
+ fatal(errno, "unlink %s", new_unix);
+ if((current_unix_fd = server_start(ev, PF_UNIX, sizeof sun,
+ (const struct sockaddr *)&sun,
+ new_unix)) >= 0) {
+ current_unix = new_unix;
+ if(chmod(new_unix, 0777) < 0)
+ fatal(errno, "error calling chmod %s", new_unix);
+ } else
+ current_unix = 0;
+ }
+
+ /* get the new listen config */
+ if(config->listen.n)
+ res = get_address(&config->listen, &pref, &name);
+ else
+ res = 0;
+
+ if((res && !current_listen_addrinfo)
+ || (current_listen_addrinfo
+ && (!res
+ || addrinfocmp(res, current_listen_addrinfo)))) {
+ /* something has to change */
+ if(current_listen_addrinfo) {
+ /* delete the old listener */
+ server_stop(ev, current_listen_fd);
+ freeaddrinfo(current_listen_addrinfo);
+ current_listen_addrinfo = 0;
+ }
+ if(res) {
+ /* start the new listener */
+ if((current_listen_fd = server_start(ev, res->ai_family, res->ai_addrlen,
+ res->ai_addr, name)) >= 0) {
+ current_listen_addrinfo = res;
+ res = 0;
+ }
+ }
+ }
+ /* if res is still set it needs freeing */
+ if(res)
+ freeaddrinfo(res);
+}
+
+int reconfigure(ev_source *ev, int reload) {
+ int need_another_rescan = 0;
+ int ret = 0;
+
+ D(("reconfigure(%d)", reload));
+ if(reload) {
+ need_another_rescan = trackdb_rescan_cancel();
+ trackdb_close();
+ if(config_read())
+ ret = -1;
+ else {
+ /* Tell the speaker it needs to reload its config too. */
+ speaker_reload();
+ info("%s: installed new configuration", configfile);
+ }
+ }
+ trackdb_open();
+ if(need_another_rescan)
+ trackdb_rescan(ev);
+ if(!ret) {
+ queue_read();
+ recent_read();
+ reset_socket(ev);
+ }
+ return ret;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:94e23a75c2ebdf8a11e17ed7b0fd8cb6 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <db.h>
+#include <sys/socket.h>
+#include <pcre.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stddef.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <time.h>
+
+#include "event.h"
+#include "mem.h"
+#include "kvp.h"
+#include "log.h"
+#include "vector.h"
+#include "trackdb.h"
+#include "configuration.h"
+#include "syscalls.h"
+#include "wstat.h"
+#include "words.h"
+#include "printf.h"
+#include "filepart.h"
+#include "trackname.h"
+#include "trackdb-int.h"
+#include "logfd.h"
+#include "cache.h"
+#include "eventlog.h"
+#include "hash.h"
+
+#define RESCAN "disorder-rescan"
+#define DEADLOCK "disorder-deadlock"
+
+static const char *getpart(const char *track,
+ const char *context,
+ const char *part,
+ const struct kvp *p,
+ int *used_db);
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp);
+static int trackdb_get_global_tid(const char *name,
+ DB_TXN *tid,
+ const char **rp);
+
+const struct cache_type cache_files_type = { 86400 };
+unsigned long cache_files_hits, cache_files_misses;
+
+/* setup and teardown ********************************************************/
+
+static const char *home; /* home had better not change */
+DB_ENV *trackdb_env; /* db environment */
+DB *trackdb_tracksdb; /* the db itself */
+DB *trackdb_prefsdb; /* preferences */
+DB *trackdb_searchdb; /* the search database */
+DB *trackdb_tagsdb; /* the tags database */
+DB *trackdb_globaldb; /* global preferences */
+static pid_t db_deadlock_pid = -1; /* deadlock manager PID */
+static pid_t rescan_pid = -1; /* rescanner PID */
+static int initialized, opened; /* state */
+
+/* tracks matched by required_tags */
+static char **reqtracks;
+static size_t nreqtracks;
+
+/* comparison function for keys */
+static int compare(DB attribute((unused)) *db_,
+ const DBT *a, const DBT *b) {
+ return compare_path_raw(a->data, a->size, b->data, b->size);
+}
+
+/* open environment */
+void trackdb_init(int recover) {
+ int err;
+ static int recover_type[] = { 0, DB_RECOVER, DB_RECOVER_FATAL };
+
+ /* sanity checks */
+ assert(initialized == 0);
+ ++initialized;
+ if(home) {
+ if(strcmp(home, config->home))
+ fatal(0, "cannot change db home without server restart");
+ home = config->home;
+ }
+
+ /* create environment */
+ if((err = db_env_create(&trackdb_env, 0))) fatal(0, "db_env_create: %s",
+ db_strerror(err));
+ if((err = trackdb_env->set_alloc(trackdb_env,
+ xmalloc_noptr, xrealloc_noptr, xfree)))
+ fatal(0, "trackdb_env->set_alloc: %s", db_strerror(err));
+ if((err = trackdb_env->set_lk_max_locks(trackdb_env, 10000)))
+ fatal(0, "trackdb_env->set_lk_max_locks: %s", db_strerror(err));
+ if((err = trackdb_env->set_lk_max_objects(trackdb_env, 10000)))
+ fatal(0, "trackdb_env->set_lk_max_objects: %s", db_strerror(err));
+ if((err = trackdb_env->open(trackdb_env, config->home,
+ DB_INIT_LOG
+ |DB_INIT_LOCK
+ |DB_INIT_MPOOL
+ |DB_INIT_TXN
+ |DB_CREATE
+ |recover_type[recover],
+ 0666)))
+ fatal(0, "trackdb_env->open: %s", db_strerror(err));
+ trackdb_env->set_errpfx(trackdb_env, "DB");
+ trackdb_env->set_errfile(trackdb_env, stderr);
+ trackdb_env->set_verbose(trackdb_env, DB_VERB_DEADLOCK, 1);
+ trackdb_env->set_verbose(trackdb_env, DB_VERB_RECOVERY, 1);
+ trackdb_env->set_verbose(trackdb_env, DB_VERB_REPLICATION, 1);
+ D(("initialized database environment"));
+}
+
+/* called when deadlock manager terminates */
+static int reap_db_deadlock(ev_source attribute((unused)) *ev,
+ pid_t attribute((unused)) pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void attribute((unused)) *u) {
+ db_deadlock_pid = -1;
+ if(initialized)
+ fatal(0, "deadlock manager unexpectedly terminated: %s",
+ wstat(status));
+ else
+ D(("deadlock manager terminated: %s", wstat(status)));
+ return 0;
+}
+
+static pid_t subprogram(ev_source *ev, const char *prog) {
+ pid_t pid;
+ int lfd;
+
+ /* If we're in the background then trap subprocess stdout/stderr */
+ if(!isatty(2))
+ lfd = logfd(ev, prog);
+ else
+ lfd = -1;
+ if(!(pid = xfork())) {
+ exitfn = _exit;
+ ev_signal_atfork(ev);
+ signal(SIGPIPE, SIG_DFL);
+ if(lfd != -1) {
+ xdup2(lfd, 1);
+ xdup2(lfd, 2);
+ }
+ /* If we were negatively niced, undo it. We don't bother checking for
+ * error, it's not that important. */
+ setpriority(PRIO_PROCESS, 0, 0);
+ execlp(prog, prog, "--config", configfile,
+ debugging ? "--debug" : "--no-debug",
+ (char *)0);
+ fatal(errno, "error invoking %s", prog);
+ }
+ if(lfd != -1) xclose(lfd);
+ return pid;
+}
+
+/* start deadlock manager */
+void trackdb_master(ev_source *ev) {
+ assert(db_deadlock_pid == -1);
+ db_deadlock_pid = subprogram(ev, DEADLOCK);
+ ev_child(ev, db_deadlock_pid, 0, reap_db_deadlock, 0);
+ D(("started deadlock manager"));
+}
+
+/* close environment */
+void trackdb_deinit(void) {
+ int err;
+
+ /* sanity checks */
+ assert(initialized == 1);
+ --initialized;
+
+ /* close the environment */
+ if((err = trackdb_env->close(trackdb_env, 0)))
+ fatal(0, "trackdb_env->close: %s", db_strerror(err));
+
+ if(rescan_pid != -1 && kill(rescan_pid, SIGTERM) < 0)
+ fatal(errno, "error killing rescanner");
+
+ /* terminate the deadlock manager */
+ if(db_deadlock_pid != -1 && kill(db_deadlock_pid, SIGTERM) < 0)
+ fatal(errno, "error killing deadlock manager");
+ db_deadlock_pid = -1;
+
+ D(("deinitialized database environment"));
+}
+
+/* open a specific database */
+static DB *open_db(const char *path,
+ u_int32_t dbflags,
+ DBTYPE dbtype,
+ u_int32_t openflags,
+ int mode) {
+ int err;
+ DB *db;
+
+ D(("open %s", path));
+ path = config_get_file(path);
+ if((err = db_create(&db, trackdb_env, 0)))
+ fatal(0, "db_create %s: %s", path, db_strerror(err));
+ if(dbflags)
+ if((err = db->set_flags(db, dbflags)))
+ fatal(0, "db->set_flags %s: %s", path, db_strerror(err));
+ if(dbtype == DB_BTREE)
+ if((err = db->set_bt_compare(db, compare)))
+ fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err));
+ if((err = db->open(db, 0, path, 0, dbtype,
+ openflags | DB_AUTO_COMMIT, mode)))
+ fatal(0, "db->open %s: %s", path, db_strerror(err));
+ return db;
+}
+
+/* open track databases */
+void trackdb_open(void) {
+ /* sanity checks */
+ assert(opened == 0);
+ ++opened;
+ /* open the databases */
+ trackdb_tracksdb = open_db("tracks.db",
+ DB_RECNUM, DB_BTREE, DB_CREATE, 0666);
+ trackdb_searchdb = open_db("search.db",
+ DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+ trackdb_tagsdb = open_db("tags.db",
+ DB_DUP|DB_DUPSORT, DB_HASH, DB_CREATE, 0666);
+ trackdb_prefsdb = open_db("prefs.db", 0, DB_HASH, DB_CREATE, 0666);
+ trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666);
+ D(("opened databases"));
+}
+
+/* close track databases */
+void trackdb_close(void) {
+ int err;
+
+ /* sanity checks */
+ assert(opened == 1);
+ --opened;
+ if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0)))
+ fatal(0, "error closing tracks.db: %s", db_strerror(err));
+ if((err = trackdb_searchdb->close(trackdb_searchdb, 0)))
+ fatal(0, "error closing search.db: %s", db_strerror(err));
+ if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0)))
+ fatal(0, "error closing tags.db: %s", db_strerror(err));
+ if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0)))
+ fatal(0, "error closing prefs.db: %s", db_strerror(err));
+ if((err = trackdb_globaldb->close(trackdb_globaldb, 0)))
+ fatal(0, "error closing global.db: %s", db_strerror(err));
+ trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
+ trackdb_tagsdb = trackdb_globaldb = 0;
+ D(("closed databases"));
+}
+
+/* generic db routines *******************************************************/
+
+/* fetch and decode a database entry. Returns 0, DB_NOTFOUND or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_getdata(DB *db,
+ const char *track,
+ struct kvp **kp,
+ DB_TXN *tid) {
+ int err;
+ DBT key, data;
+
+ switch(err = db->get(db, tid, make_key(&key, track),
+ prepare_data(&data), 0)) {
+ case 0:
+ *kp = kvp_urldecode(data.data, data.size);
+ return 0;
+ case DB_NOTFOUND:
+ *kp = 0;
+ return err;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying database: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error querying database: %s", db_strerror(err));
+ }
+}
+
+/* encode and store a database entry. Returns 0, DB_KEYEXIST or
+ * DB_LOCK_DEADLOCK. */
+int trackdb_putdata(DB *db,
+ const char *track,
+ const struct kvp *k,
+ DB_TXN *tid,
+ u_int32_t flags) {
+ int err;
+ DBT key, data;
+
+ switch(err = db->put(db, tid, make_key(&key, track),
+ encode_data(&data, k), flags)) {
+ case 0:
+ case DB_KEYEXIST:
+ return err;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating database: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating database: %s", db_strerror(err));
+ }
+}
+
+/* delete a database entry */
+int trackdb_delkey(DB *db,
+ const char *track,
+ DB_TXN *tid) {
+ int err;
+
+ DBT key;
+ switch(err = db->del(db, tid, make_key(&key, track), 0)) {
+ case 0:
+ case DB_NOTFOUND:
+ return 0;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating database: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating database: %s", db_strerror(err));
+ }
+}
+
+/* open a database cursor */
+DBC *trackdb_opencursor(DB *db, DB_TXN *tid) {
+ int err;
+ DBC *c;
+
+ switch(err = db->cursor(db, tid, &c, 0)) {
+ case 0: break;
+ default: fatal(0, "error creating cursor: %s", db_strerror(err));
+ }
+ return c;
+}
+
+/* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */
+int trackdb_closecursor(DBC *c) {
+ int err;
+
+ if(!c) return 0;
+ switch(err = c->c_close(c)) {
+ case 0:
+ return err;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error closing cursor: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error closing cursor: %s", db_strerror(err));
+ }
+}
+
+/* delete a (key,data) pair. Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */
+int trackdb_delkeydata(DB *db,
+ const char *word,
+ const char *track,
+ DB_TXN *tid) {
+ int err;
+ DBC *c;
+ DBT key, data;
+
+ c = trackdb_opencursor(db, tid);
+ switch(err = c->c_get(c, make_key(&key, word),
+ make_key(&data, track), DB_GET_BOTH)) {
+ case 0:
+ switch(err = c->c_del(c, 0)) {
+ case 0:
+ break;
+ case DB_KEYEMPTY:
+ err = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating database: %s", db_strerror(err));
+ break;
+ default:
+ fatal(0, "c->c_del: %s", db_strerror(err));
+ }
+ break;
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating database: %s", db_strerror(err));
+ break;
+ default:
+ fatal(0, "c->c_get: %s", db_strerror(err));
+ }
+ if(trackdb_closecursor(c)) err = DB_LOCK_DEADLOCK;
+ return err;
+}
+
+/* start a transaction */
+DB_TXN *trackdb_begin_transaction(void) {
+ DB_TXN *tid;
+ int err;
+
+ if((err = trackdb_env->txn_begin(trackdb_env, 0, &tid, 0)))
+ fatal(0, "trackdb_env->txn_begin: %s", db_strerror(err));
+ return tid;
+}
+
+/* abort transaction */
+void trackdb_abort_transaction(DB_TXN *tid) {
+ int err;
+
+ if(tid)
+ if((err = tid->abort(tid)))
+ fatal(0, "tid->abort: %s", db_strerror(err));
+}
+
+/* commit transaction */
+void trackdb_commit_transaction(DB_TXN *tid) {
+ int err;
+
+ if((err = tid->commit(tid, 0)))
+ fatal(0, "tid->commit: %s", db_strerror(err));
+}
+
+/* search/tags shared code ***************************************************/
+
+/* comparison function used by dedupe() */
+static int wordcmp(const void *a, const void *b) {
+ return strcmp(*(const char **)a, *(const char **)b);
+}
+
+/* sort and de-dupe VEC */
+static char **dedupe(char **vec, int nvec) {
+ int m, n;
+
+ qsort(vec, nvec, sizeof (char *), wordcmp);
+ m = n = 0;
+ if(nvec) {
+ vec[m++] = vec[0];
+ for(n = 1; n < nvec; ++n)
+ if(strcmp(vec[n], vec[m - 1]))
+ vec[m++] = vec[n];
+ }
+ vec[m] = 0;
+ return vec;
+}
+
+/* update a key/track database. Returns 0 or DB_DEADLOCK. */
+static int register_word(DB *db, const char *what,
+ const char *track, const char *word,
+ DB_TXN *tid) {
+ int err;
+ DBT key, data;
+
+ switch(err = db->put(db, tid, make_key(&key, word),
+ make_key(&data, track), DB_NODUPDATA)) {
+ case 0:
+ case DB_KEYEXIST:
+ return 0;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating %s.db: %s", what, db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating %s.db: %s", what, db_strerror(err));
+ }
+}
+
+/* search primitives *********************************************************/
+
+/* return true iff NAME is a trackname_display_ pref */
+static int is_display_pref(const char *name) {
+ static const char prefix[] = "trackname_display_";
+ return !strncmp(name, prefix, (sizeof prefix) - 1);
+}
+
+/* compute the words of a track name */
+static char **track_to_words(const char *track,
+ const struct kvp *p) {
+ struct vector v;
+ char **w;
+ int nw;
+
+ vector_init(&v);
+ if((w = words(casefold(strip_extension(track_rootless(track))), &nw)))
+ vector_append_many(&v, w, nw);
+
+ for(; p; p = p->next)
+ if(is_display_pref(p->name))
+ if((w = words(casefold(p->value), &nw)))
+ vector_append_many(&v, w, nw);
+ vector_terminate(&v);
+ return dedupe(v.vec, v.nvec);
+}
+
+/* return nonzero iff WORD is a stopword */
+static int stopword(const char *word) {
+ int n;
+
+ for(n = 0; n < config->stopword.n
+ && strcmp(word, config->stopword.s[n]); ++n)
+ ;
+ return n < config->stopword.n;
+}
+
+/* record that WORD appears in TRACK. Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_search_word(const char *track, const char *word,
+ DB_TXN *tid) {
+ if(stopword(word)) return 0;
+ return register_word(trackdb_searchdb, "search", track, word, tid);
+}
+
+/* Tags **********************************************************************/
+
+/* Return nonzero if C is a valid tag character */
+static int tagchar(int c) {
+ switch(c) {
+ case ',':
+ return 0;
+ default:
+ return c >= ' ';
+ }
+}
+
+/* Parse and de-dupe a tag list. If S=0 then assumes "". */
+static char **parsetags(const char *s) {
+ const char *t;
+ struct vector v;
+
+ vector_init(&v);
+ if(s) {
+ /* skip initial separators */
+ while(*s && (!tagchar(*s) || *s == ' '))
+ ++s;
+ while(*s) {
+ /* find the extent of the tag */
+ t = s;
+ while(*s && tagchar(*s))
+ ++s;
+ /* strip trailing spaces */
+ while(s > t && s[-1] == ' ')
+ --s;
+ vector_append(&v, xstrndup(t, s - t));
+ /* skip intermediate and trailing separators */
+ while(*s && (!tagchar(*s) || *s == ' '))
+ ++s;
+ }
+ }
+ vector_terminate(&v);
+ return dedupe(v.vec, v.nvec);
+}
+
+/* Record that TRACK has TAG. Returns 0 or DB_LOCK_DEADLOCK. */
+static int register_tag(const char *track, const char *tag, DB_TXN *tid) {
+ return register_word(trackdb_tagsdb, "tags", track, tag, tid);
+}
+
+/* aliases *******************************************************************/
+
+/* compute the alias and store at aliasp. Returns 0 or DB_LOCK_DEADLOCK. If
+ * there is no alias sets *aliasp to 0. */
+static int compute_alias(char **aliasp,
+ const char *track,
+ const struct kvp *p,
+ DB_TXN *tid) {
+ struct dynstr d;
+ const char *s = config->alias, *t, *expansion, *part;
+ int c, used_db = 0, slash_prefix, err;
+ struct kvp *at;
+
+ if(strstr(track, "Troggs"))
+ D(("computing alias for %s", track));
+ dynstr_init(&d);
+ dynstr_append_string(&d, find_track_root(track));
+ while((c = (unsigned char)*s++)) {
+ if(c != '{') {
+ dynstr_append(&d, c);
+ continue;
+ }
+ if((slash_prefix = (*s == '/')))
+ s++;
+ t = strchr(s, '}');
+ assert(t != 0); /* validated at startup */
+ part = xstrndup(s, t - s);
+ expansion = getpart(track, "display", part, p, &used_db);
+ if(*expansion) {
+ if(slash_prefix) dynstr_append(&d, '/');
+ dynstr_append_string(&d, expansion);
+ }
+ s = t + 1; /* skip {part} */
+ }
+ /* only admit to the alias if we used the db... */
+ if(!used_db) {
+ *aliasp = 0;
+ return 0;
+ }
+ dynstr_terminate(&d);
+ /* ...and the answer differs from the original... */
+ if(!strcmp(track, d.vec)) {
+ *aliasp = 0;
+ return 0;
+ }
+ /* ...and there isn't already a different track with that name (including as
+ * an alias) */
+ switch(err = trackdb_getdata(trackdb_tracksdb, d.vec, &at, tid)) {
+ case 0:
+ if(strstr(track, "Troggs"))
+ D(("found a hit for alias"));
+ if((s = kvp_get(at, "_alias_for"))
+ && !strcmp(s, track)) {
+ case DB_NOTFOUND:
+ if(strstr(track, "Troggs"))
+ D(("accepting anyway"));
+ *aliasp = d.vec;
+ } else {
+ if(strstr(track, "Troggs")) {
+ D(("rejecting"));
+ D(("%s", track));
+ D(("%s", s ? s : "(null)"));
+ }
+ *aliasp = 0;
+ }
+ return 0;
+ default:
+ return err;
+ }
+}
+
+/* get track and prefs data (if tp/pp not null pointers). Returns 0 on
+ * success, DB_NOTFOUND if the track does not exist or DB_LOCK_DEADLOCK.
+ * Always sets the return values, even if only to null pointers. */
+static int gettrackdata(const char *track,
+ struct kvp **tp,
+ struct kvp **pp,
+ const char **actualp,
+ unsigned flags,
+#define GTD_NOALIAS 0x0001
+ DB_TXN *tid) {
+ int err;
+ const char *actual = track;
+ struct kvp *t = 0, *p = 0;
+
+ if((err = trackdb_getdata(trackdb_tracksdb, track, &t, tid))) goto done;
+ if((actual = kvp_get(t, "_alias_for"))) {
+ if(flags & GTD_NOALIAS) {
+ error(0, "alias passed to gettrackdata where real path required");
+ abort();
+ }
+ if((err = trackdb_getdata(trackdb_tracksdb, actual, &t, tid))) goto done;
+ } else
+ actual = track;
+ assert(actual != 0);
+ if(pp) {
+ if((err = trackdb_getdata(trackdb_prefsdb, actual, &p, tid)) == DB_LOCK_DEADLOCK)
+ goto done;
+ }
+ err = 0;
+done:
+ if(actualp) *actualp = actual;
+ if(tp) *tp = t;
+ if(pp) *pp = p;
+ return err;
+}
+
+/* trackdb_notice() **********************************************************/
+
+/* notice a track */
+int trackdb_notice(const char *track,
+ const char *path) {
+ int err;
+ DB_TXN *tid;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ err = trackdb_notice_tid(track, path, tid);
+ if((err == DB_LOCK_DEADLOCK)) goto fail;
+ break;
+ fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return err;
+}
+
+int trackdb_notice_tid(const char *track,
+ const char *path,
+ DB_TXN *tid) {
+ int err, n;
+ struct kvp *t, *a, *p;
+ int t_changed, ret;
+ char *alias, **w;
+
+ /* notice whether the tracks.db entry changes */
+ t_changed = 0;
+ /* get any existing tracks entry */
+ if((err = gettrackdata(track, &t, &p, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+ return err;
+ ret = err;
+ /* this is a real track */
+ t_changed += kvp_set(&t, "_alias_for", 0);
+ t_changed += kvp_set(&t, "_path", path);
+ /* if we have an alias record it in the database */
+ if((err = compute_alias(&alias, track, p, tid))) return err;
+ if(alias) {
+ /* won't overwrite someone else's alias as compute_alias() checks */
+ D(("%s: alias %s", track, alias));
+ a = 0;
+ kvp_set(&a, "_alias_for", track);
+ if((err = trackdb_putdata(trackdb_tracksdb, alias, a, tid, 0))) return err;
+ }
+ /* update search.db */
+ w = track_to_words(track, p);
+ for(n = 0; w[n]; ++n)
+ if((err = register_search_word(track, w[n], tid)))
+ return err;
+ /* update tags.db */
+ w = parsetags(kvp_get(p, "tags"));
+ for(n = 0; w[n]; ++n)
+ if((err = register_tag(track, w[n], tid)))
+ return err;
+ reqtracks = 0;
+ /* only store the tracks.db entry if it has changed */
+ if(t_changed && (err = trackdb_putdata(trackdb_tracksdb, track, t, tid, 0)))
+ return err;
+ return ret;
+}
+
+/* trackdb_obsolete() ********************************************************/
+
+/* obsolete a track */
+int trackdb_obsolete(const char *track, DB_TXN *tid) {
+ int err, n;
+ struct kvp *p;
+ char *alias, **w;
+
+ if((err = gettrackdata(track, 0, &p, 0,
+ GTD_NOALIAS, tid)) == DB_LOCK_DEADLOCK)
+ return err;
+ else if(err == DB_NOTFOUND) return 0;
+ /* compute the alias, if any, and delete it */
+ if(compute_alias(&alias, track, p, tid)) return err;
+ if(alias) {
+ /* if the alias points to some other track then compute_alias won't
+ * return it */
+ if(trackdb_delkey(trackdb_tracksdb, alias, tid))
+ return err;
+ }
+ /* update search.db */
+ w = track_to_words(track, p);
+ for(n = 0; w[n]; ++n)
+ if(trackdb_delkeydata(trackdb_searchdb,
+ w[n], track, tid) == DB_LOCK_DEADLOCK)
+ return err;
+ /* update tags.db */
+ w = parsetags(kvp_get(p, "tags"));
+ for(n = 0; w[n]; ++n)
+ if(trackdb_delkeydata(trackdb_tagsdb,
+ w[n], track, tid) == DB_LOCK_DEADLOCK)
+ return err;
+ reqtracks = 0;
+ /* update tracks.db */
+ if(trackdb_delkey(trackdb_tracksdb, track, tid) == DB_LOCK_DEADLOCK)
+ return err;
+ /* We don't delete the prefs, so they survive temporary outages of the
+ * (possibly virtual) track filesystem */
+ return 0;
+}
+
+/* trackdb_stats() ***********************************************************/
+
+#define H(name) { #name, offsetof(DB_HASH_STAT, name) }
+#define B(name) { #name, offsetof(DB_BTREE_STAT, name) }
+
+static const struct statinfo {
+ const char *name;
+ size_t offset;
+} statinfo_hash[] = {
+ H(hash_magic),
+ H(hash_version),
+ H(hash_nkeys),
+ H(hash_ndata),
+ H(hash_pagesize),
+ H(hash_ffactor),
+ H(hash_buckets),
+ H(hash_free),
+ H(hash_bfree),
+ H(hash_bigpages),
+ H(hash_big_bfree),
+ H(hash_overflows),
+ H(hash_ovfl_free),
+ H(hash_dup),
+ H(hash_dup_free),
+}, statinfo_btree[] = {
+ B(bt_magic),
+ B(bt_version),
+ B(bt_nkeys),
+ B(bt_ndata),
+ B(bt_pagesize),
+ B(bt_minkey),
+ B(bt_re_len),
+ B(bt_re_pad),
+ B(bt_levels),
+ B(bt_int_pg),
+ B(bt_leaf_pg),
+ B(bt_dup_pg),
+ B(bt_over_pg),
+ B(bt_free),
+ B(bt_int_pgfree),
+ B(bt_leaf_pgfree),
+ B(bt_dup_pgfree),
+ B(bt_over_pgfree),
+};
+
+/* look up stats for DB */
+static int get_stats(struct vector *v,
+ DB *database,
+ const struct statinfo *si,
+ size_t nsi,
+ DB_TXN *tid) {
+ void *sp;
+ size_t n;
+ char *str;
+ int err;
+
+ if(database) {
+ switch(err = database->stat(database, tid, &sp, 0)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying database: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error querying database: %s", db_strerror(err));
+ }
+ for(n = 0; n < nsi; ++n) {
+ byte_xasprintf(&str, "%s=%"PRIuMAX, si[n].name,
+ (uintmax_t)*(u_int32_t *)((char *)sp + si[n].offset));
+ vector_append(v, str);
+ }
+ }
+ return 0;
+}
+
+struct search_entry {
+ char *word;
+ int n;
+};
+
+/* find the top COUNT words in the search database */
+static int search_league(struct vector *v, int count, DB_TXN *tid) {
+ struct search_entry *se;
+ DBT k, d;
+ DBC *cursor;
+ int err, n = 0, nse = 0, i;
+ char *word = 0;
+ size_t wl = 0;
+ char *str;
+
+ cursor = trackdb_opencursor(trackdb_searchdb, tid);
+ se = xmalloc(count * sizeof *se);
+ while(!(err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT))) {
+ if(word && wl == k.size && !strncmp(word, k.data, wl))
+ ++n;
+ else {
+#define FINALIZE() do { \
+ if(word && (nse < count || n > se[nse - 1].n)) { \
+ if(nse == count) \
+ i = nse - 1; \
+ else \
+ i = nse++; \
+ while(i > 0 && n > se[i - 1].n) \
+ --i; \
+ memmove(&se[i + 1], &se[i], (nse - i) * sizeof *se); \
+ se[i].word = word; \
+ se[i].n = n; \
+ } \
+} while(0)
+ FINALIZE();
+ word = xstrndup(k.data, wl = k.size);
+ n = 1;
+ }
+ }
+ switch(err) {
+ case DB_NOTFOUND:
+ err = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying search database: %s", db_strerror(err));
+ break;
+ default:
+ fatal(0, "error querying search database: %s", db_strerror(err));
+ }
+ if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+ if(err) return err;
+ FINALIZE();
+ byte_xasprintf(&str, "Top %d search words:", nse);
+ vector_append(v, str);
+ for(i = 0; i < nse; ++i) {
+ byte_xasprintf(&str, "%4d: %5d %s", i + 1, se[i].n, se[i].word);
+ vector_append(v, str);
+ }
+ return 0;
+}
+
+#define SI(what) statinfo_##what, \
+ sizeof statinfo_##what / sizeof (struct statinfo)
+
+/* return a list of database stats */
+char **trackdb_stats(int *nstatsp) {
+ DB_TXN *tid;
+ struct vector v;
+ char *s;
+
+ vector_init(&v);
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ v.nvec = 0;
+ vector_append(&v, (char *)"Tracks database stats:");
+ if(get_stats(&v, trackdb_tracksdb, SI(btree), tid)) goto fail;
+ vector_append(&v, (char *)"");
+ vector_append(&v, (char *)"Search database stats:");
+ if(get_stats(&v, trackdb_searchdb, SI(hash), tid)) goto fail;
+ vector_append(&v, (char *)"");
+ vector_append(&v, (char *)"Prefs database stats:");
+ if(get_stats(&v, trackdb_prefsdb, SI(hash), tid)) goto fail;
+ vector_append(&v, (char *)"");
+ if(search_league(&v, 10, tid)) goto fail;
+ vector_append(&v, (char *)"");
+ vector_append(&v, (char *)"Server stats:");
+ byte_xasprintf(&s, "track lookup cache hits: %lu", cache_files_hits);
+ vector_append(&v, (char *)s);
+ byte_xasprintf(&s, "track lookup cache misses: %lu", cache_files_misses);
+ vector_append(&v, (char *)s);
+ vector_terminate(&v);
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ if(nstatsp) *nstatsp = v.nvec;
+ return v.vec;
+}
+
+/* set a pref (remove if value=0) */
+int trackdb_set(const char *track,
+ const char *name,
+ const char *value) {
+ struct kvp *t, *p, *a;
+ DB_TXN *tid;
+ int err, cmp;
+ char *oldalias, *newalias, **oldtags = 0, **newtags;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if((err = gettrackdata(track, &t, &p, 0,
+ 0, tid)) == DB_LOCK_DEADLOCK)
+ goto fail;
+ if(err == DB_NOTFOUND) break;
+ if(name[0] == '_') {
+ if(kvp_set(&t, name, value))
+ if(trackdb_putdata(trackdb_tracksdb, track, t, tid, 0))
+ goto fail;
+ } else {
+ /* get the old alias name */
+ if(compute_alias(&oldalias, track, p, tid)) goto fail;
+ /* get the old tags */
+ if(!strcmp(name, "tags"))
+ oldtags = parsetags(kvp_get(p, "tags"));
+ /* set the value */
+ if(kvp_set(&p, name, value))
+ if(trackdb_putdata(trackdb_prefsdb, track, p, tid, 0))
+ goto fail;
+ /* compute the new alias name */
+ if((err = compute_alias(&newalias, track, p, tid))) goto fail;
+ /* check whether alias has changed */
+ if(!(oldalias == newalias
+ || (oldalias && newalias && !strcmp(oldalias, newalias)))) {
+ /* adjust alias records to fit change */
+ if(oldalias
+ && trackdb_delkey(trackdb_tracksdb, oldalias, tid)) goto fail;
+ if(newalias) {
+ a = 0;
+ kvp_set(&a, "_alias_for", track);
+ if(trackdb_putdata(trackdb_tracksdb, newalias, a, tid, 0)) goto fail;
+ }
+ }
+ /* check whether tags have changed */
+ if(!strcmp(name, "tags")) {
+ newtags = parsetags(value);
+ while(*oldtags || *newtags) {
+ if(*oldtags && *newtags) {
+ cmp = strcmp(*oldtags, *newtags);
+ if(!cmp) {
+ /* keeping this tag */
+ ++oldtags;
+ ++newtags;
+ } else if(cmp < 0)
+ /* old tag fits into a gap in the new list, so delete old */
+ goto delete_old;
+ else
+ /* new tag fits into a gap in the old list, so insert new */
+ goto insert_new;
+ } else if(*oldtags) {
+ /* we've run out of new tags, so remaining old ones are to be
+ * deleted */
+ delete_old:
+ if(trackdb_delkeydata(trackdb_tagsdb,
+ *oldtags, track, tid) == DB_LOCK_DEADLOCK)
+ goto fail;
+ ++oldtags;
+ } else {
+ /* we've run out of old tags, so remainig new ones are to be
+ * inserted */
+ insert_new:
+ if(register_tag(track, *newtags, tid)) goto fail;
+ ++newtags;
+ }
+ }
+ reqtracks = 0;
+ }
+ }
+ err = 0;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return err == 0 ? 0 : -1;
+}
+
+/* get a pref */
+const char *trackdb_get(const char *track,
+ const char *name) {
+ return kvp_get(trackdb_get_all(track), name);
+}
+
+/* get all prefs as a 0-terminated array */
+struct kvp *trackdb_get_all(const char *track) {
+ struct kvp *t, *p, **pp;
+ DB_TXN *tid;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+ goto fail;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ for(pp = &p; *pp; pp = &(*pp)->next)
+ ;
+ *pp = t;
+ return p;
+}
+
+/* resolve alias */
+const char *trackdb_resolve(const char *track) {
+ DB_TXN *tid;
+ const char *actual;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(gettrackdata(track, 0, 0, &actual, 0, tid) == DB_LOCK_DEADLOCK)
+ goto fail;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return actual;
+}
+
+int trackdb_isalias(const char *track) {
+ const char *actual = trackdb_resolve(track);
+
+ return strcmp(actual, track);
+}
+
+/* test whether a track exists (perhaps an alias) */
+int trackdb_exists(const char *track) {
+ DB_TXN *tid;
+ int err;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ /* unusually, here we want the return value */
+ if((err = gettrackdata(track, 0, 0, 0, 0, tid)) == DB_LOCK_DEADLOCK)
+ goto fail;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return (err == 0);
+}
+
+/* return the list of tags */
+char **trackdb_alltags(void) {
+ DB_TXN *tid;
+ int err;
+ char **taglist;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ err = trackdb_alltags_tid(tid, &taglist);
+ if(!err) break;
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return taglist;
+}
+
+static int trackdb_alltags_tid(DB_TXN *tid, char ***taglistp) {
+ struct vector v;
+ DBC *c;
+ DBT k, d;
+ int err;
+
+ vector_init(&v);
+ c = trackdb_opencursor(trackdb_tagsdb, tid);
+ memset(&k, 0, sizeof k);
+ while(!(err = c->c_get(c, &k, prepare_data(&d), DB_NEXT_NODUP)))
+ vector_append(&v, xstrndup(k.data, k.size));
+ switch(err) {
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ return err;
+ default:
+ fatal(0, "c->c_get: %s", db_strerror(err));
+ }
+ if((err = trackdb_closecursor(c))) return err;
+ vector_terminate(&v);
+ *taglistp = v.vec;
+ return 0;
+}
+
+/* return 1 iff sorted tag lists A and B have at least one member in common */
+static int tag_intersection(char **a, char **b) {
+ int cmp;
+
+ /* Same sort of logic as trackdb_set() above */
+ while(*a && *b) {
+ if(!(cmp = strcmp(*a, *b))) return 1;
+ else if(cmp < 0) ++a;
+ else ++b;
+ }
+ return 0;
+}
+
+/* Check whether a track is suitable for random play. Returns 0 if it is,
+ * DB_NOTFOUND if it or DB_LOCK_DEADLOCK. */
+static int check_suitable(const char *track,
+ DB_TXN *tid,
+ char **required_tags,
+ char **prohibited_tags) {
+ char **track_tags;
+ time_t last, now;
+ struct kvp *p, *t;
+ const char *pick_at_random, *played_time;
+
+ /* don't pick aliases - only pick the canonical form */
+ if(gettrackdata(track, &t, &p, 0, 0, tid) == DB_LOCK_DEADLOCK)
+ return DB_LOCK_DEADLOCK;
+ if(kvp_get(t, "_alias_for"))
+ return DB_NOTFOUND;
+ /* check that random play is not suppressed for this track */
+ if((pick_at_random = kvp_get(p, "pick_at_random"))
+ && !strcmp(pick_at_random, "0"))
+ return DB_NOTFOUND;
+ /* don't pick a track that's been played in the last 8 hours */
+ if((played_time = kvp_get(p, "played_time"))) {
+ last = atoll(played_time);
+ now = time(0);
+ if(now < last + 8 * 3600) /* TODO configurable */
+ return DB_NOTFOUND;
+ }
+ track_tags = parsetags(kvp_get(p, "tags"));
+ /* check that no prohibited tag is present for this track */
+ if(prohibited_tags && tag_intersection(track_tags, prohibited_tags))
+ return DB_NOTFOUND;
+ /* check that at least one required tags is present for this track */
+ if(*required_tags && !tag_intersection(track_tags, required_tags))
+ return DB_NOTFOUND;
+ return 0;
+}
+
+/* attempt to pick a random non-alias track */
+const char *trackdb_random(int tries) {
+ DBT key, data;
+ DB_BTREE_STAT *sp;
+ int err, n;
+ DB_TXN *tid;
+ const char *track, *candidate;
+ db_recno_t r;
+ const char *tags;
+ char **required_tags, **prohibited_tags, **tp;
+ hash *h;
+ DBC *c = 0;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if((err = trackdb_get_global_tid("required-tags", tid, &tags)))
+ goto fail;
+ required_tags = parsetags(tags);
+ if((err = trackdb_get_global_tid("prohibited-tags", tid, &tags)))
+ goto fail;
+ prohibited_tags = parsetags(tags);
+ track = 0;
+ if(*required_tags) {
+ /* Bung all the suitable tracks into a hash and convert to a list of keys
+ * (to eliminate duplicates). We cache this list since it is possible
+ * that it will be very large. */
+ if(!reqtracks) {
+ h = hash_new(0);
+ for(tp = required_tags; *tp; ++tp) {
+ c = trackdb_opencursor(trackdb_tagsdb, tid);
+ memset(&key, 0, sizeof key);
+ key.data = *tp;
+ key.size = strlen(*tp);
+ n = 0;
+ err = c->c_get(c, &key, prepare_data(&data), DB_SET);
+ while(err == 0) {
+ hash_add(h, xstrndup(data.data, data.size), 0,
+ HASH_INSERT_OR_REPLACE);
+ ++n;
+ err = c->c_get(c, &key, prepare_data(&data), DB_NEXT_DUP);
+ }
+ switch(err) {
+ case 0:
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ goto fail;
+ default:
+ fatal(0, "error querying tags.db: %s", db_strerror(err));
+ }
+ trackdb_closecursor(c);
+ c = 0;
+ if(!n)
+ error(0, "required tag %s does not match any tracks", *tp);
+ }
+ nreqtracks = hash_count(h);
+ reqtracks = hash_keys(h);
+ }
+ while(nreqtracks && !track && tries-- > 0) {
+ r = (rand() * (double)nreqtracks / (RAND_MAX + 1.0));
+ candidate = reqtracks[r];
+ switch(check_suitable(candidate, tid,
+ required_tags, prohibited_tags)) {
+ case 0:
+ track = candidate;
+ break;
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ goto fail;
+ }
+ }
+ } else {
+ /* No required tags. We pick random record numbers in the database
+ * instead. */
+ switch(err = trackdb_tracksdb->stat(trackdb_tracksdb, tid, &sp,
+ DB_RECORDCOUNT)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying tracks.db: %s", db_strerror(err));
+ goto fail;
+ default:
+ fatal(0, "error querying tracks.db: %s", db_strerror(err));
+ }
+ if(!sp->bt_nkeys)
+ error(0, "cannot pick tracks at random from an empty database");
+ while(sp->bt_nkeys && !track && tries-- > 0) {
+ /* record numbers count from 1 upwards */
+ r = 1 + (rand() * (double)sp->bt_nkeys / (RAND_MAX + 1.0));
+ memset(&key, sizeof key, 0);
+ key.flags = DB_DBT_MALLOC;
+ key.size = sizeof r;
+ key.data = &r;
+ switch(err = trackdb_tracksdb->get(trackdb_tracksdb, tid, &key, prepare_data(&data),
+ DB_SET_RECNO)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying tracks.db: %s", db_strerror(err));
+ goto fail;
+ default:
+ fatal(0, "error querying tracks.db: %s", db_strerror(err));
+ }
+ candidate = xstrndup(key.data, key.size);
+ switch(check_suitable(candidate, tid,
+ required_tags, prohibited_tags)) {
+ case 0:
+ track = candidate;
+ break;
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ goto fail;
+ }
+ }
+ }
+ break;
+fail:
+ trackdb_closecursor(c);
+ c = 0;
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ if(!track)
+ error(0, "could not pick a random track");
+ return track;
+}
+
+/* get a track name given the prefs. Set *used_db to 1 if we got the answer
+ * from the prefs. */
+static const char *getpart(const char *track,
+ const char *context,
+ const char *part,
+ const struct kvp *p,
+ int *used_db) {
+ const char *result;
+ char *pref;
+
+ byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+ if((result = kvp_get(p, pref)))
+ *used_db = 1;
+ else
+ result = trackname_part(track, context, part);
+ assert(result != 0);
+ return result;
+}
+
+/* get a track name part, like trackname_part(), but taking the database into
+ * account. */
+const char *trackdb_getpart(const char *track,
+ const char *context,
+ const char *part) {
+ struct kvp *p;
+ DB_TXN *tid;
+ char *pref;
+ const char *actual;
+ int used_db, err;
+
+ /* construct the full pref */
+ byte_xasprintf(&pref, "trackname_%s_%s", context, part);
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if((err = gettrackdata(track, 0, &p, &actual, 0, tid)) == DB_LOCK_DEADLOCK)
+ goto fail;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return getpart(actual, context, part, p, &used_db);
+}
+
+/* get the raw path name for @track@ (might be an alias) */
+const char *trackdb_rawpath(const char *track) {
+ DB_TXN *tid;
+ struct kvp *t;
+ const char *path;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(gettrackdata(track, &t, 0, 0, 0, tid) == DB_LOCK_DEADLOCK)
+ goto fail;
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ if(!(path = kvp_get(t, "_path"))) path = track;
+ return path;
+}
+
+/* trackdb_list **************************************************************/
+
+/* this is incredibly ugly, sorry, perhaps it will be rewritten to be actually
+ * readable at some point */
+
+/* return true if the basename of TRACK[0..TL-1], as defined by DL, matches RE.
+ * If RE is a null pointer then it matches everything. */
+static int track_matches(size_t dl, const char *track, size_t tl,
+ const pcre *re) {
+ int ovec[3], rc;
+
+ if(!re)
+ return 1;
+ track += dl + 1;
+ tl -= (dl + 1);
+ switch(rc = pcre_exec(re, 0, track, tl, 0, 0, ovec, 3)) {
+ case PCRE_ERROR_NOMATCH: return 0;
+ default:
+ if(rc < 0) {
+ error(0, "pcre_exec returned %d, subject '%s'", rc, track);
+ return 0;
+ }
+ return 1;
+ }
+}
+
+static int do_list(struct vector *v, const char *dir,
+ enum trackdb_listable what, const pcre *re, DB_TXN *tid) {
+ DBC *cursor;
+ DBT k, d;
+ size_t dl;
+ char *ptr;
+ int err;
+ size_t l, last_dir_len = 0;
+ char *last_dir = 0, *track, *alias;
+ struct kvp *p;
+
+ dl = strlen(dir);
+ cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+ make_key(&k, dir);
+ prepare_data(&d);
+ /* find the first key >= dir */
+ err = cursor->c_get(cursor, &k, &d, DB_SET_RANGE);
+ /* keep going while we're dealing with <dir/anything> */
+ while(err == 0
+ && k.size > dl
+ && ((char *)k.data)[dl] == '/'
+ && !memcmp(k.data, dir, dl)) {
+ ptr = memchr((char *)k.data + dl + 1, '/', k.size - (dl + 1));
+ if(ptr) {
+ /* we have <dir/component/anything>, so <dir/component> is a directory */
+ l = ptr - (char *)k.data;
+ if(what & trackdb_directories)
+ if(!(last_dir
+ && l == last_dir_len
+ && !memcmp(last_dir, k.data, l))) {
+ last_dir = xstrndup(k.data, last_dir_len = l);
+ if(track_matches(dl, k.data, l, re))
+ vector_append(v, last_dir);
+ }
+ } else {
+ /* found a plain file */
+ if((what & trackdb_files)) {
+ track = xstrndup(k.data, k.size);
+ if((err = trackdb_getdata(trackdb_prefsdb,
+ track, &p, tid)) == DB_LOCK_DEADLOCK)
+ goto deadlocked;
+ /* if this file has an alias in the same directory then we skip it */
+ if((err = compute_alias(&alias, track, p, tid)))
+ goto deadlocked;
+ if(!(alias && !strcmp(d_dirname(alias), d_dirname(track))))
+ if(track_matches(dl, k.data, k.size, re))
+ vector_append(v, track);
+ }
+ }
+ err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+ }
+ switch(err) {
+ case 0:
+ break;
+ case DB_NOTFOUND:
+ err = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying database: %s", db_strerror(err));
+ break;
+ default:
+ fatal(0, "error querying database: %s", db_strerror(err));
+ }
+deadlocked:
+ if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+ return err;
+}
+
+/* return the directories or files below @dir@ */
+char **trackdb_list(const char *dir, int *np, enum trackdb_listable what,
+ const pcre *re) {
+ DB_TXN *tid;
+ int n;
+ struct vector v;
+
+ vector_init(&v);
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ v.nvec = 0;
+ if(dir) {
+ if(do_list(&v, dir, what, re, tid))
+ goto fail;
+ } else {
+ for(n = 0; n < config->collection.n; ++n)
+ if(do_list(&v, config->collection.s[n].root, what, re, tid))
+ goto fail;
+ }
+ break;
+fail:
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ vector_terminate(&v);
+ if(np)
+ *np = v.nvec;
+ return v.vec;
+}
+
+/* If S is tag:something, return something. Else return 0. */
+static const char *checktag(const char *s) {
+ if(!strncmp(s, "tag:", 4))
+ return s + 4;
+ else
+ return 0;
+}
+
+/* return a list of tracks containing all of the words given. If you
+ * ask for only stopwords you get no tracks. */
+char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
+ const char **w, *best = 0, *tag;
+ char **twords, **tags;
+ int i, j, n, err, what;
+ DBC *cursor = 0;
+ DBT k, d;
+ struct vector u, v;
+ DB_TXN *tid;
+ struct kvp *p;
+ int ntags = 0;
+ DB *db;
+ const char *dbname;
+
+ *ntracks = 0; /* for early returns */
+ /* casefold all the words */
+ w = xmalloc(nwordlist * sizeof (char *));
+ for(n = 0; n < nwordlist; ++n) {
+ w[n] = casefold(wordlist[n]);
+ if(checktag(w[n])) ++ntags; /* count up tags */
+ }
+ /* find the longest non-stopword */
+ for(n = 0; n < nwordlist; ++n)
+ if(!stopword(w[n]) && !checktag(w[n]))
+ if(!best || strlen(w[n]) > strlen(best))
+ best = w[n];
+ /* TODO: we should at least in principal be able to identify the word or tag
+ * with the least matches in log time, and choose that as our primary search
+ * term. */
+ if(ntags && !best) {
+ /* Only tags are listed. We limit to the first and narrow down with the
+ * rest. */
+ best = checktag(w[0]);
+ db = trackdb_tagsdb;
+ dbname = "tags";
+ } else if(best) {
+ /* We can limit to some word. */
+ db = trackdb_searchdb;
+ dbname = "search";
+ } else {
+ /* Only stopwords */
+ return 0;
+ }
+ vector_init(&u);
+ vector_init(&v);
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ /* find all the tracks that have that word */
+ make_key(&k, best);
+ prepare_data(&d);
+ what = DB_SET;
+ v.nvec = 0;
+ cursor = trackdb_opencursor(db, tid);
+ while(!(err = cursor->c_get(cursor, &k, &d, what))) {
+ vector_append(&v, xstrndup(d.data, d.size));
+ what = DB_NEXT_DUP;
+ }
+ switch(err) {
+ case DB_NOTFOUND:
+ err = 0;
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error querying %s database: %s", dbname, db_strerror(err));
+ break;
+ default:
+ fatal(0, "error querying %s database: %s", dbname, db_strerror(err));
+ }
+ if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
+ cursor = 0;
+ /* do a naive search over that (hopefuly fairly small) list of tracks */
+ u.nvec = 0;
+ for(n = 0; n < v.nvec; ++n) {
+ if((err = gettrackdata(v.vec[n], 0, &p, 0, 0, tid) == DB_LOCK_DEADLOCK))
+ goto fail;
+ else if(err) {
+ error(0, "track %s unexpected error: %s", v.vec[n], db_strerror(err));
+ continue;
+ }
+ twords = track_to_words(v.vec[n], p);
+ tags = parsetags(kvp_get(p, "tags"));
+ for(i = 0; i < nwordlist; ++i) {
+ if((tag = checktag(w[i]))) {
+ /* Track must have this tag */
+ for(j = 0; tags[j]; ++j)
+ if(!strcmp(tag, tags[j])) break; /* tag found */
+ if(!tags[j]) break; /* tag not found */
+ } else {
+ /* Track must contain this word */
+ for(j = 0; twords[j]; ++j)
+ if(!strcmp(w[i], twords[j])) break; /* word found */
+ if(!twords[j]) break; /* word not found */
+ }
+ }
+ if(i >= nwordlist) /* all words found */
+ vector_append(&u, v.vec[n]);
+ }
+ break;
+ fail:
+ trackdb_closecursor(cursor);
+ cursor = 0;
+ trackdb_abort_transaction(tid);
+ info("retrying search");
+ }
+ trackdb_commit_transaction(tid);
+ vector_terminate(&u);
+ if(ntracks)
+ *ntracks = u.nvec;
+ return u.vec;
+}
+
+/* trackdb_scan **************************************************************/
+
+int trackdb_scan(const char *root,
+ int (*callback)(const char *track,
+ struct kvp *data,
+ void *u,
+ DB_TXN *tid),
+ void *u,
+ DB_TXN *tid) {
+ DBC *cursor;
+ DBT k, d;
+ size_t root_len = strlen(root);
+ int err;
+ struct kvp *data;
+
+ cursor = trackdb_opencursor(trackdb_tracksdb, tid);
+ err = cursor->c_get(cursor, make_key(&k, root), prepare_data(&d),
+ DB_SET_RANGE);
+ while(!err) {
+ if(k.size > root_len
+ && !strncmp(k.data, root, root_len)
+ && ((char *)k.data)[root_len] == '/') {
+ data = kvp_urldecode(d.data, d.size);
+ if(kvp_get(data, "_path"))
+ if((err = callback(xstrndup(k.data, k.size), data, u, tid)))
+ break;
+ err = cursor->c_get(cursor, &k, &d, DB_NEXT);
+ } else
+ break;
+ }
+ trackdb_closecursor(cursor);
+ switch(err) {
+ case EINTR:
+ return err;
+ case 0:
+ case DB_NOTFOUND:
+ return 0;
+ case DB_LOCK_DEADLOCK:
+ error(0, "c->c_get: %s", db_strerror(err));
+ return err;
+ default:
+ fatal(0, "c->c_get: %s", db_strerror(err));
+ }
+}
+
+/* trackdb_rescan ************************************************************/
+
+/* called when the rescanner terminates */
+static int reap_rescan(ev_source attribute((unused)) *ev,
+ pid_t pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void attribute((unused)) *u) {
+ if(pid == rescan_pid) rescan_pid = -1;
+ if(status)
+ error(0, "disorderd-rescan: %s", wstat(status));
+ else
+ D(("disorderd-rescan terminate: %s", wstat(status)));
+ /* Our cache of file lookups is out of date now */
+ cache_clean(&cache_files_type);
+ return 0;
+}
+
+void trackdb_rescan(ev_source *ev) {
+ if(rescan_pid != -1) {
+ error(0, "rescan already underway");
+ return;
+ }
+ rescan_pid = subprogram(ev, RESCAN);
+ ev_child(ev, rescan_pid, 0, reap_rescan, 0);
+ D(("started rescanner"));
+
+}
+
+int trackdb_rescan_cancel(void) {
+ if(rescan_pid == -1) return 0;
+ if(kill(rescan_pid, SIGTERM) < 0)
+ fatal(errno, "error killing rescanner");
+ rescan_pid = -1;
+ return 1;
+}
+
+/* global prefs **************************************************************/
+
+void trackdb_set_global(const char *name,
+ const char *value,
+ const char *who) {
+ DB_TXN *tid;
+ DBT k, d;
+ int err;
+ int state;
+
+ memset(&k, 0, sizeof k);
+ memset(&d, 0, sizeof d);
+ k.data = (void *)name;
+ k.size = strlen(name);
+ if(value) {
+ d.data = (void *)value;
+ d.size = strlen(value);
+ }
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(value)
+ err = trackdb_globaldb->put(trackdb_globaldb, tid, &k, &d, 0);
+ else
+ err = trackdb_globaldb->del(trackdb_globaldb, tid, &k, 0);
+ if(!err || err == DB_NOTFOUND) break;
+ if(err != DB_LOCK_DEADLOCK)
+ fatal(0, "error updating database: %s", db_strerror(err));
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ /* log important state changes */
+ if(!strcmp(name, "playing")) {
+ state = !value || !strcmp(value, "yes");
+ info("playing %s by %s",
+ state ? "enabled" : "disabled",
+ who ? who : "-");
+ eventlog("state", state ? "enable_play" : "disable_play", (char *)0);
+ }
+ if(!strcmp(name, "random-play")) {
+ state = !value || !strcmp(value, "yes");
+ info("random play %s by %s",
+ state ? "enabled" : "disabled",
+ who ? who : "-");
+ eventlog("state", state ? "enable_random" : "disable_random", (char *)0);
+ }
+ if(!strcmp(name, "required-tags"))
+ reqtracks = 0;
+}
+
+const char *trackdb_get_global(const char *name) {
+ DB_TXN *tid;
+ int err;
+ const char *r;
+
+ for(;;) {
+ tid = trackdb_begin_transaction();
+ if(!(err = trackdb_get_global_tid(name, tid, &r)))
+ break;
+ trackdb_abort_transaction(tid);
+ }
+ trackdb_commit_transaction(tid);
+ return r;
+}
+
+static int trackdb_get_global_tid(const char *name,
+ DB_TXN *tid,
+ const char **rp) {
+ DBT k, d;
+ int err;
+
+ memset(&k, 0, sizeof k);
+ k.data = (void *)name;
+ k.size = strlen(name);
+ switch(err = trackdb_globaldb->get(trackdb_globaldb, tid, &k,
+ prepare_data(&d), 0)) {
+ case 0:
+ *rp = xstrndup(d.data, d.size);
+ return 0;
+ case DB_NOTFOUND:
+ *rp = 0;
+ return 0;
+ case DB_LOCK_DEADLOCK:
+ return err;
+ default:
+ fatal(0, "error updating database: %s", db_strerror(err));
+ }
+}
+
+/* tidying up ****************************************************************/
+
+void trackdb_gc(void) {
+ int err;
+ char **logfiles;
+
+ if((err = trackdb_env->txn_checkpoint(trackdb_env,
+ config->checkpoint_kbyte,
+ config->checkpoint_min,
+ 0)))
+ fatal(0, "trackdb_env->txn_checkpoint: %s", db_strerror(err));
+ if((err = trackdb_env->log_archive(trackdb_env, &logfiles, DB_ARCH_REMOVE)))
+ fatal(0, "trackdb_env->log_archive: %s", db_strerror(err));
+ /* This makes catastrophic recovery impossible. However, the user can still
+ * preserve the important data by using disorder-dump to snapshot their
+ * prefs, and later to restore it. This is likely to have much small
+ * long-term storage requirements than record the db logfiles. */
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
+/* arch-tag:/CBQ6uebrasgkefvjc+fHQ */
--- /dev/null
+/*
+ * 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 */
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <locale.h>
+#include <errno.h>
+#include <string.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "trackname.h"
+#include "mem.h"
+#include "charset.h"
+#include "defs.h"
+
+static const struct option options[] = {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "config", required_argument, 0, 'c' },
+ { "debug", no_argument, 0, 'd' },
+ { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+ xprintf("Usage:\n"
+ " trackname [OPTIONS] TRACK CONTEXT PART\n"
+ "Options:\n"
+ " --help, -h Display usage message\n"
+ " --version, -V Display version number\n"
+ " --config PATH, -c PATH Set configuration file\n"
+ " --debug, -d Turn on debugging\n");
+ xfclose(stdout);
+ exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+ xprintf("disorder version %s\n", disorder_version_string);
+ xfclose(stdout);
+ exit(0);
+}
+
+int main(int argc, char **argv) {
+ int n;
+ const char *s;
+
+ mem_init(0);
+ if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+ while((n = getopt_long(argc, argv, "hVc:d", options, 0)) >= 0) {
+ switch(n) {
+ case 'h': help();
+ case 'V': version();
+ case 'c': configfile = optarg; break;
+ case 'd': debugging = 1; break;
+ default: fatal(0, "invalid option");
+ }
+ }
+ if(argc - optind < 3) fatal(0, "not enough arguments");
+ if(argc - optind > 3) fatal(0, "too many arguments");
+ if(config_read()) fatal(0, "cannot read configuration");
+ s = trackname_part(argv[optind], argv[optind+1], argv[optind+2]);
+ if(!s) fatal(0, "trackname_part returned NULL");
+ xprintf("%s\n", nullcheck(utf82mb(s)));
+ if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+End:
+*/
+/* arch-tag:DiPOUeRLYf0fgoUqzejUDA */
--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:about.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:about.title@</h1>
+
+ <h2>Copyright</h2>
+
+ <p><a
+ href="http://www.greenend.org.uk/rjk/disorder/">DisOrder
+ version @version@</a> - select and play digital
+ audio files</p>
+
+ <p>Copyright © 2003, 2004, 2005, 2006 Richard Kettlewell</p>
+
+ <p>Portions extracted from
+ <a href="http://mpg321.sourceforge.net/">MPG321</a>,
+ Copyright © 2001 Joe Drew,
+ Copyright © 2000-2001 Robert Leslie</p>
+
+ <p>This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of the
+ License, or (at your option) any later version.</p>
+
+ <p>This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.</p>
+
+ <p>You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ USA</p>
+
+ <h2>Server Statistics</h2>
+
+@stats@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:bc42c6969d7ea0909aa52460cfd633ae -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:choose.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:choose.title@</h1>
+
+ @if{@ne{@arg:directory@}{}@}{
+ <p class=directoryname>@navigate{@arg:directory@}{/<a
+ class=directory
+ href="@url@/?action=choose&directory=@urlquote{@fullname@}@&nonce=@nonce@">@basename@</a>}@:</p>
+ }@
+
+ @if{@isdirectories@}{
+ <div class=directories>
+ <p class=directories>
+ @label:choose.directories@
+ </p>
+ @choose{directories}{
+ <p class=directory>
+ <a class=directory href="@url@/?action=choose&directory=@urlquote{@file@}@&nonce=@nonce@">
+ @transform{@file@}{dir}{display}@
+ </a>
+ </p>
+ }@
+ </div>
+ }@
+ @if{@isfiles@}{
+ <div class=files>
+ <p class=files>
+ @label:choose.files@
+ </p>
+ @choose{files}{
+ <p class=file>
+ <a class=imgprefs href="@url@/?action=prefs&0_file=@urlquote{@resolve{@file@}@}@&nonce=@nonce@">
+ <img class=button src="@label:images.edit@"
+ title="@label:choose.prefsverbose@" alt="@label:choose.prefs@">
+ </a>
+ <a class=file href="@url@/?action=play&file=@urlquote{@file@}@&back=@urlquote{@thisurl@}@&nonce=@nonce@">
+ @transform{@file@}{track}{display}@
+ </a>
+ @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
+ @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
+ </p>
+ }@
+ </div>
+ <p class=allfiles>
+ <a class=imgprefs href="@url@/?action=prefs&directory=@urlquote{@arg:directory@}@&nonce=@nonce@&back=@urlquote{@thisurl@}@">
+ <img class=button src="@label:images.edit@"
+ title="@label:choose.allprefsverbose@" alt="@label:choose.allprefs@">
+ </a>
+ <a class=allfiles href="@url@/?action=play&directory=@urlquote{@arg:directory@}@&nonce=@nonce@&back=@urlquote{@thisurl@}@">
+ @label:choose.playall@
+ </a>
+ </p>
+ }@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:f62f829c03df666f4587b87b08c5e3bf -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:choose.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:choose.title@</h1>
+
+ <p class=choosealpha>
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?a">A</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?b">B</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?c">C</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?d">D</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?e">E</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?f">F</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?g">G</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?h">H</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?i">I</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?j">J</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?k">K</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?l">L</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?m">M</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?n">N</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?o">O</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?p">P</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?q">Q</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?r">R</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?s">S</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(?!the [^t])t">T</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?u">U</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?v">V</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?w">W</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?x">X</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?y">Y</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^(the )?z">Z</a> |
+ <a class=choosealpha href="@url@?action=choose&regexp=^[^a-z]">*</a>
+ </p>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:be6e8dfa8452199ee4f5aa988881ec03 -->
--- /dev/null
+<p class=credits><a
+href="http://www.greenend.org.uk/rjk/disorder/"
+title="DisOrder web site">DisOrder
+version @version@</a> © 2003, 2004, 2005, 2006 Richard Kettlewell</p>
+@@
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:625eff5d601a737c8dfb1bf2ff4320d4 -->
--- /dev/null
+/* default colors */
+body {
+ color: black;
+ background-color: white
+}
+
+/* general link colors */
+a:link {
+ color: blue
+}
+
+a:visited {
+ color: blue
+}
+
+a:active {
+ color: red
+}
+
+h1.title {
+ font-family: sans-serif;
+ font-weight: bold;
+ text-align: center;
+ font-size: 18pt
+}
+
+/* playing and recent *********************************************************/
+
+/* table of current and future tracks */
+table.playing {
+ width: 100%; /* use the full available width */
+ border-spacing: 0 /* no unsightly gaps between cells */
+}
+
+/* table of recently played tracks */
+table.recent {
+ width: 100%; /* use the full available width */
+ border-spacing: 0 /* no unsightly gaps between cells */
+}
+
+/* titles in tables */
+th {
+ text-align: left
+}
+
+/* ordinary cells in tables */
+td {
+ vertical-align: center
+}
+
+/* the headings <tr> of the table */
+tr.headings {
+ background-color: black;
+ color: white
+}
+
+/* The 'now playing' heading */
+tr.nowplaying {
+}
+
+td.nowplaying {
+ background-color: #d0d0d0;
+ font-weight: bold;
+ text-align: center
+}
+
+/* the currently playing track */
+tr.playing {
+ background-color: #e0ffe0 /* pastel green */
+}
+
+/* the "next" heading */
+tr.next {
+}
+
+td.next {
+ background-color: #d0d0d0;
+ font-weight: bold;
+ text-align: center
+}
+
+/* even-numbered rows */
+tr.even {
+ background-color: #ffecec /* faint pastel red */
+}
+
+/* odd-numbered rows */
+tr.odd {
+ background-color: #ffffff /* white */
+}
+
+/* column titles */
+th.when {
+}
+
+th.who {
+}
+
+th.artist {
+}
+
+th.album {
+}
+
+th.title {
+}
+
+th.length {
+ text-align: right
+}
+
+th.button {
+}
+
+/* individual cells */
+
+td.when {
+}
+
+td.who {
+}
+
+td.artist {
+}
+
+td.album {
+}
+
+td.title {
+}
+
+td.length {
+ text-align: right;
+ font-size: small /* because otherwise visually intrusive */
+}
+
+td.button {
+ text-align: center;
+ padding: 1px;
+ border-color: black;
+ border-width: 1px;
+ border-style: solid;
+ background-color: #c0c0c0;
+ color: #000000
+}
+
+p.mgmt,form.volume {
+ display: inline
+}
+
+/* choose *********************************************************************/
+
+/* first letter choice */
+p.choosealpha {
+ text-align: center
+}
+
+/* containing directory */
+p.directoryname {
+ font-weight: bold
+}
+
+/* directories */
+div.directories {
+}
+
+/* heading for directories */
+p.directories {
+ font-weight: bold
+}
+
+/* one directory */
+p.directory {
+ margin-left: 1em
+}
+
+a.directory {
+}
+
+a.directory:link {
+ color: black
+}
+
+a.directory:visited {
+ color: black
+}
+
+a.directory:active {
+ color: red
+}
+
+/* files */
+div.files {
+}
+
+/* heading for files */
+p.files {
+ font-weight: bold
+}
+
+/* one file */
+p.file {
+ margin-left: 1em
+}
+
+a.file {
+ text-decoration: none;
+}
+
+a.file:link {
+ color: black
+}
+
+a.file:visited {
+ color: black
+}
+
+a.file:active {
+ color: red
+}
+
+/* buttons ********************************************************************/
+
+/* a.allfiles turns up in track choice
+ * button is used e.g. in searching
+ */
+a.allfiles,a.prefs,button,span.button {
+ padding: 1px;
+ border-color: #fefefe;
+ border-style: inset;
+ background-color: #c0c0c0;
+ color: #000000;
+ text-decoration: none;
+ font-family: sans-serif
+}
+
+a.button {
+ text-decoration: none;
+ font-family: sans-serif
+}
+
+a.button:link,a.button:visited,a.allfiles:link,a.allfiles:visited {
+ background-color: #c0c0c0;
+ color: #000000
+}
+
+a.button:active,a.allfiles:active,button:active {
+ background-color: #c0c0c0;
+ color: #ffffff
+}
+
+img.button {
+ border-width: 0
+}
+
+/* searching ******************************************************************/
+
+div.searchresults {
+}
+
+div.search_artist {
+}
+
+p.search_artist {
+}
+
+span.search_artist {
+ font-weight: bold
+}
+
+div.search_album {
+ margin-left: 1em
+}
+
+p.search_album {
+}
+
+span.search_album {
+}
+
+div.search_title {
+ margin-left: 1em
+}
+
+p.search_title {
+ margin-top: 0;
+ margin-bottom: 0
+}
+
+a.search_title {
+ text-decoration: underline
+}
+
+a.search_title:link {
+ color: black
+}
+
+a.search_title:visited {
+ color: black
+}
+
+a.search_title:active {
+ color: red
+}
+
+/* sidebar ********************************************************************/
+
+div#sidebar {
+ margin: 1em;
+ position: absolute;
+ width: 10em;
+ top: 0;
+ right: auto;
+ left: 0;
+}
+
+div#content {
+ position: absolute;
+ width: auto;
+ top: 0;
+ right: 1em;
+ left: 6em;
+}
+
+.sidebarlink {
+ font-family: sans-serif
+}
+
+a.sidebarlink {
+ text-decoration: none;
+ color: black
+}
+
+a.sidebarlink:visited {
+ color: black
+}
+
+a.sidebarlink:active {
+ color: red
+}
+
+a.sidebarlink:visited {
+ color: black
+}
+
+/* topbar *********************************************************************/
+
+p.menubar {
+ word-spacing: 1em
+}
+
+.activemenu {
+ font-family: sans-serif;
+ font-weight: bold;
+ font-size: 14pt
+}
+
+.inactivemenu {
+ font-family: sans-serif;
+ font-weight: bold;
+ font-size: 14pt
+}
+
+a.inactivemenu,a.inactivemenu:visited {
+ text-decoration: none;
+ color: black
+}
+
+a.activemenu,a.activemenu:visited {
+ text-decoration: none;
+ color: red
+}
+
+a.activemenu:active,a.inactivemenu:active {
+ text-decoration: none;
+ color: red
+}
+
+/* prefs **********************************************************************/
+
+p.prefs_new,p.prefs_head {
+ font-weight: bold
+}
+
+table.prefs {
+ border-spacing: 0
+}
+
+tr.prefs_headings {
+ background-color: black;
+ color: white
+}
+
+th.prefs_name {
+}
+
+th.prefs_value {
+}
+
+td.prefs_name {
+ vertical-align: top
+}
+
+td.prefs_value {
+ vertical-align: top
+}
+
+td.prefs_delete {
+ vertical-align: top
+}
+
+input.prefs_name,input.prefs_value {
+ font-family: monospace
+}
+
+/* help ***********************************************************************/
+
+.helpbuttons,.helpprefs,.helpcontexts {
+ margin-left: 2em;
+ margin-right: 2em;
+ vertical-align: top
+}
+
+.helpsection {
+ margin-left: 1em;
+}
+
+.helppref {
+ font-family: monospace
+}
+
+.helpprefbit {
+ font-family: monospace;
+ font-style: italic
+}
+
+.helpcontext {
+ font-weight: bold
+}
+
+/* volume *********************************************************************/
+
+p.volume {
+ text-align: center
+}
+
+/* miscelleanous **************************************************************/
+
+/* credits */
+p.credits {
+ font-size: small; /* because visually intrusive */
+ text-align: right
+}
+/*
+This file is part of DisOrder.
+Copyright (C) 2003, 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+*/
+/* arch-tag:tlWcgChjjqNVaC/UmG9Zaw */
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:error.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:error.title@</h1>
+
+ <p>@label{error.@label:error@}@</p>
+
+ <p>@label:error.generic@</p>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:5UQWniMns+CWWMBBF0qoUg -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include{stdhead}@
+ <title>@label{help.title}@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label{help.title}@</h1>
+
+ <h2 class=sidebarlink><a name=playing>Playing</a></h2>
+
+ <div class=helpsection>
+
+ <p>This screen displays the currently playing track (if there is one) and
+ lists all the tracks in the queue (the track that will be played soonest
+ being listed first.) Where possible, estimated start times are
+ given.</p>
+
+ <p>Each track has a <img class=button
+ src="@label:images.scratch@"
+ title="@label:playing.scratch@"
+ alt="@label:playing.scratch@"> button next to it. For the
+ currently playing track this can be used to stop playing the
+ track before it has finished. For a track in the queue it
+ removes the track from the queue.</p>
+
+ <p>Depending on the server configuration, you may be able to do
+ this for any track, or only for tracks you submitted or that were
+ randomly picked. See the "restrict" option in <a
+ href="@url@?action=disorder_config.5">disorder_config(5)</a> for more
+ details.</p>
+
+ <p>Artist and album names are hyperlinks to the relevant locations
+ in the <a href="#choose">Choose</a> screen (see below).</p>
+
+ </div>
+
+ <h2 class=sidebarlink><a name=manage>Manage</a></h2>
+
+ <div class=helpsection>
+
+ <p>This screen is almost identical to <a
+ href="#playing">Playing</a> except that it includes extra
+ management features.</p>
+
+ <p>At the top of the screen are the following controls:</p>
+
+ <ul>
+ <li>Pause. This button can be used to pause playing (provided the
+ player supports it). <img width=16 height=16 class=imgbutton
+ src="@label:images.enabled@"> indicates that playing is paused,
+ <img width=16 height=16 class=imgbutton
+ src="@label:images.disabled@"> that it is not.</li>
+
+ <li>Enable/disable random play. If disabled then queued tracks
+ will still be played but if the queue is empty nothing will be
+ picked at random. <img width=16 height=16 class=imgbutton
+ src="@label:images.enabled@"> indicates that random play is
+ enabled, <img width=16 height=16 class=imgbutton
+ src="@label:images.disabled@"> that it is disabled.</li>
+
+ <li>Enable/disable play. If disabled then tracks in the queue
+ will not be played, but will remain in the queue instead. <img
+ width=16 height=16 class=imgbutton src="@label:images.enabled@">
+ indicates that play is enabled, <img width=16 height=16
+ class=imgbutton src="@label:images.disabled@"> that it is
+ disabled.</li>
+
+ <li>Volume control. You can use the <img class=button
+ src="@label:images.up@"
+ title="@label:volume.increase@"
+ alt="@label:volume.increase@"> and <img
+ src="@label:images.down@"
+ title="@label:volume.reduce@"
+ alt="@label:volume.reduce@"> buttons to increase or
+ decrease the volume, or enter new volume settings for the left
+ and/or right speakers.</li>
+
+ </ul>
+
+ <p>Below this is the same table of current and queued tracks as for
+ the main playing screen, but with extra buttons for managing the
+ queue.
+ The <img class=button src="@label:images.up@"
+ title="@label:playing.up@" alt="@label:playing.up@"> and <img
+ src="@label:images.down@" title="@label:playing.down@"
+ alt="@label:playing.down@"> buttons on each track move that
+ track around in the queue. Similarly the <img class=button
+ src="@label:images.upall@" title="@label:playing.upall@"
+ alt="@label:playing.upall@"> and <img
+ src="@label:images.downall@" title="@label:playing.downall@"
+ alt="@label:playing.downall@"> buttons move each track to the head or
+ tail of the queue.
+ Depending on server configuration, it may be that only trusted
+ users can move tracks around the queue.</p>
+
+ </div>
+
+ <h2 class=sidebarlink><a name=recent>Recent</a></h2>
+
+ <div class=helpsection>
+
+ <p>This screen displays recently played tracks, most recent first.
+ The <img class=button src="@label:images.edit@"
+ title="@label:choose.prefs@" alt="@label:choose.prefs@">
+ button can be used to edit the details for a track; see <a
+ href="#prefs">Editing Preferences</a> below.</p>
+
+ <p>The number of tracks remembered is controlled by the server
+ configuration. See the "history" option in <a
+ href="@url@?action=disorder_config.5">disorder_config(5)</a> for more
+ details.</p>
+
+ </div>
+
+ <h2 class=sidebarlink><a name=choose>Choose</a></h2>
+
+ <div class=helpsection>
+
+ <p>This screen allows you to choose a track to be played, by navigating
+ through the directory structure of the tracks filesystem. The following
+ buttons appear:</p>
+
+ <table class=helpbuttons>
+ <tbody>
+ <tr>
+ <td class=helpbuttons><img
+ class=button src="@label:images.edit@"
+ title="@label:choose.prefs@"
+ alt="@label:choose.prefs@"></td>
+ <td class=helpbuttons>This button can be used to edit the details for a
+ track; see <a href="#prefs">Editing Preferences</a> below.</td>
+ </tr>
+ <tr>
+ <td class=helpbuttons><span class=button>@label{choose.playall}@</span></td>
+ <td class=helpbuttons>This button plays all the tracks in a directory,
+ in order. This is used to efficiently play a whole album.</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <p>This screen has two forms: <a
+ href="@url@?action=choose&nonce=@nonce@">choose</a>, which give
+ you all the top-level directories at once, and <a
+ href="@url@?action=choosealpha&nonce=@nonce@">choosealpha</a>,
+ which breaks them down by initial letter.</p>
+
+ </div>
+
+ <h2 class=sidebarlink><a name=prefs>Editing Preferences</a></h2>
+
+ <div class=helpsection>
+
+ <p>This screen, reached from <a href="#choose">Choose</a> or <a
+ href="#recent">Recent</a>, is used to edit a track's preferences.
+ Preferences can be edited in two ways.</p>
+
+ <p>At the top appear "cooked" preferences. These can be used to
+ edit artist, album and title fields for the track as displayed, or
+ to set the tags for a track, or to enable or disable random play
+ for the track.</p>
+
+ <p>Tags are separated by commas and can contain any other printing
+ characters (including spaces). Leading and trailing spaces are
+ not significant.</p>
+
+ <p>Random play for any given track is enabled by default, but you
+ can use this screen to disable it for undesirable tracks.</p>
+
+ <p>Below this are "raw" preferences, which allow individual
+ database fields to be modified.</p>
+
+ <p>To change an existing preference, edit its value and press its
+ <span class=button>@label{prefs.set}@</span> button.</p>
+
+ <p>To delete an existing preference, press its
+ <span class=button>@label{prefs.delete}@</span> button.</p>
+
+ <p>To add a new preference, enter its name and value in the box at the
+ bottom and press the <span class=button>@label{prefs.new}@</span> button.
+ If the preference exists already it will be overwritten.</p>
+
+
+ <p>Preferences can have any name or value but certain names have special
+ significance:</p>
+
+ <table class=helpprefs>
+ <tbody>
+ <tr>
+ <td class=helpprefs><span class=helppref>pick_at_random</span></td>
+ <td class=helpprefs>If this preference is present and set to "0" then
+ the track will not be picked for random play. Otherwise it may be.</td>
+ </tr>
+ <tr>
+ <td class=helpprefs><span class=helppref>trackname_<span class=helpprefbit>context</span>_<span class=helpprefbit>part</span></span></td>
+ <td class=helpprefs>These preferences can be used to override the
+ filename parsing rules to find a track name part. <span
+ class=helppref>trackname_<span class=helpprefbit>part</span></span> will
+ be used if the full version is not present.</td>
+ </tbody>
+ </table>
+
+ <p><span class=helpprefbit>context</span> can be anything but standard
+ values are:</p>
+
+ <table class=helpcontexts>
+ <tbody>
+ <tr>
+ <td class=helpcontexts><span class=helpcontext>display</span></td>
+ <td class=helpcontexts>Displayed in a web page</td>
+ </tr>
+ <tr>
+ <td class=helpcontexts><span class=helpcontext>sort</span></td>
+ <td class=helpcontexts>Used when sorting track names</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <p><span class=helpprefbit>part</span> can be anything too but standard
+ values are "artist", "album" and "title", with the obvious meanings.</p>
+
+ <p>See also <a href="@url@?action=disorder.1">disorder(1)</a> and <a
+ href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+ details.</p>
+
+ </div>
+
+ <h2 class=sidebarlink>Search</h2>
+
+ <div class=helpsection>
+
+ <p>This screen allows you to search for keywords in track names. If you
+ specify more than one keyword then only tracks containing all of them are
+ listed. Results are grouped by artist, album and title.</p>
+
+ <p>It is possible to limit results to tracks with a particular
+ tag, by using <b>tag:</b><i>TAG</i> among the search terms.</p>
+
+ <p>Some keywords, known as "stopwords", are excluded from the search, and
+ will never match. See the "stopword" option in <a
+ href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+ details about this.</p>
+
+ </div>
+
+ @if{@eq{@label:menu@}{sidebar}@}
+ {
+
+ <h2 class=sidebarlink>Volume</h2>
+
+ <div class=helpsection>
+
+ <p>This screen allows you to set the playback volume, if this is enabled in
+ the server configuration. See the "channel" and "mixer" options in <a
+ href="@url@?action=disorder_config.5">disorder_config(5)</a> for further
+ details about this.</p>
+
+ </div>
+
+ }{<!-- volume currently only linked in sidebar menu -->}@
+
+ <h2 class=sidebarlink>Troubleshooting</h2>
+
+ <div class=helpsection>
+
+ <p>If you cannot play a track, or it does not appear in the
+ database even after a rescan, check the following things:</p>
+
+ <ul>
+
+ <li>Are there any error messages in the system log? The server
+ logs to <tt>LOG_DAEMON</tt>, which typically ends up in
+ <i>/var/log/daemon.log</i> or <i>/var/log/messages</i>, though
+ this depends on local configuration.
+
+ <li>Is the track in a known format? Have a look at the
+ configuration file for the formats recognized by the local
+ installation. The filename matching is case-sensitive.
+
+ <li>Do permissions on the track allow the server to read it?
+
+ <li>Do the permissions on the containing directories allow the
+ server to read and execute them?
+
+ </ul>
+
+ <p>The user the server runs as is determined by the <tt>user</tt>
+ directive in the configuration file. The README recommends using
+ <b>jukebox</b> for this purpose but it could be different
+ locally.</p>
+
+ </div>
+
+ <h2 class=sidebarlink>Man Pages</h2>
+
+ <div class=helpsection>
+
+ <p><a href="@url@?action=disorder_config.5">disorder_config(5)</a> -
+ configuration</p>
+
+ <p><a href="@url@?action=disorder.1">disorder(1)</a> - command line
+ client</p>
+
+ <p><a href="@url@?action=disobedience.1">disobedience(1)</a> - GTK+
+ client</p>
+
+ <p><a href="@url@?action=tkdisorder.1">tkdisorder(1)</a> - GUI
+ client</p>
+
+ <p><a href="@url@?action=disorderd.8">disorderd(8)</a> - server</p>
+
+ <p><a href="@url@?action=disorder-dump.8">disorder-dump(8)</a> -
+ dump/restore preferences database</p>
+
+ <p><a href="@url@?action=disorder.3">disorder(3)</a> - C API</p>
+
+ <p><a href="@url@?action=disorder_protocol.5">disorder_protocol(5)</a> -
+ DisOrder control protocol</p>
+
+ </div>
+
+@include{@label{menu}@end}@
+ </div>
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:eb6205dbb8596ff6fb3290e4625a2ada -->
--- /dev/null
+# 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
--- /dev/null
+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
--- /dev/null
+# Labels used by the web interface.
+#
+# Rather then editing this file, edit options.user instead.
+#
+# Where there is 'short and long' text this means that the short text
+# will appear in the ALT attribute, and so appear in a text-only browser
+# (or if images are disabled); usually this should be a single short word.
+# The long text will appear in the TITLE attribute, and so appear for
+# instance if the user hovers over whatever the widget is.
+
+# <TITLE> for the 'Playing' screen
+label playing.title "Now Playing"
+
+label playing.randomtrack
+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
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@if{@isplaying@}{@playing{@part:title@}@}{@label:playing.title@}@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:playing.title@</h1>
+
+ @#{extra control buttons for the management page}@
+ @if{@arg:mgmt@}{
+ <p class=mgmt>
+ @if{@paused@}{
+ <!-- paused -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=resume&nonce=@nonce@&mgmt=true"
+ title="@label:playing.resumeverbose@">@label:playing.pause@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+ }{
+ <!-- not paused -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=pause&nonce=@nonce@&mgmt=true"
+ title="@label:playing.pauseverbose@">@label:playing.pause@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+ }@
+ @if{@random-enabled@}{
+ <!-- random played enabled -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=random-disable&nonce=@nonce@&mgmt=true"
+ title="@label:playing.randomdisableverbose@">@label:playing.random@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+ }{
+ <!-- random played disabled -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=random-enable&nonce=@nonce@&mgmt=true"
+ title="@label:playing.randomenableverbose@">@label:playing.random@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+ }@
+ @if{@enabled@}{
+ <!-- playing enabled -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=disable&nonce=@nonce@&mgmt=true"
+ title="@label:playing.disableverbose@">@label:playing.playing@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.enabled@">
+ }{
+ <!-- playing disabled -->
+ <span class=button>
+ <a class=button
+ href="@url@?action=enable&nonce=@nonce@&mgmt=true"
+ title="@label:playing.enableverbose@">@label:playing.playing@</a>
+ </a>
+ </span>
+ <img width=16 height=16 class=imgbutton src="@label:images.disabled@">
+ }@
+ <form class=volume action="@url@" method=POST
+ enctype="multipart/form-data" accept-charset=utf-8>
+ <span class=volume>
+ @label:playing.volume@
+ <a class=imgbutton
+ href="@url@?action=volume&delta=-@label:volume.resolution@&back=@urlquote{@thisurl@?mgmt=true}@">
+ <img class=button src="@label:images.down@"
+ alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
+ </a>
+ @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
+ @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
+ <input name=nonce type=hidden value="@nonce@">
+ <input name=back type=hidden value="@thisurl@?mgmt=true">
+ <button class=search name=action type=submit value=volume>
+ @label:volume.set@
+ </button>
+ <a class=imgbutton
+ href="@url@?action=volume&delta=@label:volume.resolution@&back=@urlquote{@thisurl@?mgmt=true}@">
+ <img class=button src="@label:images.up@"
+ alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
+ </a>
+ </form>
+ </span>
+ }@
+
+@#{only display the table if there is something to put in it}@
+@if{@or{@isplaying@}{@isqueue@}@}{
+ <table class=playing>
+ <tr class=headings>
+ <th class=when>@label:heading.when@</th>
+ <th class=who>@label:heading.who@</th>
+ <th class=artist>@label:heading.artist@</th>
+ <th class=album>@label:heading.album@</th>
+ <th class=title>@label:heading.title@</th>
+ <th class=length>@label:heading.length@</th>
+ <th class=button> </th>
+ @if{@arg:mgmt@}{
+ <th class=imgbutton> </th>
+ <th class=imgbutton> </th>
+ <th class=imgbutton> </th>
+ <th class=imgbutton> </th>
+ }@
+ </tr>
+ @if{@isplaying@}{
+ <tr class=nowplaying>
+ <td colspan=@if{@arg:mgmt@}{11}{7}@ class=nowplaying>@label:playing.now@</td>
+ </tr>
+ @playing{
+ <tr class=playing>
+ <td class=when>@when@</td>
+ <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:playing.randomtrack@}{ }@}{@who@}@</td>
+ <td class=artist><a class=directory
+ href="@url@?action=choose&directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
+ title="@label:playing.artistverbose@">@part:artist@</a></td>
+ <td class=album><a class=directory
+ href="@url@?action=choose&directory=@urlquote{@dirname{@part:path@}@}@"
+ title="@label:playing.albumverbose@">@part:album@</a></td>
+ <td class=title>@part:title@</td>
+ <td class=length>@length@</td>
+ <td class=imgbutton>@if{@scratchable@}{<a class=imgbutton
+ href="@url@?action=scratch&nonce=@nonce@&id=@id@&mgmt=@arg:mgmt@"><img
+ class=button src="@label:images.scratch@"
+ title="@label:playing.scratchverbose@"
+ alt="@label:playing.scratch@"></a>}{<img
+ class=button src="@label:images.noscratch@"
+ title="@label:playing.scratchverbose@"
+ alt="@label:playing.scratch@">}@</td>
+ @if{@arg:mgmt@}{
+ <td class=imgbutton> </td>
+ <td class=imgbutton> </td>
+ <td class=imgbutton> </td>
+ <td class=imgbutton> </td>
+ }@
+ </tr>
+ }@}@
+ @if{@isqueue@}{
+ <tr class=next>
+ <td colspan=@if{@arg:mgmt@}{11}{7}@ class=next>@label:playing.next@</td>
+ </tr>
+ @queue{
+ <tr class=@parity@>
+ <td class=when>@when@</td>
+ <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:queue.randomtrack@}{ }@}{@who@}@</td>
+ <td class=artist><a class=directory href="@url@?action=choose&directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@">@part:artist@</a></td>
+ <td class=album><a class=directory href="@url@?action=choose&directory=@urlquote{@dirname{@part:path@}@}@">@part:album@</a></td>
+ <td class=title>@part:title@</td>
+ <td class=length>@length@</td>
+ <td class=imgbutton>@if{@removable@}{<a class=imgbutton
+ href="@url@?action=remove&nonce=@nonce@&id=@id@&mgmt=@arg:mgmt@"><img
+ class=button src="@label:images.scratch@"
+ title="@label:playing.removeverbose@"
+ alt="@label:playing.remove@"></a>}{ }@</td>
+ @if{@arg:mgmt@}{
+ @if{@isfirst@}
+ {<td class=imgbutton>
+ <img
+ class=button src="@label:images.noupall@"
+ title="@label:playing.upallverbose@" alt="">
+ <td class=imgbutton>
+ <img
+ class=button src="@label:images.noup@"
+ title="@label:playing.upverbose@" alt="">}
+ {<td class=imgbutton>
+ <a class=imgbutton
+ href="@url@?action=move&nonce=@nonce@&id=@id@&delta=2147483647&mgmt=true"><img
+ class=button src="@label:images.upall@"
+ title="@label:playing.upallverbose@"
+ alt="@label:playing.upall@"></a>
+ <td class=imgbutton>
+ <a class=imgbutton
+ href="@url@?action=move&nonce=@nonce@&id=@id@&delta=1&mgmt=true"><img
+ class=button src="@label:images.up@"
+ title="@label:playing.upverbose@" alt="@label:playing.up@"></a>}@
+ @if{@islast@}
+ {<td class=imgbutton>
+ <img
+ class=button src="@label:images.nodown@"
+ title="@label:playing.downverbose@" alt="">
+ <td class=imgbutton>
+ <img
+ class=button src="@label:images.nodownall@"
+ title="@label:playing.downallverbose@" alt="">}
+ {<td class=imgbutton>
+ <a class=imgbutton href="@url@?action=move&nonce=@nonce@&id=@id@&delta=-1&mgmt=true"><img
+ class=button src="@label:images.down@"
+ title="@label:playing.downverbose@"
+ alt="@label:playing.down@">
+ <td class=imgbutton>
+ <a class=imgbutton href="@url@?action=move&nonce=@nonce@&id=@id@&delta=-2147483647&mgmt=true"><img
+ class=button src="@label:images.downall@"
+ title="@label:playing.downallverbose@"
+ alt="@label:playing.downall@">}@</a>
+ }@
+ </tr>
+ }@}@
+ </table>
+}@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:0a255d8605a49fdb5ca6213aefbb21f2 -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005, 2006 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:prefs.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:prefs.title@</h1>
+
+ <form class=prefs action="@url@" method=POST
+ enctype="multipart/form-data" accept-charset=utf-8>
+ <input type=hidden name="files" value="@nfiles@">
+ <input type=hidden name=nonce value=@nonce@>
+ <input type=hidden name=parts value="artist album title">
+ @files{
+ <p class="prefs_head">Preferences for <span class="prefs_track">@arg{@index@_file}@</span></p>
+ <input type=hidden name="@index@_file" value="@arg{@index@_file}@">
+ <table class=prefs>
+ <tr class="prefs_headings">
+ <th class="prefs_name">@label:prefs.name@</th>
+ <th class="prefs_value">@label:prefs.value@</th>
+ </tr>
+ <tr class=even>
+ <td class="prefs_name">@label:heading.title@</td>
+ <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_title" value="@part{display}{title}{@arg{@index@_file}@}@"></td>
+ </tr>
+ <tr class=odd>
+ <td class="prefs_name">@label:heading.album@</td>
+ <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_album" value="@part{display}{album}{@arg{@index@_file}@}@"></td>
+ </tr>
+ <tr class=even>
+ <td class="prefs_name">@label:heading.artist@</td>
+ <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_artist" value="@part{display}{artist}{@arg{@index@_file}@}@"></td>
+ </tr>
+ <tr class=odd>
+ <td class="prefs_name">@label:prefs.tags@</td>
+ <td class="prefs_value"><input size=40 class="prefs_value" type=text name="@index@_tags" value="@pref{@arg{@index@_file}@}{tags}@"></td>
+ </tr>
+ <tr class=even>
+ <td class="prefs_name">@label:prefs.random@</td>
+ <td class="prefs_value"><input class="prefs_value" type=checkbox
+ name="@index@_random" value=true
+ @if{@ne{@pref{@arg{@index@_file}@}{pick_at_random}@}{0}@}{ checked}{}@></td>
+ </table>
+ }@
+
+ <p>
+ <button class="pref_set" type=submit name=action value=prefs>
+ @label:prefs.set@
+ </button>
+ </p>
+ </form>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:bc3f0280181afcc8d930e2118e1a2e53 -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:recent.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:recent.title@</h1>
+
+@#{only display the table if there is something to put in it}@
+@if{@isrecent@}{
+ <table class=recent>
+ <tr class=headings>
+ <th class=when>@label:heading.when@</th>
+ <th class=who>@label:heading.who@</th>
+ <th class=artist>@label:heading.artist@</th>
+ <th class=album>@label:heading.album@</th>
+ <th class=title>@label:heading.title@</th>
+ <th class=length>@label:heading.length@</th>
+ <th class=button> </th>
+ </tr>
+ @recent{
+ <tr class=@parity@>
+ <td class=when>@when@</td>
+ <td class=who>@who@</td>
+ <td class=artist><a class=directory href="@url@?action=choose&directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@">@part:artist@</a></td>
+ <td class=album><a class=directory href="@url@?action=choose&directory=@urlquote{@dirname{@part:path@}@}@">@part:album@</a></td>
+ <td class=title>@part:title@</td>
+ <td class=length>@length@</td>
+ <td class=imgbutton><a class=imgbutton
+ href="@url@?action=prefs&nonce=@nonce@&0_file=@urlquote{@file@}@"><img
+ class=button src="@label:images.edit@"
+ title="@label:choose.prefsverbose@"
+ alt="@label:choose.prefs@"></a></td>
+ </tr>
+ }@
+ </table>
+}@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:48fdc42a2503829d74876a84289a0eaa -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2003, 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:search.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:search.title@</h1>
+
+ <form class=search action="@url@" method=POST
+ enctype="multipart/form-data" accept-charset=utf-8>
+ <div class=search>
+ <input class=query name=query type=text value="@arg:query@"
+ size=32>
+ <button class=search name=action type=submit value=search>
+ @label:search.search@
+ </button>
+ <input name=nonce type=hidden value="@nonce@">
+ </div>
+ </form>
+
+ <div class=searchresults>
+ @search{artist}{display}{
+ <div class="search_artist">
+ <p class="search_artist">Artist:
+ <span class="search_artist">@part:artist@</span></p>
+ @search{album}{display}{
+ <div class="search_album">
+ <p class="search_album">Album:
+ <span class="search_album">@part:album@</span></p>
+ @search{title}{
+ <div class="search_title">
+ <p class="search_title">Title:
+ <a href="@url@/?action=play&file=@urlquote{@file@}@&back=@urlquote{@thisurl@}@&nonce=@nonce@">@part:title@</a>
+ @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
+ @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
+ </p>
+ </div>
+ }@
+ </div>
+ }@
+ </div>
+ }@
+ </div>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:a4f39935b0c2a424c601c796ebf1a023 -->
--- /dev/null
+<div id=sidebar>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@">@label:sidebar.playing@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=recent&nonce=@nonce@">@label:sidebar.recent@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=@label:sidebar.choosewhich@&nonce=@nonce@">@label:sidebar.choose@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=search&nonce=@nonce@">@label:sidebar.search@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=volume&nonce=@nonce@">@label:sidebar.volume@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?mgmt=true">@label:sidebar.manage@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=help&nonce=@nonce@">@label:sidebar.help@</a>
+ </p>
+ <p class=sidebarlink>
+ <a class=sidebarlink href="@url@?action=about&nonce=@nonce@">@label:sidebar.about@</a>
+ </p>
+</div>
+<div id=content>
+@@
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:4d13881dcc3c1c93ff42a9ed5ca0f737 -->
--- /dev/null
+@include:credits@
+</div>
+@@
+<!--
+This file is part of DisOrder.
+Copyright (C) 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:NDGZgbsXX8WFUdqrRBzhcQ -->
--- /dev/null
+@include:stylesheet@
+@@
+Anything that goes in all html HEAD elements goes here.
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:82672a6cf795696767ec187456e17c86 -->
--- /dev/null
+ <link rel=stylesheet type="text/css" href="@label:links.css@">
+@@
+This file is a standard place to put a link to a stylesheet,
+or an embedded stylesheet.
+<!--
+This file is part of DisOrder.
+Copyright (C) 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:d243bbe6e40ef52213cf0f72d92a7c6a -->
--- /dev/null
+<p class=menubar>
+ <a class=@if{@eq{@action@}{playing}@}{activemenu}{inactivemenu}@
+ href="@url@"
+ title="@label:sidebar.playingverbose@">@label:sidebar.playing@</a>
+ <a class=@if{@eq{@action@}{recent}@}{activemenu}{inactivemenu}@
+ href="@url@?action=recent&nonce=@nonce@"
+ title="@label:sidebar.recentverbose@">@label:sidebar.recent@</a>
+ <a class=@if{@or{@eq{@action@}{choose}@}
+ {@eq{@action@}{choosealpha}@}@}
+ {activemenu}
+ {inactivemenu}@
+ href="@url@?action=@label:sidebar.choosewhich@&nonce=@nonce@"
+ title="@label:sidebar.chooseverbose@">@label:sidebar.choose@</a>
+ <a class=@if{@eq{@action@}{search}@}{activemenu}{inactivemenu}@
+ href="@url@?action=search&nonce=@nonce@"
+ title="@label:sidebar.searchverbose@">@label:sidebar.search@</a>
+<!-- disabled by default since now available from 'manage'
+ <a class=@if{@eq{@action@}{volume}@}{activemenu}{inactivemenu}@
+ href="@url@?action=volume&nonce=@nonce@"
+ title="@label:sidebar.volumeverbose@">@label:sidebar.volume@</a>
+-->
+ <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
+ href="@url@?mgmt=true"
+ title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
+ <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
+ href="@url@?action=help&nonce=@nonce@"
+ title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>
+ <a class=@if{@eq{@action@}{about}@}{activemenu}{inactivemenu}@
+ href="@url@?action=about&nonce=@nonce@"
+ title="@label:sidebar.aboutverbose@">@label:sidebar.about@</a>
+</p>
+<hr>
+@@
+<!--
+This file is part of DisOrder.
+Copyright (C) 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:zo3pu0olF7vPOu0nAx703g -->
--- /dev/null
+@include:credits@
+@@
+<!--
+This file is part of DisOrder.
+Copyright (C) 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<!-- arch-tag:8+NViAUIcjCGcp5rEhCQ6A -->
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2004, 2005 Richard Kettlewell
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+-->
+<html>
+ <head>
+@include:stdhead@
+ <title>@label:volume.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+ <h1 class=title>@label:volume.title@</h1>
+
+ <form class=volume action="@url@" method=POST>
+ <p class=volume>
+ <a class=imgbutton
+ href="@url@?action=volume&delta=-@label:volume.resolution@">
+ <img class=button src="@label:images.down@"
+ alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
+ </a>
+ @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
+ @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
+ <input name=nonce type=hidden value="@nonce@">
+ <button class=search name=action type=submit value=volume>
+ @label:volume.set@
+ </button>
+ <a class=imgbutton
+ href="@url@?action=volume&delta=@label:volume.resolution@">
+ <img class=button src="@label:images.up@"
+ alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
+ </a>
+ </p>
+ </form>
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
+<!-- arch-tag:2aa86adb876245eb10d501168f30db07 -->