chiark / gitweb /
Merge memory hygeine branch
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 Jul 2010 16:08:21 +0000 (17:08 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 Jul 2010 16:08:21 +0000 (17:08 +0100)
157 files changed:
.bzrignore
AUTHORS [new file with mode: 0644]
CHANGES.html
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
Makefile.am
NEWS [new file with mode: 0644]
README
README.developers
README.raw
TODO [deleted file]
autogen.sh [moved from prepare with 85% similarity]
cgi/actions.c
cgi/macros-disorder.c
configure.ac
debian/Makefile.am
debian/changelog
debian/copyright
debian/disobedience-manual [new file with mode: 0644]
debian/rules
disobedience/Makefile.am
disobedience/added.c
disobedience/autoscroll.c
disobedience/choose-menu.c
disobedience/choose-search.c
disobedience/choose.c
disobedience/control.c
disobedience/disobedience.c
disobedience/disobedience.h
disobedience/help.c
disobedience/log.c
disobedience/login.c
disobedience/manual/Makefile.am [new file with mode: 0644]
disobedience/manual/arch-simple.png [new file with mode: 0644]
disobedience/manual/arch-simple.svg [new file with mode: 0644]
disobedience/manual/button-pause.png [new file with mode: 0644]
disobedience/manual/button-playing.png [new file with mode: 0644]
disobedience/manual/button-random.png [new file with mode: 0644]
disobedience/manual/button-rtp.png [new file with mode: 0644]
disobedience/manual/button-scratch.png [new file with mode: 0644]
disobedience/manual/choose-search.png [new file with mode: 0644]
disobedience/manual/choose.png [new file with mode: 0644]
disobedience/manual/disobedience-debian-menu.png [new file with mode: 0644]
disobedience/manual/disobedience-terminal.png [new file with mode: 0644]
disobedience/manual/disobedience.css [new file with mode: 0644]
disobedience/manual/disorder-email-confirm.png [new file with mode: 0644]
disobedience/manual/disorder-web-login.png [new file with mode: 0644]
disobedience/manual/index.html [new file with mode: 0644]
disobedience/manual/intro.html [new file with mode: 0644]
disobedience/manual/login.png [new file with mode: 0644]
disobedience/manual/menu-control.png [new file with mode: 0644]
disobedience/manual/menu-edit.png [new file with mode: 0644]
disobedience/manual/menu-help.png [new file with mode: 0644]
disobedience/manual/menu-server.png [new file with mode: 0644]
disobedience/manual/misc.html [new file with mode: 0644]
disobedience/manual/playlist-create.png [new file with mode: 0644]
disobedience/manual/playlist-picker-menu.png [new file with mode: 0644]
disobedience/manual/playlist-popup-menu.png [new file with mode: 0644]
disobedience/manual/playlist-window.png [new file with mode: 0644]
disobedience/manual/playlists.html [new file with mode: 0644]
disobedience/manual/properties.html [new file with mode: 0644]
disobedience/manual/queue-menu.png [new file with mode: 0644]
disobedience/manual/queue.png [new file with mode: 0644]
disobedience/manual/queue2.png [new file with mode: 0644]
disobedience/manual/recent.png [new file with mode: 0644]
disobedience/manual/tabs.html [new file with mode: 0644]
disobedience/manual/track-properties.png [new file with mode: 0644]
disobedience/manual/volume-slider.png [new file with mode: 0644]
disobedience/manual/window.html [new file with mode: 0644]
disobedience/menu.c
disobedience/misc.c
disobedience/multidrag.c
disobedience/playlists.c
disobedience/popup.c
disobedience/popup.h
disobedience/progress.c
disobedience/properties.c
disobedience/queue-generic.c
disobedience/queue-generic.h
disobedience/queue-menu.c
disobedience/queue.c
disobedience/recent.c
disobedience/rtp.c
disobedience/users.c
doc/Makefile.am
doc/disobedience.1.in
doc/disorder.3
doc/disorder_config.5.in
doc/disorder_protocol.5.in
driver/Makefile.am [deleted file]
driver/disorder.c [deleted file]
images/Makefile.am
images/cards-simple-fanned.svg [new file with mode: 0644]
images/cards-thin.svg [new file with mode: 0644]
images/cards24.png [new file with mode: 0644]
images/cards48.png [new file with mode: 0644]
lib/Makefile.am
lib/cgi.c
lib/client.c
lib/configuration.c
lib/configuration.h
lib/defs.c
lib/defs.h
lib/hreader.c [new file with mode: 0644]
lib/hreader.h [new file with mode: 0644]
lib/mime.c
lib/queue.h
lib/resample.c
lib/sendmail.c
lib/speaker-protocol.h
lib/trackdb-int.h
lib/trackdb-playlists.c
lib/trackdb.c
lib/trackdb.h
lib/trackname.c
lib/trackname.h
lib/trackorder.c
lib/tracksort.c
lib/validity.c [new file with mode: 0644]
lib/validity.h [new file with mode: 0644]
lib/wav.c
lib/wav.h
libtests/Makefile.am
libtests/t-configuration.c [new file with mode: 0644]
libtests/t-hash.c
libtests/t-hex.c
libtests/test.h
plugins/Makefile.am
plugins/exec.c
plugins/tracklength-flac.c [new file with mode: 0644]
plugins/tracklength-mp3.c [new file with mode: 0644]
plugins/tracklength-ogg.c [new file with mode: 0644]
plugins/tracklength-wav.c [new file with mode: 0644]
plugins/tracklength.c
plugins/tracklength.h [new file with mode: 0644]
scripts/htmlman
scripts/sedfiles.make
server/Makefile.am
server/decode-flac.c [new file with mode: 0644]
server/decode-mp3.c [new file with mode: 0644]
server/decode-ogg.c [new file with mode: 0644]
server/decode-wav.c [new file with mode: 0644]
server/decode.c
server/decode.h [new file with mode: 0644]
server/disorder-server.h
server/disorderd.c
server/dump.c
server/mount.c [new file with mode: 0644]
server/normalize.c
server/play.c
server/queue-ops.c
server/rescan.c
server/schedule.c
server/server.c
server/speaker.c
sounds/Makefile.am
tests/dtest.py

index 0cb8173e69789522b595b5615377a6adb28dd9b1..150c85c8e411e76136edf02898f010129caa1bc6 100644 (file)
@@ -107,7 +107,7 @@ doc/disorder-normalize.8.html
 doc/disorder-decode.8.html
 doc/disorder-decode.8
 doc/plumbing.png
-disobedience/images.h
+images/images.h
 debian/disorder-server
 doc/disorder-stats.8
 doc/disorder-stats.8.html
@@ -172,6 +172,7 @@ libtests/t-words
 libtests/t-wstat
 libtests/t-macros
 libtests/t-cgi
+libtests/t-configuration
 doc/*.tmpl
 doc/disorder_templates.5
 oc/disorder_templates.5.html
@@ -203,3 +204,4 @@ server/endian
 clients/rtpmon
 libtests/t-resample
 clients/resample
+disobedience/manual/Makefile
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..57c71b4
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+See the end of README for authorship details.
index 90101b0cc384ae4b8b019444117c147fe111db79..53c9fa031e50466f0885d3c78ec6a1da4a3142de 100644 (file)
@@ -39,11 +39,21 @@ h4 {
 }
 
 table.bugs {
-  width: 100%
+  width: 100%;
+  font-size: 12pt;
+  border-collapse: collapse;
+  border:1px
 }
 
 table.bugs th {
-  text-align: left
+  text-align: left;
+  border: 1px solid black;
+  background-color: black;
+  color: white
+}
+
+table.bugs td {
+  border: 1px solid
 }
 
 span.command {
@@ -58,7 +68,27 @@ span.command {
 <p>This file documents recent user-visible changes to <a
  href="http://www.greenend.org.uk/rjk/disorder/">DisOrder</a>.</p>
 
-<h2>Changes up to version 4.4</h2>
+<h2>Changes up to version 5.1</h2>
+
+<div class=section>
+
+  <h3>Removable Device Support</h3>
+
+  <div class=section>
+
+    <p>The server will now automatically initiate a rescan when a filesystem is
+    mounted or unmounted.  (Use the <tt>mount_rescan</tt> option if you want to
+    suppress this behavior.)</p>
+
+    <p>The server takes care not to hold audio files open unnecessarily, so
+    that devices can be unmounted even if tracks from them are currently being
+    buffered.</p>
+
+  </div>
+
+</div>
+
+<h2>Changes up to version 5.0</h2>
 
   <div class=section>
   
@@ -75,7 +105,8 @@ span.command {
       <p>Gapless play should be more reliable, and playback latency over RTP
       should be a bit lower.  Note though that all the sound output code has
       been reorganized and in some cases completely rewritten, so it's possible
-      that bugs may have been (re-)introduced.</p>
+      that bugs may have been (re-)introduced.  Decoding of scratches is also
+      initiated ahead of time, giving more reliable playback.</p>
       
       <p>The <tt>command</tt> backend now (optionally) sends silence instead
       of suspending writes when a pause occurs or no track is playing.</p>
@@ -85,6 +116,12 @@ span.command {
       <a href="http://sox.sourceforge.net/">SoX</a>.  SoX support will be
       removed in a future version.</p>
 
+      <p>The libao plugin has been removed, because the plugin API is not
+      usable in libao 1.0.0.</p>
+
+      <p>Playlists are now supported.  These allow a collection of tracks to be
+      prepared offline and played as a unit.</p>
+
     </div>
       
     <h3>Disobedience</h3>
@@ -96,6 +133,12 @@ span.command {
       &ldquo;Recent&rdquo;, &ldquo;Added&rdquo; and &ldquo;Choose&rdquo; tabs
       to the queue.</p>
 
+      <p>Disobedience now supports playlist editing and has a compact mode,
+      available from the <b>Control</b> menu.</p>
+
+      <p>Disobedience has a <a href="disobedience/manual/index.html">new
+      manual</a>.</p>
+
     </div>
 
     <h3>Web Interface</h3>
@@ -105,7 +148,7 @@ span.command {
       <p>Confirmation URLs should be cleaner (and in particular not end
       with punctuation).  (Please see <a
       href="README.upgrades">README.upgrades</a> for more about this.)</p>
-      
+
     </div>
       
     <h3>RTP Player</h3>
@@ -148,12 +191,27 @@ span.command {
           <th>ID</th>
           <th>Description</th>
         </tr>
-        
+
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=22">#22</a></td>
+          <td>Background decoders interact badly with server reload</td>
+        </tr>
+
         <tr>
           <td><a href="http://code.google.com/p/disorder/issues/detail?id=27">#27</a></td>
           <td>Mac DisOrder uses wrong sound device</td>
         </tr>
 
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=30">#30</a></d>
+          <td>mini disobedience interface</td>
+        </tr>
+
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=32">#32</a></d>
+          <td>Excessively verbose log chatter on shutdown</td>
+        </tr>
+
         <tr>
           <td><a href="http://code.google.com/p/disorder/issues/detail?id=33">#33</a></d>
           <td>(Some) plugins need -lm.</td>
@@ -194,14 +252,39 @@ span.command {
           <td>disobedience doesn't configure its back end</td>
         </tr>
 
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=46">#46</a></d>
+          <td>Sort search results in web interface</td>
+        </tr>
+
         <tr>
           <td><a href="http://code.google.com/p/disorder/issues/detail?id=48">#48</a></d>
           <td>build-time dependency on <tt>oggdec</tt> removed</td>
         </tr>
 
-        
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=49">#49</a></d>
+          <td>Disobedience's 'When' column gets out of date</td>
+        </tr>
+
+        <tr>
+          <td><a href="http://code.google.com/p/disorder/issues/detail?id=51">#51</a></td>
+          <td>Improved speaker process robustness</td>
+        </tr>
+
+        <tr>
+          <td>(none)</td>
+         <td>&ldquo;found track in no collection&rdquo; messages for scratches
+         are now suppressed</td>
+        </tr>
+
+        <tr>
+          <td>(none)</td>
+          <td>Disobedience would sometimes fail to notice when a track
+          started, leading to its display getting out of date.</td>
+        </tr>
+
       </table>
-      
     </div>
   </div>
 
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..4432540
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,676 @@
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                      TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..c263762
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,2 @@
+See version control history for detailed change information.
+       
index 501f344842485420a9bf423a10ba47a6b8131e28..b41014f52b6b97dc04fb1b567632208878d5b588 100644 (file)
@@ -16,7 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-EXTRA_DIST=TODO CHANGES.html README.streams BUGS \
+EXTRA_DIST=CHANGES.html README.streams BUGS \
 README.upgrades README.client README.raw README.vhost README.developers
 SUBDIRS=@subdirs@
 
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..28889c4
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,3 @@
+See CHANGES.html for high-level change information.
+
+See README.upgrades for upgrade information.
diff --git a/README b/README
index 57435a817ababb23692700736c740e342592fe93..b1ddc92a5bdfe902bbdc1a281745080bb0e308d8 100644 (file)
--- a/README
+++ b/README
@@ -25,17 +25,17 @@ effort.
 
 Build dependencies:
   Name             Tested      Notes
-  libdb            4.3.29      not 4.2/4.6; 4.[457] seem to be ok
+  libdb            4.5.20      not 4.6; 4.[78] seem to be ok
   libgc            6.8
-  libvorbisfile    1.1.2
-  libpcre          6.7         need UTF-8 support
+  libvorbisfile    1.2.0
+  libpcre          7.6         need UTF-8 support
   libmad           0.15.1b
-  libgcrypt        1.2.3
-  libao            0.8.6
-  libasound        1.0.13
-  libFLAC          1.1.2
+  libgcrypt        1.4.1
+  libao            0.8.8       1.0.0 is broken
+  libasound        1.0.16
+  libFLAC          1.2.1
   libsamplerate    0.1.4       currently optional
-  GNU C            4.1.2       }
+  GNU C            4.2.1       }
   GNU Make         3.81        } Non-GNU versions will NOT work
   GNU Sed          4.1.5       }
   Python           2.5.2       (optional; 2.4 won't work)
@@ -89,9 +89,6 @@ platform, please get in touch.
      --without-gtk          Don't build GTK+ client (Disobedience)
      --without-python       Don't build Python support
 
-   On a Mac you can use --with-bits=64 to request a 64-bit build.  The default
-   is 32 bits.  You will need suitable versions of all the libraries used.
-
    If configure cannot guess where your web server keeps its HTML documents and
    CGI programs, you may have to tell it, for instance:
 
index 9e84520dd3519d462e7278d460b39bbce62f56f9..5c71b07bbdc75f26fb2e6e3e1592c07bdb2fbc9e 100644 (file)
@@ -18,22 +18,13 @@ Dependencies:
      refuse to use it).
 
    * On FreeBSD you'll need at least these packages:
-        autotools
-        bash
-        flac
-        mad
-         boehm-gc
-         db43
-         gmake
-         gsed
-         libao
-         libgcrypt
-         wget
-         vorbis-tools
+       autotools bash flac mad boehm-gc db43 gmake gsed libao libgcrypt wget
+       vorbis-tools
 
    * On OS X with Fink:
 
-     fink install gtk+2-dev gc libgrypt pcre flac vorbis-tools libmad wget sed
+     fink install gtk+2-dev gc libgrypt pcre flac vorbis-tools libmad wget \
+                  sed libsamplerate0-dev
 
    * Please report unstated dependencies (here, README or debian/control).
 
@@ -42,7 +33,7 @@ Building:
    * Compiled versions of configure and the makefiles are not included in bzr,
      so if you didn't use a source tarball, you must start as follows:
 
-        bash ./prepare
+        bash ./autogen.sh
         ./configure -C
         make
 
index a2da62306e30efd4e501aaf53846b681abd56d75..a1079649115980859d7d91ad2fca39129d7af488 100644 (file)
@@ -13,22 +13,8 @@ The purpose of raw format players is:
 
 ** Usage
 
-To use raw format, use the execraw module and make the command choose the
-"disorder" libao driver.  You may need to link the driver from wherever
-DisOrder installs it (e.g. /usr/local/lib/ao/plugins-2) to where libao will
-look for it (e.g. /usr/lib/ao/plugins-2 or /sw/lib/ao/plugins-2).
-
-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.
+By default, built-in raw-format players are used for several encodings, so you
+do not need to do anything.
 
 ** Low-Level Details
 
diff --git a/TODO b/TODO
deleted file mode 100644 (file)
index e15c225..0000000
--- a/TODO
+++ /dev/null
@@ -1,25 +0,0 @@
--*-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.
similarity index 85%
rename from prepare
rename to autogen.sh
index dd3de9b8628e15ca5db3890040ecd815f333f329..a63049ad8daa3cb8315df90857bc62adbf6232cf 100755 (executable)
--- a/prepare
@@ -21,13 +21,6 @@ set -e
 srcdir=$(dirname $0)
 here=$(pwd)
 cd $srcdir
-rm -f COPYING
-for f in /usr/share/common-licenses/GPL-3 $HOME/doc/GPL-3 $HOME/Documents/GPL-3; 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
index 30ddba1ae03bea23b0e24e3eb1dbe5ff36318358..f6755b0317ad2ca080c1d28098e3902df38cb559 100644 (file)
@@ -95,6 +95,10 @@ static void act_playing(void) {
     if(refresh > config->gap)
       refresh = config->gap;
   }
+  /* Bound the refresh interval below as a back-stop against the above
+   * calculations coming up with a stupid answer */
+  if(refresh < config->refresh_min)
+    refresh = config->refresh_min;
   if((action = cgi_get("action")))
     url = cgi_makeurl(config->url, "action", action, (char *)0);
   else
@@ -608,7 +612,7 @@ static int process_prefs(int numfile) {
     byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
     disorder_set(dcgi_client, file, name, value);
   }
-  if((value = numbered_arg("random", numfile)))
+  if(numbered_arg("random", numfile))
     disorder_unset(dcgi_client, file, "pick_at_random");
   else
     disorder_set(dcgi_client, file, "pick_at_random", "0");
index 30614c1a26d2fecca465457e52e053169d88d466..29835bbc849c196fcea60124fc55b10969cf6bde 100644 (file)
@@ -859,24 +859,36 @@ static int exp__files_dirs(int nargs,
   /* Get the list */
   if(fn(dcgi_client, dir, re, &tracks, &ntracks))
     return 0;
-  /* Sort it.  NB trackname_transform() does not go to the server. */
-  tsd = tracksort_init(ntracks, tracks, type);
-  /* Expand the subsiduary templates.  We chuck in @sort and @display because
-   * it is particularly easy to do so. */
-  for(n = 0; n < ntracks; ++n)
-    if((rc = mx_expand(mx_rewritel(m,
-                                   "index", make_index(n),
-                                   "parity", n % 2 ? "odd" : "even",
-                                   "track", tsd[n].track,
-                                   "first", n == 0 ? "true" : "false",
-                                   "last", n + 1 == ntracks ? "false" : "true",
-                                   "sort", tsd[n].sort,
-                                   "display", tsd[n].display,
-                                   (char *)0),
-                       output, u)))
-      return rc;
+  if(type) {
+    /* Sort it.  NB trackname_transform() does not go to the server. */
+    tsd = tracksort_init(ntracks, tracks, type);
+    /* Expand the subsiduary templates.  We chuck in @sort and @display because
+     * it is particularly easy to do so. */
+    for(n = 0; n < ntracks; ++n)
+      if((rc = mx_expand(mx_rewritel(m,
+                                     "index", make_index(n),
+                                     "parity", n % 2 ? "odd" : "even",
+                                     "track", tsd[n].track,
+                                     "first", n == 0 ? "true" : "false",
+                                     "last", n + 1 == ntracks ? "false" : "true",
+                                     "sort", tsd[n].sort,
+                                     "display", tsd[n].display,
+                                     (char *)0),
+                         output, u)))
+        return rc;
+  } else {
+    for(n = 0; n < ntracks; ++n)
+      if((rc = mx_expand(mx_rewritel(m,
+                                     "index", make_index(n),
+                                     "parity", n % 2 ? "odd" : "even",
+                                     "track", tracks[n],
+                                     "first", n == 0 ? "true" : "false",
+                                     "last", n + 1 == ntracks ? "false" : "true",
+                                     (char *)0),
+                         output, u)))
+        return rc;
+  }
   return 0;
-
 }
 
 /*$ @tracks{DIR}{RE}{TEMPLATE}
@@ -936,14 +948,12 @@ static int exp__search_shim(disorder_client *c, const char *terms,
  * - @parity: "even" or "odd" alternately
  * - @first: "true" on the first directory and "false" otherwise
  * - @last: "true" on the last directory and "false" otherwise
- * - @sort: the sort key for this track
- * - @display: the UNQUOTED display string for this track
  */
 static int exp_search(int nargs,
                       const struct mx_node **args,
                       struct sink *output,
                       void *u) {
-  return exp__files_dirs(nargs, args, output, u, "track", exp__search_shim);
+  return exp__files_dirs(nargs, args, output, u, NULL, exp__search_shim);
 }
 
 /*$ @label{NAME}
index e48cce252c3c2b1517ecc004fb2783f5cdb7611b..1d81d832d429ac8f8b1668be1325b21b4d80a169 100644 (file)
@@ -1,7 +1,7 @@
 # Process this file with autoconf to produce a configure script.
 #
 # This file is part of DisOrder.
-# Copyright (C) 2004-2009 Richard Kettlewell
+# Copyright (C) 2004-2010 Richard Kettlewell
 # Portions copyright (C) 2007 Ross Younger
 #
 # This program is free software: you can redistribute it and/or modify
@@ -18,9 +18,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-AC_INIT([disorder], [4.3+], [richard+disorder@sfere.greenend.org.uk])
+AC_INIT([disorder], [5.0], [richard+disorder@sfere.greenend.org.uk])
 AC_CONFIG_AUX_DIR([config.aux])
-AM_INIT_AUTOMAKE(disorder, [4.3+])
+AM_INIT_AUTOMAKE(disorder, [5.0])
 AC_CONFIG_SRCDIR([server/disorderd.c])
 AM_CONFIG_HEADER([config.h])
 
@@ -156,7 +156,7 @@ case "$host" in
   # Look for a suitable version of libdb among the versions found in FreeBSD 7.0
   AC_CACHE_CHECK([looking for a libdb install],[rjk_cv_libdb],[
     rjk_cv_libdb="none"
-    for db in db43 db44 db45 db46; do
+    for db in db43 db44 db45 db47; do
       if test -e /usr/local/lib/$db; then
         rjk_cv_libdb=$db
         break
@@ -326,6 +326,11 @@ if test -z "$pkghttpdir"; then
 fi
 AC_SUBST([pkghttpdir])
 
+if test -z "$dochtmldir"; then
+  dochtmldir='$(docdir)/html'
+fi
+AC_SUBST([dochtmldir])
+
 subdirs="scripts lib"
 if test $want_tests = yes; then
   subdirs="${subdirs} libtests"
@@ -333,7 +338,7 @@ fi
 subdirs="${subdirs} clients doc examples debian"
 
 if test $want_server = yes; then
-  subdirs="${subdirs} server plugins driver sounds"
+  subdirs="${subdirs} server plugins sounds"
 fi
 if test $want_cgi = yes; then
   subdirs="${subdirs} cgi templates images"
@@ -517,7 +522,6 @@ if test $want_server = yes; then
   AC_CHECK_HEADERS([db.h],[:],[
     missing_headers="$missing_headers $ac_header"
   ])
-  AC_CHECK_HEADERS([FLAC/file_decoder.h])
 fi
 AC_CHECK_HEADERS([dlfcn.h gcrypt.h \
                 getopt.h iconv.h langinfo.h \
@@ -542,6 +546,18 @@ AC_C_BIGENDIAN
 AC_CHECK_TYPES([struct sockaddr_in6],,,[AC_INCLUDES_DEFAULT
 #include <netinet/in.h>])
 
+# Figure out how we'll check for devices being mounted and unmounted
+AC_CACHE_CHECK([for list of mounted filesystems],[rjk_cv_mtab],[
+  if test -e /etc/mtab; then
+    rjk_cv_mtab=/etc/mtab
+  else
+    rjk_cv_mtab=none
+  fi
+])
+if test $rjk_cv_mtab != none; then
+  AC_DEFINE_UNQUOTED([PATH_MTAB],["$rjk_cv_mtab"],[path to file containing mount list])
+fi
+
 # enable -Werror when we check for certain characteristics:
 
 old_CFLAGS="${CFLAGS}"
@@ -634,7 +650,7 @@ if test ! -z "$missing_functions"; then
 fi
 
 # Functions we can take or leave
-AC_CHECK_FUNCS([fls])
+AC_CHECK_FUNCS([fls getfsstat])
 
 if test $want_server = yes; then
   # <db.h> had better be version 3 or later
@@ -680,6 +696,7 @@ AM_CONDITIONAL([SERVER], [test x$want_server = xyes])
 if test $want_gtk = yes; then
   AC_DEFINE([WITH_GTK], [1], [define if using GTK+])
 fi
+AM_CONDITIONAL([GTK], [test x$want_gtk = xyes])
 
 if test "x$GCC" = xyes; then
   # We need LLONG_MAX and annoyingly GCC doesn't always give it to us
@@ -746,7 +763,7 @@ if test "x$GCC" = xyes; then
   fi
 
   # a reasonable default set of warnings
-  CC="${CC} -Wall -W -Wpointer-arith -Wbad-function-cast \
+  CC="${CC} -Wall -W -Wpointer-arith \
        -Wwrite-strings -Wmissing-prototypes \
        -Wmissing-declarations -Wnested-externs"
 
@@ -823,10 +840,10 @@ AC_CONFIG_FILES([Makefile
                 cgi/Makefile
                 clients/Makefile
                 disobedience/Makefile
+                disobedience/manual/Makefile
                 doc/Makefile
                 templates/Makefile
                 plugins/Makefile
-                driver/Makefile
                 debian/Makefile
                 sounds/Makefile
                 python/Makefile
index b799a1cddc89126921b8fe938e3ac5aa7e5bb1e1..293df227d41714d591915ab9a40f68565bd34263 100644 (file)
@@ -25,4 +25,4 @@ EXTRA_DIST=README.Debian config.disorder-server control \
           postrm.disorder-server overrides.disorder-server \
           templates.disorder-server conffiles.disorder-server \
           rules changelog usr.share.menu.disobedience \
-          postinst.disobedience
+          postinst.disobedience disobedience-manual
index 61e1fe7f08329ba7c8626561675d5d421f8c651e..e76e763f350116678903c405d19c5d3320db0c0d 100644 (file)
@@ -1,3 +1,9 @@
+disorder (5.0) unstable; urgency=low
+
+  * DisOrder 5.0
+
+ -- Richard Kettlewell <rjk@greenend.org.uk>  Sun, 06 Jun 2010 12:43:21 +0100
+
 disorder (4.3) unstable; urgency=low
 
   * DisOrder 4.3
index bc90bffd0881fe48a9367363370bd3ab85b1424f..918dbb44e3f7c3f18c85d8ae6cccab38a5519ade 100644 (file)
@@ -29,5 +29,5 @@ GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-On Debian systems, look in /usr/share/common-licenses/GPL for a copy
+On Debian systems, look in /usr/share/common-licenses/GPL-3 for a copy
 of the GPL.
diff --git a/debian/disobedience-manual b/debian/disobedience-manual
new file mode 100644 (file)
index 0000000..93a81b3
--- /dev/null
@@ -0,0 +1,12 @@
+Document: disobedience-manual
+Title: Disobedience Manual
+Author: Richard Kettlewell
+Abstract: This manual describes how to use Disobedience, the GUI
+ client for DisOrder.
+Section: Sound
+
+Format: HTML
+Index: /usr/share/doc/disorder/html/index.html
+Files: /usr/share/doc/disorder/html/*.html
+ /usr/share/doc/disorder/html/*.png
+ /usr/share/doc/disorder/html/*.css
index 620c77564d7ef2ac2b5d176bfa76ec3ef2841a54..1861c139e96d8ed71d8d73679fb45e5b7812d8ee 100755 (executable)
@@ -62,12 +62,12 @@ FAKEROOT=fakeroot
 
 SHELL=bash
 
-# ./prepare is the script that generates configure etc.  It only needs to be
+# ./autogen.sh is the script that generates configure etc.  It only needs to be
 # run if building from a checkout rather than a tarball.
 build:
        @set -e;if test ! -f configure; then \
-         echo ./prepare;\
-         ./prepare;\
+         echo ./autogen.sh;\
+         ./autogen.sh;\
        fi
        @set -e;if test ! -f config.status; then \
          echo ./configure ${CONFIGURE} ${CONFIGURE_EXTRA};\
@@ -166,7 +166,6 @@ pkg-disorder-server: build
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C images
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C server
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C templates
-       $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C driver
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C plugins
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C sounds
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C doc
@@ -181,10 +180,6 @@ pkg-disorder-server: build
          echo mv $$f $${f/.0.0.0};\
          mv $$f $${f/.0.0.0};\
        done
-       @for f in debian/disorder-server/usr/lib/ao/plugins*/*.so.0.0.0; do \
-         echo mv $$f $${f/.0.0.0};\
-         mv $$f $${f/.0.0.0};\
-       done
        find debian/disorder-server -name '*.so' -print0 | xargs -r0 strip --strip-unneeded
        find debian/disorder-server -name '*.so' -print0 | xargs -r0 chmod -x
        $(MKDIR) debian/disorder-server/etc/disorder
@@ -211,7 +206,6 @@ pkg-disorder-server: build
        strip --remove-section=.comment \
                debian/disorder-server/usr/sbin/* \
                debian/disorder-server${cgiexecdir}/* \
-               debian/disorder-server/usr/lib/ao/plugins*/*.so \
                debian/disorder-server/usr/lib/disorder/*.so
        cd debian/disorder-server && \
                find -name DEBIAN -prune -o -type f -print \
@@ -280,6 +274,7 @@ pkg-disobedience: build
        $(MKDIR) debian/disobedience/usr/share/man/man1
        $(MKDIR) debian/disobedience/usr/share/pixmaps
        $(MKDIR) debian/disobedience/usr/share/menu
+       $(MKDIR) debian/disobedience/usr/share/doc-base
        $(MAKE) -C disobedience install DESTDIR=`pwd`/debian/disobedience
        strip --remove-section=.comment debian/disobedience/usr/bin/disobedience
        $(INSTALL_DATA) doc/disobedience.1 \
@@ -289,6 +284,8 @@ pkg-disobedience: build
                        debian/disobedience/usr/share/pixmaps
        $(INSTALL_DATA) debian/usr.share.menu.disobedience \
                debian/disobedience/usr/share/menu/disobedience
+       $(INSTALL_DATA) debian/disobedience-manual \
+               debian/disobedience/usr/share/doc-base/disobedience-manual
        gzip -9f debian/disobedience/usr/share/man/man*/*
        dpkg-shlibdeps -Tdebian/substvars.disobedience \
                debian/disobedience/usr/bin/*
index 01716338c0b0fbceaace771e43405b0bdb5518fd..7b823c262d9bf6860b58aeaeaf2f91572d862a91 100644 (file)
@@ -1,6 +1,6 @@
 #
 # This file is part of DisOrder.
-# Copyright (C) 2006-2009 Richard Kettlewell
+# Copyright (C) 2006-2010 Richard Kettlewell
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 
 bin_PROGRAMS=disobedience
-pkgdata_DATA=disobedience.html
+SUBDIRS=manual
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
-PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png)
 
 disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c    \
        recent.c added.c queue-generic.c queue-generic.h queue-menu.c   \
        choose.c choose-menu.c choose-search.c popup.c misc.c           \
        control.c properties.c menu.c log.c progress.c login.c rtp.c    \
        help.c ../lib/memgc.c settings.c users.c lookup.c choose.h      \
-       popup.h playlists.c multidrag.c multidrag.h autoscroll.c
+       popup.h playlists.c multidrag.c multidrag.h autoscroll.c        \
        autoscroll.h
 disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
        $(LIBASOUND) $(COREAUDIO) $(LIBDB) $(LIBICONV)
@@ -39,35 +38,13 @@ install-exec-hook:
 
 check: check-help
 
-disobedience.html: ../doc/disobedience.1 $(top_srcdir)/scripts/htmlman
-       rm -f $@.new
-       $(top_srcdir)/scripts/htmlman $< >$@.new
-       chmod 444 $@.new
-       mv -f $@.new $@
-
-misc.o: images.h
-
-images.h: $(PNGS)
-       set -e;                                                         \
-       exec > @$.new;                                                  \
-       for png in $(PNGS); do                                          \
-         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
-         gdk-pixbuf-csource --raw --name=image_$$name $$png;           \
-       done;                                                           \
-       echo "static const struct image images[] = {";                  \
-       for png in $(PNGS); do                                          \
-         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
-         echo "  { \"$$name.png\", image_$$name },";                   \
-       done;                                                           \
-       echo "};"
-       mv @$.new $@
+misc.o: ../images/images.h
 
 # check everything has working --help
 check-help: all
        unset DISPLAY;./disobedience --version > /dev/null
        unset DISPLAY;./disobedience --help > /dev/null
 
-CLEANFILES=disobedience.html images.h \
-          *.gcda *.gcov *.gcno *.c.html index.html
+CLEANFILES=*.gcda *.gcov *.gcno *.c.html index.html
 
 export GNUSED
index 7f654ee48ac18400fd1744aaaf9fe73a32b24b3f..04e1d77e13e233d0d569efcd86b20e12df46fa21 100644 (file)
@@ -80,10 +80,10 @@ static const struct queue_column added_columns[] = {
 
 /** @brief Pop-up menu for new tracks list */
 static struct menuitem added_menuitems[] = {
-  { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
-  { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
-  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+  { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+  { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+  { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
 };
 
 struct queuelike ql_added = {
@@ -93,6 +93,8 @@ struct queuelike ql_added = {
   .ncolumns = sizeof added_columns / sizeof *added_columns,
   .menuitems = added_menuitems,
   .nmenuitems = sizeof added_menuitems / sizeof *added_menuitems,
+  .drag_source_targets = choose_targets,
+  .drag_source_actions = GDK_ACTION_COPY,
 };
 
 GtkWidget *added_widget(void) {
index bfe71c1af0e0b867196b9b764c64c16db38d7d1a..2b246998930ec26a0cd15ef49cf7f713a0d95128 100644 (file)
@@ -68,12 +68,11 @@ static gboolean autoscroll_timeout(gpointer data) {
 
   /* see if we are near the edge. */
   offset = ty - (visible_rect.y + 2 * SCROLL_EDGE_SIZE);
-  if (offset > 0)
-    {
-      offset = ty - (visible_rect.y + visible_rect.height - 2 * SCROLL_EDGE_SIZE);
-      if (offset < 0)
-       return TRUE;
-    }
+  if (offset > 0) {
+    offset = ty - (visible_rect.y + visible_rect.height - 2 * SCROLL_EDGE_SIZE);
+    if (offset < 0)
+      return TRUE;
+  }
 
   GtkAdjustment *vadjustment = gtk_tree_view_get_vadjustment(tree_view);
 
index f1aa3b02c18323a5ed93fbcf8f21cd4dc44594f3..b0f59b5dad7deb3b6b23f104c6ca8061362f1243 100644 (file)
 #include "popup.h"
 #include "choose.h"
 
+static void choose_playchildren_callback(GtkTreeModel *model,
+                                         GtkTreePath *path,
+                                         GtkTreeIter *iter,
+                                         gpointer data);
+static void choose_playchildren_received(void *v,
+                                         const char *err,
+                                         int nvec, char **vec);
+static void choose_playchildren_played(void *v, const char *err);
+
 /** @brief Popup menu */
 static GtkWidget *choose_menu;
 
@@ -121,7 +130,7 @@ static void choose_properties_activate(GtkMenuItem attribute((unused)) *item,
   gtk_tree_selection_selected_foreach(choose_selection,
                                       choose_gather_selected_files_callback,
                                       v);
-  properties(v->nvec, (const char **)v->vec);
+  properties(v->nvec, (const char **)v->vec, toplevel);
 }
 
 /** @brief Set sensitivity for select children
@@ -210,10 +219,53 @@ static void choose_selectchildren_activate
                                       0);
 }
 
+/** @brief Play all children */
+static void choose_playchildren_activate
+    (GtkMenuItem attribute((unused)) *item,
+     gpointer attribute((unused)) userdata) {
+  /* Only one thing is selected */
+  gtk_tree_selection_selected_foreach(choose_selection,
+                                      choose_playchildren_callback,
+                                      0);
+}
+
+static void choose_playchildren_callback(GtkTreeModel attribute((unused)) *model,
+                                         GtkTreePath *path,
+                                         GtkTreeIter *iter,
+                                         gpointer attribute((unused)) data) {
+  /* Find the children and play them */
+  disorder_eclient_files(client, choose_playchildren_received,
+                         choose_get_track(iter),
+                         NULL/*re*/,
+                         NULL);
+  /* Expand the node */
+  gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view), path, FALSE);
+}
+
+static void choose_playchildren_received(void attribute((unused)) *v,
+                                         const char *err,
+                                         int nvec, char **vec) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
+  for(int n = 0; n < nvec; ++n)
+    disorder_eclient_play(client, vec[n], choose_playchildren_played, NULL);
+}
+
+static void choose_playchildren_played(void attribute((unused)) *v,
+                                       const char *err) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
+}
+
 /** @brief Pop-up menu for choose */
 static struct menuitem choose_menuitems[] = {
   {
     "Play track",
+    GTK_STOCK_MEDIA_PLAY,
     choose_play_activate,
     choose_play_sensitive,
     0,
@@ -221,6 +273,7 @@ static struct menuitem choose_menuitems[] = {
   },
   {
     "Track properties",
+    GTK_STOCK_PROPERTIES,
     choose_properties_activate,
     choose_properties_sensitive,
     0,
@@ -228,13 +281,23 @@ static struct menuitem choose_menuitems[] = {
   },
   {
     "Select children",
+    NULL,
     choose_selectchildren_activate,
     choose_selectchildren_sensitive,
     0,
     0
   },
+  {
+    "Play children",
+    NULL,
+    choose_playchildren_activate,
+    choose_selectchildren_sensitive,    /* re-use */
+    0,
+    0
+  },
   {
     "Deselect all tracks",
+    NULL,
     choose_selectnone_activate,
     choose_selectnone_sensitive,
     0,
index acfddfa8d536c6b735d59585472d86748f51a1fb..60df2ec095b9c6d92e8ae2012f7151d320df3323 100644 (file)
@@ -441,6 +441,7 @@ static gboolean choose_get_visible_range(GtkTreeView *tree_view,
  * @param direction -1 for prev, +1 for next
  */
 static void choose_move(int direction) {
+  assert(direction);                    /* placate analyzer */
   /* Refocus the main view so typahead find continues to work */
   gtk_widget_grab_focus(choose_view);
   /* If there's no results we have nothing to do */
index 9829f199fc2ebebc1d9b178dedc1d6f23ce481eb..a1d50c1a62c18f584af0f74435f9bd533f435be8 100644 (file)
 #include "disobedience.h"
 #include "choose.h"
 #include "multidrag.h"
+#include "queue-generic.h"
 #include <gdk/gdkkeysyms.h>
 
 /** @brief Drag types */
-static const GtkTargetEntry choose_targets[] = {
+const GtkTargetEntry choose_targets[] = {
   {
-    (char *)"text/x-disorder-playable-tracks", /* drag type */
+    PLAYABLE_TRACKS,                             /* drag type */
     GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
-    1                                     /* ID value */
+    PLAYABLE_TRACKS_ID                           /* ID value */
   },
+  {
+    .target = NULL
+  }
 };
 
 /** @brief The current selection tree */
@@ -711,7 +715,7 @@ GtkWidget *choose_widget(void) {
   gtk_drag_source_set(choose_view,
                       GDK_BUTTON1_MASK,
                       choose_targets,
-                      sizeof choose_targets / sizeof *choose_targets,
+                      1,
                       GDK_ACTION_COPY);
   g_signal_connect(choose_view, "drag-data-get",
                    G_CALLBACK(choose_drag_data_get), NULL);
index 931746a88aac41cb2b23729416fe03cc713baf02..f45492f1e191ed767f273fe452d3f18c26721462 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,7 +25,9 @@
 
 struct icon;
 
-static void clicked_icon(GtkButton *, gpointer);
+static void clicked_icon(GtkToolButton *, gpointer);
+static void toggled_icon(GtkToggleToolButton *button,
+                         gpointer user_data);
 static void clicked_menu(GtkMenuItem *, gpointer userdata);
 static void toggled_menu(GtkCheckMenuItem *, gpointer userdata);
 
@@ -51,12 +53,18 @@ static void icon_changed(const char *event,
 static void volume_changed(const char *event,
                            void *eventdata,
                            void *callbackdata);
+static void control_minimode(const char *event,
+                             void *eventdata,
+                             void *callbackdata);
 
 /* Control bar ------------------------------------------------------------- */
 
 /** @brief Guard against feedback */
 int suppress_actions = 1;
 
+/** @brief Toolbar widget */
+static GtkWidget *toolbar;
+
 /** @brief Definition of an icon
  *
  * We have two kinds of icon:
@@ -69,21 +77,27 @@ int suppress_actions = 1;
  * (All icons can be sensitive or insensitive, separately to the above.)
  */
 struct icon {
-  /** @brief Filename for 'on' image */
-  const char *icon_on;
+  /** @brief TRUE to use GTK+ stock icons instead of filenames */
+  gboolean stock;
+
+  /** @brief TRUE for toggle buttons, FALSE for action buttons */
+  gboolean toggle;
+  
+  /** @brief Filename for image or stock string */
+  const char *icon;
 
   /** @brief Text for 'on' tooltip */
   const char *tip_on;
 
-  /** @brief Filename for 'off' image or NULL for an action icon */
-  const char *icon_off;
-
-  /** @brief Text for 'off tooltip */
+  /** @brief Text for 'off' tooltip */
   const char *tip_off;
 
   /** @brief Associated menu item or NULL */
   const char *menuitem;
 
+  /** @brief Label text */
+  const char *label;
+
   /** @brief Events that change this icon, separated by spaces */
   const char *events;
 
@@ -114,6 +128,9 @@ struct icon {
    * Can be NULL for always sensitive.
    */
   int (*sensitive)(void);
+
+  /** @brief True if the menu item has inverse sense to the button */
+  gboolean menu_invert;
   
   /** @brief Pointer to button */
   GtkWidget *button;
@@ -121,16 +138,16 @@ struct icon {
   /** @brief Pointer to menu item */
   GtkWidget *item;
 
-  GtkWidget *image_on;
-  GtkWidget *image_off;
+  GtkWidget *image;
 };
 
 static int pause_resume_on(void) {
-  return !(last_state & DISORDER_TRACK_PAUSED);
+  return !!(last_state & DISORDER_TRACK_PAUSED);
 }
 
 static int pause_resume_sensitive(void) {
-  return !!(last_state & DISORDER_PLAYING)
+  return playing_track
+    && !!(last_state & DISORDER_PLAYING)
     && (last_rights & RIGHT_PAUSE);
 }
 
@@ -166,19 +183,24 @@ static int rtp_sensitive(void) {
 /** @brief Table of all icons */
 static struct icon icons[] = {
   {
-    icon_on: "pause32.png",
-    tip_on: "Pause playing track",
-    icon_off: "play32.png",
-    tip_off: "Resume playing track",
+    toggle: TRUE,
+    stock: TRUE,
+    icon: GTK_STOCK_MEDIA_PAUSE,
+    label: "Pause",
+    tip_on: "Resume playing track",
+    tip_off: "Pause playing track",
     menuitem: "<GdisorderMain>/Control/Playing",
     on: pause_resume_on,
     sensitive: pause_resume_sensitive,
-    action_go_on: disorder_eclient_resume,
-    action_go_off: disorder_eclient_pause,
-    events: "pause-changed playing-changed rights-changed",
+    action_go_on: disorder_eclient_pause,
+    action_go_off: disorder_eclient_resume,
+    events: "pause-changed playing-changed rights-changed playing-track-changed",
+    menu_invert: TRUE,
   },
   {
-    icon_on: "cross32.png",
+    stock: TRUE,
+    icon: GTK_STOCK_STOP,
+    label: "Scratch",
     tip_on: "Cancel playing track",
     menuitem: "<GdisorderMain>/Control/Scratch",
     sensitive: scratch_sensitive,
@@ -186,9 +208,11 @@ static struct icon icons[] = {
     events: "playing-track-changed rights-changed",
   },
   {
-    icon_on: "randomenabled32.png",
+    toggle: TRUE,
+    stock: FALSE,
+    icon: "cards24.png",
+    label: "Random",
     tip_on: "Disable random play",
-    icon_off: "randomdisabled32.png",
     tip_off: "Enable random play",
     menuitem: "<GdisorderMain>/Control/Random play",
     on: random_enabled,
@@ -198,9 +222,11 @@ static struct icon icons[] = {
     events: "random-changed rights-changed",
   },
   {
-    icon_on: "playenabled32.png",
+    toggle: TRUE,
+    stock: TRUE,
+    icon: GTK_STOCK_MEDIA_PLAY,
+    label: "Play",
     tip_on: "Disable play",
-    icon_off: "playdisabled32.png",
     tip_off: "Enable play",
     on: playing_enabled,
     sensitive: playing_sensitive,
@@ -209,9 +235,11 @@ static struct icon icons[] = {
     events: "enabled-changed rights-changed",
   },
   {
-    icon_on: "rtpenabled32.png",
+    toggle: TRUE,
+    stock: TRUE,
+    icon: GTK_STOCK_CONNECT,
+    label: "RTP",
     tip_on: "Stop playing network stream",
-    icon_off: "rtpdisabled32.png",
     tip_off: "Play network stream",
     menuitem: "<GdisorderMain>/Control/Network player",
     on: rtp_enabled,
@@ -232,36 +260,52 @@ static GtkWidget *balance_widget;
 
 /** @brief Create the control bar */
 GtkWidget *control_widget(void) {
-  GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox;
+  GtkWidget *hbox = gtk_hbox_new(FALSE, 1);
   int n;
 
   D(("control_widget"));
   assert(mainmenufactory);              /* ordering must be right */
+  toolbar = gtk_toolbar_new();
+  /* Don't permit overflow arrow as otherwise the toolbar isn't greedy enough
+   * in asking for space.  The ideal is probably to make the volume and balance
+   * sliders hang down from the toolbar so it unavoidably gets the whole width
+   * of the window to play with. */
+  gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar), FALSE);
+  gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), 
+                        full_mode ? GTK_TOOLBAR_BOTH : GTK_TOOLBAR_ICONS);
   for(n = 0; n < NICONS; ++n) {
-    /* Create the button */
-    icons[n].button = gtk_button_new();
+    struct icon *const icon = &icons[n];
+    icon->button = (icon->toggle
+                    ? GTK_WIDGET(gtk_toggle_tool_button_new())
+                    : GTK_WIDGET(gtk_tool_button_new(NULL, NULL)));
     gtk_widget_set_style(icons[n].button, tool_style);
-    icons[n].image_on = gtk_image_new_from_pixbuf(find_image(icons[n].icon_on));
-    gtk_widget_set_style(icons[n].image_on, tool_style);
-    g_object_ref(icons[n].image_on);
-    /* If it's a toggle icon, create the 'off' half too */
-    if(icons[n].icon_off) {
-      icons[n].image_off = gtk_image_new_from_pixbuf(find_image(icons[n].icon_off));
-      gtk_widget_set_style(icons[n].image_off, tool_style);
-      g_object_ref(icons[n].image_off);
+    if(icons[n].stock) {
+      /* We'll use the stock icons for this one */
+      icon->image = gtk_image_new_from_stock(icons[n].icon,
+                                             GTK_ICON_SIZE_LARGE_TOOLBAR);
+    } else {
+      /* Create the 'on' image */
+      icon->image = gtk_image_new_from_pixbuf(find_image(icons[n].icon));
     }
-    g_signal_connect(G_OBJECT(icons[n].button), "clicked",
-                     G_CALLBACK(clicked_icon), &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);
+    assert(icon->image);
+    gtk_tool_button_set_icon_widget(GTK_TOOL_BUTTON(icon->button),
+                                    icon->image);
+    gtk_tool_button_set_label(GTK_TOOL_BUTTON(icon->button),
+                                    icon->label);
+    if(icon->toggle)
+      g_signal_connect(G_OBJECT(icon->button), "toggled",
+                       G_CALLBACK(toggled_icon), icon);
+    else
+      g_signal_connect(G_OBJECT(icon->button), "clicked",
+                       G_CALLBACK(clicked_icon), icon);
+    gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
+                       GTK_TOOL_ITEM(icon->button),
+                       -1);
     if(icons[n].menuitem) {
       /* Find the menu item */
       icons[n].item = gtk_item_factory_get_widget(mainmenufactory,
                                                   icons[n].menuitem);
-      if(icons[n].icon_off)
+      if(icon->toggle)
         g_signal_connect(G_OBJECT(icons[n].item), "toggled",
                          G_CALLBACK(toggled_menu), &icons[n]);
       else
@@ -287,12 +331,16 @@ GtkWidget *control_widget(void) {
   gtk_widget_set_style(balance_widget, tool_style);
   gtk_scale_set_digits(GTK_SCALE(volume_widget), 10);
   gtk_scale_set_digits(GTK_SCALE(balance_widget), 10);
-  gtk_widget_set_size_request(volume_widget, 192, -1);
-  gtk_widget_set_size_request(balance_widget, 192, -1);
+  gtk_widget_set_size_request(volume_widget, 128, -1);
+  gtk_widget_set_size_request(balance_widget, 128, -1);
   gtk_widget_set_tooltip_text(volume_widget, "Volume");
   gtk_widget_set_tooltip_text(balance_widget, "Balance");
-  gtk_box_pack_start(GTK_BOX(hbox), volume_widget, FALSE, TRUE, 0);
-  gtk_box_pack_start(GTK_BOX(hbox), balance_widget, FALSE, TRUE, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), toolbar,
+                     FALSE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), volume_widget,
+                     FALSE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), balance_widget,
+                     FALSE/*expand*/, TRUE/*fill*/, 0);
   /* space updates rather than hammering the server */
   gtk_range_set_update_policy(GTK_RANGE(volume_widget), GTK_UPDATE_DELAYED);
   gtk_range_set_update_policy(GTK_RANGE(balance_widget), GTK_UPDATE_DELAYED);
@@ -308,29 +356,31 @@ GtkWidget *control_widget(void) {
                    G_CALLBACK(format_balance), 0);
   event_register("volume-changed", volume_changed, 0);
   event_register("rtp-changed", volume_changed, 0);
+  event_register("mini-mode-changed", control_minimode, 0);
   return hbox;
 }
 
+/** @brief Return TRUE if volume setting is supported */
+static int volume_supported(void) {
+  /* TODO: if the server doesn't know how to set the volume [but isn't using
+   * network play] then we should have volume_supported = FALSE */
+  return (!rtp_supported
+          || (rtp_supported && backend && backend->set_volume));
+}
+
 /** @brief Update the volume control when it changes */
 static void volume_changed(const char attribute((unused)) *event,
                            void attribute((unused)) *eventdata,
                            void attribute((unused)) *callbackdata) {
   double l, r;
-  gboolean volume_supported;
 
   D(("volume_changed"));
   ++suppress_actions;
   /* Only display volume/balance controls if they will work */
-  if(!rtp_supported
-     || (rtp_supported && backend && backend->set_volume))
-    volume_supported = TRUE;
-  else
-    volume_supported = FALSE;
-  /* TODO: if the server doesn't know how to set the volume [but isn't using
-   * network play] then we should have volume_supported = FALSE */
-  if(volume_supported) {
+  if(volume_supported()) {
     gtk_widget_show(volume_widget);
-    gtk_widget_show(balance_widget);
+    if(full_mode)
+      gtk_widget_show(balance_widget);
     l = volume_l / 100.0;
     r = volume_r / 100.0;
     gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto);
@@ -352,22 +402,14 @@ static void icon_changed(const char attribute((unused)) *event,
   int on = icon->on ? icon->on() : 1;
   int sensitive = icon->sensitive ? icon->sensitive() : 1;
   //fprintf(stderr, "sensitive->%d\n", sensitive);
-  GtkWidget *child, *newchild;
 
   ++suppress_actions;
   /* If the connection is down nothing is ever usable */
   if(!(last_state & DISORDER_CONNECTED))
     sensitive = 0;
-  //fprintf(stderr, "(checked connected) sensitive->%d\n", sensitive);
-  /* Replace the child */
-  newchild = on ? icon->image_on : icon->image_off;
-  child = gtk_bin_get_child(GTK_BIN(icon->button));
-  if(child != newchild) {
-    if(child)
-      gtk_container_remove(GTK_CONTAINER(icon->button), child);
-    gtk_container_add(GTK_CONTAINER(icon->button), newchild);
-    gtk_widget_show(newchild);
-  }
+  if(icon->toggle)
+    gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(icon->button),
+                                      on);
   /* If you disable play or random play NOT via the icon (for instance, via the
    * edit menu or via a completely separate command line invocation) then the
    * icon shows up as insensitive.  Hover the mouse over it and the correct
@@ -379,8 +421,9 @@ static void icon_changed(const char attribute((unused)) *event,
   gtk_widget_set_sensitive(icon->button, sensitive);
   /* Icons with an associated menu item */
   if(icon->item) {
-    if(icon->icon_off)
-      gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item), on);
+    if(icon->toggle)
+      gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item),
+                                     !!icon->menu_invert ^ !!on);
     gtk_widget_set_sensitive(icon->item, sensitive);
   }
   --suppress_actions;
@@ -392,13 +435,22 @@ static void icon_action_completed(void attribute((unused)) *v,
     popup_protocol_error(0, err);
 }
 
-static void clicked_icon(GtkButton attribute((unused)) *button,
+static void clicked_icon(GtkToolButton attribute((unused)) *button,
                          gpointer userdata) {
   const struct icon *icon = userdata;
 
   if(suppress_actions)
     return;
-  if(!icon->on || icon->on())
+  icon->action_go_off(client, icon_action_completed, 0);
+}
+
+static void toggled_icon(GtkToggleToolButton attribute((unused)) *button,
+                         gpointer user_data) {
+  const struct icon *icon = user_data;
+
+  if(suppress_actions)
+    return;
+  if(icon->on())
     icon->action_go_off(client, icon_action_completed, 0);
   else
     icon->action_go_on(client, icon_action_completed, 0);
@@ -411,7 +463,7 @@ static void clicked_menu(GtkMenuItem attribute((unused)) *menuitem,
 
 static void toggled_menu(GtkCheckMenuItem attribute((unused)) *menuitem,
                          gpointer userdata) {
-  clicked_icon(NULL, userdata);
+  toggled_icon(NULL, userdata);
 }
 
 /** @brief Called when a volume command completes */
@@ -559,6 +611,20 @@ static int disable_rtp(disorder_eclient attribute((unused)) *c,
   return 0;
 }
 
+static void control_minimode(const char attribute((unused)) *event,
+                             void attribute((unused)) *evendata,
+                             void attribute((unused)) *callbackdata) {
+  if(full_mode && volume_supported()) {
+    gtk_widget_show(balance_widget);
+    gtk_scale_set_value_pos(GTK_SCALE(volume_widget), GTK_POS_TOP);
+  } else {
+    gtk_widget_hide(balance_widget);
+    gtk_scale_set_value_pos(GTK_SCALE(volume_widget), GTK_POS_RIGHT);
+  }
+  gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), 
+                        full_mode ? GTK_TOOLBAR_BOTH : GTK_TOOLBAR_ICONS);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 9b050f4e1b1c5b796da72749263a1ad6410301f0..639eb73c2e74efba3e67a618177ba8740cb2ef52 100644 (file)
@@ -44,6 +44,9 @@ GtkWidget *report_label;
 /** @brief Main tab group */
 GtkWidget *tabs;
 
+/** @brief Mini-mode widget for playing track */
+GtkWidget *playing_mini;
+
 /** @brief Main client */
 disorder_eclient *client;
 
@@ -100,6 +103,10 @@ const char *server_version;
 /** @brief Parsed server version */
 long server_version_bytes;
 
+static GtkWidget *queue;
+
+static GtkWidget *notebook_box;
+
 static void check_rtp_address(const char *event,
                               void *eventdata,
                               void *callbackdata);
@@ -154,7 +161,7 @@ static GtkWidget *notebook(void) {
    * produces not too dreadful appearance */
   gtk_widget_set_style(tabs, tool_style);
   g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0);
-  gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(),
+  gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue = queue_widget(),
                            gtk_label_new("Queue"));
   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(),
                            gtk_label_new("Recent"));
@@ -165,18 +172,78 @@ static GtkWidget *notebook(void) {
   return tabs;
 }
 
+/* Tracking of window sizes */
+static int toplevel_width = 640, toplevel_height = 480;
+static int mini_width = 480, mini_height = 140;
+static struct timeval last_mode_switch;
+
+static void main_minimode(const char attribute((unused)) *event,
+                          void attribute((unused)) *evendata,
+                          void attribute((unused)) *callbackdata) {
+  if(full_mode) {
+    gtk_window_resize(GTK_WINDOW(toplevel), toplevel_width, toplevel_height);
+    gtk_widget_show(tabs);
+    gtk_widget_hide(playing_mini);
+    /* Show the queue (bit confusing otherwise!) */
+    gtk_notebook_set_current_page(GTK_NOTEBOOK(tabs), 0);
+  } else {
+    gtk_window_resize(GTK_WINDOW(toplevel), mini_width, mini_height);
+    gtk_widget_hide(tabs);
+    gtk_widget_show(playing_mini);
+  }
+  xgettimeofday(&last_mode_switch, NULL);
+}
+
+/* Called when the window size is allocate */
+static void toplevel_size_allocate(GtkWidget attribute((unused)) *w,
+                                   GtkAllocation *a,
+                                   gpointer attribute((unused)) user_data) {
+  struct timeval now;
+  xgettimeofday(&now, NULL);
+  if(tvdouble(tvsub(now, last_mode_switch)) < 0.5) {
+    /* Suppress size-allocate signals that are within half a second of a mode
+     * switch: they are quite likely to be the result of re-arranging widgets
+     * within the old size, not the application of the new size.  Yes, this is
+     * a disgusting hack! */
+    return;                             /* OMG too soon! */
+  }
+  if(full_mode) {
+    toplevel_width = a->width;
+    toplevel_height = a->height;
+  } else {
+    mini_width = a->width;
+    mini_height = a->height;
+  }
+}
+
+/* Periodically check the toplevel's size
+ * (the hack in toplevel_size_allocate() means we could in principle
+ * miss a user-initiated resize)
+ */
+static void check_toplevel_size(const char attribute((unused)) *event,
+                                void attribute((unused)) *evendata,
+                                void attribute((unused)) *callbackdata) {
+  GtkAllocation a;
+  gtk_window_get_size(GTK_WINDOW(toplevel), &a.width, &a.height);
+  toplevel_size_allocate(NULL, &a, NULL);
+}
+
 /** @brief Create and populate the main window */
 static void make_toplevel_window(void) {
-  GtkWidget *const vbox = gtk_vbox_new(FALSE, 1);
+  GtkWidget *const vbox = gtk_vbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
   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);
+  gtk_window_set_default_size(GTK_WINDOW(toplevel),
+                              toplevel_width, toplevel_height);
   /* terminate on close */
   g_signal_connect(G_OBJECT(toplevel), "delete_event",
                    G_CALLBACK(delete_event), NULL);
+  /* track size */
+  g_signal_connect(G_OBJECT(toplevel), "size-allocate",
+                   G_CALLBACK(toplevel_size_allocate), NULL);
   /* lay out the window */
   gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience");
   gtk_container_add(GTK_CONTAINER(toplevel), vbox);
@@ -191,13 +258,23 @@ static void make_toplevel_window(void) {
                      FALSE,             /* expand */
                      FALSE,             /* fill */
                      0);
-  gtk_container_add(GTK_CONTAINER(vbox), notebook());
+  playing_mini = playing_widget();
+  gtk_box_pack_start(GTK_BOX(vbox),
+                     playing_mini,
+                     FALSE,
+                     FALSE,
+                     0);
+  notebook_box = gtk_vbox_new(FALSE, 0);
+  gtk_container_add(GTK_CONTAINER(notebook_box), notebook());
+  gtk_container_add(GTK_CONTAINER(vbox), notebook_box);
   gtk_box_pack_end(GTK_BOX(vbox),
                    rb,
                    FALSE,             /* expand */
                    FALSE,             /* fill */
                    0);
   gtk_widget_set_style(toplevel, tool_style);
+  event_register("mini-mode-changed", main_minimode, 0);
+  event_register("periodic-fast", check_toplevel_size, 0);
 }
 
 static void userinfo_rights_completed(void attribute((unused)) *v,
@@ -477,6 +554,7 @@ int main(int argc, char **argv) {
   /* 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);
+  gtk_widget_hide(playing_mini);
   /* issue a NOP every so often */
   g_timeout_add_full(G_PRIORITY_LOW,
                      2000/*interval, ms*/,
@@ -490,9 +568,7 @@ int main(int argc, char **argv) {
   disorder_eclient_version(client, version_completed, 0);
   event_register("log-connected", check_rtp_address, 0);
   suppress_actions = 0;
-#if PLAYLISTS
   playlists_init();
-#endif
   /* If no password is set yet pop up a login box */
   if(!config->password)
     login_box();
index cafa48dcd75e9e90bdb6925f3d74adc786d44c66..719eea40dc98931e603f22c27c483e00cb42ec38 100644 (file)
@@ -85,6 +85,11 @@ struct button {
   void (*clicked)(GtkButton *button, gpointer userdata);
   const char *tip;
   GtkWidget *widget;
+  void (*pack)(GtkBox *box,
+               GtkWidget *child,
+               gboolean expand,
+               gboolean fill,
+               guint padding);
 };
 
 /* Variables --------------------------------------------------------------- */
@@ -117,7 +122,8 @@ void popup_protocol_error(int code,
                           const char *msg);
 /* Report an error */
 
-void properties(int ntracks, const char **tracks);
+void properties(int ntracks, const char **tracks,
+                GtkWidget *parent);
 /* Pop up a properties window for a list of tracks */
 
 GtkWidget *scroll_widget(GtkWidget *child);
@@ -134,7 +140,8 @@ void popup_submsg(GtkWidget *parent, GtkMessageType mt, const char *msg);
 
 void fpopup_msg(GtkMessageType mt, const char *fmt, ...);
 
-struct progress_window *progress_window_new(const char *title);
+struct progress_window *progress_window_new(const char *title,
+                                            GtkWidget *parent);
 /* Pop up a progress window */
 
 void progress_window_progress(struct progress_window *pw,
@@ -159,6 +166,7 @@ void all_update(void);
 
 GtkWidget *menubar(GtkWidget *w);
 /* Create the menu bar */
+int full_mode;
 
 void users_set_sensitive(int sensitive);
 
@@ -172,6 +180,7 @@ extern int suppress_actions;
 /* Queue/Recent/Added */
 
 GtkWidget *queue_widget(void);
+GtkWidget *playing_widget(void);
 GtkWidget *recent_widget(void);
 GtkWidget *added_widget(void);
 /* Create widgets for displaying the queue, the recently played list and the
@@ -212,6 +221,8 @@ void choose_update(void);
 void play_completed(void *v,
                     const char *err);
 
+extern const GtkTargetEntry choose_targets[];
+
 /* Login details */
 
 void login_box(void);
@@ -224,7 +235,7 @@ void manage_users(void);
 
 /* Help */
 
-void popup_help(void);
+void popup_help(const char *what);
 
 /* RTP */
 
@@ -253,17 +264,15 @@ void popup_settings(void);
 
 /* Playlists */
 
-#if PLAYLISTS
 void playlists_init(void);
-void edit_playlists(gpointer callback_data,
-                    guint callback_action,
-                    GtkWidget  *menu_item);
+void playlist_window_create(gpointer callback_data,
+                            guint callback_action,
+                            GtkWidget  *menu_item);
 extern char **playlists;
 extern int nplaylists;
-extern GtkWidget *playlists_widget;
+extern GtkWidget *menu_playlists_widget;
 extern GtkWidget *playlists_menu;
-extern GtkWidget *editplaylists_widget;
-#endif
+extern GtkWidget *menu_editplaylists_widget;
 
 #endif /* DISOBEDIENCE_H */
 
index 8ec405cd206f072fb64f913674dc454cb4f28129..ca5a8d8d90af85f3925c4be5e12520562b73fff6 100644 (file)
 #include <unistd.h>
 
 /** @brief Display the manual page */
-void popup_help(void) {
+void popup_help(const char *what) {
   char *path;
   pid_t pid;
   int w;
 
-  byte_xasprintf(&path, "%s/disobedience.html", pkgdatadir);
+  if(!what)
+    what = "index.html";
+#if __APPLE__
+  if(!strcmp(browser, "open"))
+    /* Apple's open(1) isn't really a web browser so needs some extra hints
+     * that it should see the argument as a URL.  Otherwise it doesn't treat #
+     * specially.  A better answer would be to identify the system web browser
+     * and invoke it directly. */
+    byte_xasprintf(&path, "file:///%s/%s", dochtmldir, what);
+  else
+#endif
+    byte_xasprintf(&path, "%s/%s", dochtmldir, what);
   if(!(pid = xfork())) {
     exitfn = _exit;
     if(!xfork()) {
index f1c4f79d814597863d0a717425a484871c0b2789..71d2af04bf32c006e91a4dff7642a251be0d1265 100644 (file)
@@ -115,6 +115,7 @@ static void log_moved(void attribute((unused)) *v,
 static void log_playing(void attribute((unused)) *v,
                         const char attribute((unused)) *track,
                         const char attribute((unused)) *user) {
+  event_raise("playing-started", 0);
 }
 
 /** @brief Called when a track is added to the queue */
index 90eaa0dde4ddcd30dbd7b4f9065b35a2720538c4..0c6e7d74dd44756d10f123f6bbaba055aafc59e3 100644 (file)
@@ -232,6 +232,12 @@ static void login_cancel(GtkButton attribute((unused)) *button,
   gtk_widget_destroy(login_window);
 }
 
+/** @brief User pressed cancel in the login window */
+static void login_help(GtkButton attribute((unused)) *button,
+                       gpointer attribute((unused)) userdata) {
+  popup_help("intro.html#login");
+}
+
 /** @brief Keypress handler */
 static gboolean login_keypress(GtkWidget attribute((unused)) *widget,
                                GdkEventKey *event,
@@ -253,16 +259,25 @@ static gboolean login_keypress(GtkWidget attribute((unused)) *widget,
 /* Buttons that appear at the bottom of the window */
 static struct button buttons[] = {
   {
-    "Login",
-    login_ok,
-    "(Re-)connect using these settings",
-    0
+    GTK_STOCK_HELP,
+    login_help,
+    "Go to manual",
+    0,
+    gtk_box_pack_start,
   },
   {
     GTK_STOCK_CLOSE,
     login_cancel,
     "Discard changes and close window",
-    0
+    0,
+    gtk_box_pack_end,
+  },
+  {
+    "Login",
+    login_ok,
+    "(Re-)connect using these settings",
+    0,
+    gtk_box_pack_end,
   },
 };
 
diff --git a/disobedience/manual/Makefile.am b/disobedience/manual/Makefile.am
new file mode 100644 (file)
index 0000000..cd607f0
--- /dev/null
@@ -0,0 +1,30 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2009 Richard Kettlewell
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+dist_dochtml_DATA=index.html intro.html misc.html playlists.html       \
+       properties.html tabs.html window.html disobedience.css          \
+       arch-simple.png button-pause.png button-playing.png             \
+       button-random.png button-rtp.png button-scratch.png             \
+       choose-search.png choose.png disobedience-debian-menu.png       \
+       disobedience-terminal.png disorder-email-confirm.png            \
+       disorder-web-login.png login.png menu-control.png               \
+       menu-edit.png menu-help.png menu-server.png                     \
+       playlist-create.png playlist-picker-menu.png                    \
+       playlist-popup-menu.png playlist-window.png queue-menu.png      \
+       queue.png queue2.png recent.png track-properties.png            \
+       volume-slider.png
diff --git a/disobedience/manual/arch-simple.png b/disobedience/manual/arch-simple.png
new file mode 100644 (file)
index 0000000..c7d965a
Binary files /dev/null and b/disobedience/manual/arch-simple.png differ
diff --git a/disobedience/manual/arch-simple.svg b/disobedience/manual/arch-simple.svg
new file mode 100644 (file)
index 0000000..a03d78a
--- /dev/null
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="400"
+   height="400"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   version="1.0"
+   sodipodi:docname="arch-simple.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   inkscape:export-filename="/home/richard/src/disorder.dmanual/disobedience/manual/arch-simple.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="151.50215"
+     inkscape:cy="200"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:snap-global="true"
+     inkscape:window-width="1104"
+     inkscape:window-height="672"
+     inkscape:window-x="321"
+     inkscape:window-y="297">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2383"
+       visible="true"
+       enabled="true"
+       spacingx="8px"
+       spacingy="8px"
+       empspacing="4"
+       color="#007fff"
+       opacity="0.1254902" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.96492909999999998;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3224"
+       width="335.9653"
+       height="304.03506"
+       x="7.9824643"
+       y="7.9824643" />
+    <g
+       id="g3161"
+       transform="translate(-16.229471,-255.77051)">
+      <rect
+         y="272.22946"
+         x="32.229473"
+         height="63.541054"
+         width="128.54106"
+         id="rect2385"
+         style="fill:#00ffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text3157"
+         y="312"
+         x="56"
+         style="font-size:24px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           y="312"
+           x="56"
+           id="tspan3159"
+           sodipodi:role="line">Server</tspan></text>
+    </g>
+    <rect
+       style="fill:#00ffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3166"
+       width="160"
+       height="64"
+       x="176.54106"
+       y="16" />
+    <text
+       xml:space="preserve"
+       style="font-size:20px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="187.13188"
+       y="55.456055"
+       id="text3168"><tspan
+         sodipodi:role="line"
+         id="tspan3170"
+         x="187.13188"
+         y="55.456055">Web interface</tspan></text>
+    <rect
+       style="fill:#00ffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3174"
+       width="128.54106"
+       height="63.541054"
+       x="15.053802"
+       y="144.95049" />
+    <text
+       xml:space="preserve"
+       style="font-size:8px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="45.493561"
+       y="208.58369"
+       id="text3176"><tspan
+         sodipodi:role="line"
+         id="tspan3178"
+         x="45.493561"
+         y="208.58369" /></text>
+    <text
+       xml:space="preserve"
+       style="font-size:20px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="25.344841"
+       y="182.23859"
+       id="text3180"><tspan
+         sodipodi:role="line"
+         id="tspan3182"
+         x="25.344841"
+         y="182.23859">RTP player</tspan></text>
+    <rect
+       style="fill:#00ff00;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3184"
+       width="128.54106"
+       height="63.541054"
+       x="16"
+       y="240.45895" />
+    <text
+       xml:space="preserve"
+       style="font-size:20px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="23.89846"
+       y="279.68555"
+       id="text3186"><tspan
+         sodipodi:role="line"
+         id="tspan3188"
+         x="23.89846"
+         y="279.68555">Sound card</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 46.824329,80.72103 L 46.824329,112.72103"
+       id="path3190" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 16.980579,112.72103 L 328.98058,112.72103"
+       id="path3192" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 80.541067,112 L 80.541067,144"
+       id="path3194" />
+    <text
+       xml:space="preserve"
+       style="font-size:10px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="62.652657"
+       y="104.48068"
+       id="text3196"><tspan
+         sodipodi:role="line"
+         id="tspan3198"
+         x="62.652657"
+         y="104.48068">Local Network</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 80.541067,208 L 80.541067,240"
+       id="path3207" />
+    <rect
+       style="font-size:20px;fill:#00ffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3209"
+       width="160.45293"
+       height="63.147457"
+       x="176.08813"
+       y="144.85254" />
+    <text
+       xml:space="preserve"
+       style="font-size:20px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="188.28726"
+       y="183.88232"
+       id="text3211"><tspan
+         sodipodi:role="line"
+         id="tspan3213"
+         x="188.28726"
+         y="183.88232">Disobedience</tspan></text>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3.13968921;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 240.54107,112 L 240.54107,143.96567"
+       id="path3218" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 144.54107,48 L 176.54107,48"
+       id="path3220" />
+  </g>
+</svg>
diff --git a/disobedience/manual/button-pause.png b/disobedience/manual/button-pause.png
new file mode 100644 (file)
index 0000000..565f227
Binary files /dev/null and b/disobedience/manual/button-pause.png differ
diff --git a/disobedience/manual/button-playing.png b/disobedience/manual/button-playing.png
new file mode 100644 (file)
index 0000000..7b6fb2a
Binary files /dev/null and b/disobedience/manual/button-playing.png differ
diff --git a/disobedience/manual/button-random.png b/disobedience/manual/button-random.png
new file mode 100644 (file)
index 0000000..9791c2c
Binary files /dev/null and b/disobedience/manual/button-random.png differ
diff --git a/disobedience/manual/button-rtp.png b/disobedience/manual/button-rtp.png
new file mode 100644 (file)
index 0000000..ad0d749
Binary files /dev/null and b/disobedience/manual/button-rtp.png differ
diff --git a/disobedience/manual/button-scratch.png b/disobedience/manual/button-scratch.png
new file mode 100644 (file)
index 0000000..c90bfc5
Binary files /dev/null and b/disobedience/manual/button-scratch.png differ
diff --git a/disobedience/manual/choose-search.png b/disobedience/manual/choose-search.png
new file mode 100644 (file)
index 0000000..bc28194
Binary files /dev/null and b/disobedience/manual/choose-search.png differ
diff --git a/disobedience/manual/choose.png b/disobedience/manual/choose.png
new file mode 100644 (file)
index 0000000..2f88ece
Binary files /dev/null and b/disobedience/manual/choose.png differ
diff --git a/disobedience/manual/disobedience-debian-menu.png b/disobedience/manual/disobedience-debian-menu.png
new file mode 100644 (file)
index 0000000..49bbdb4
Binary files /dev/null and b/disobedience/manual/disobedience-debian-menu.png differ
diff --git a/disobedience/manual/disobedience-terminal.png b/disobedience/manual/disobedience-terminal.png
new file mode 100644 (file)
index 0000000..5cf2e8d
Binary files /dev/null and b/disobedience/manual/disobedience-terminal.png differ
diff --git a/disobedience/manual/disobedience.css b/disobedience/manual/disobedience.css
new file mode 100644 (file)
index 0000000..04b6f80
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* default font and colors */
+body {
+  color: black;
+  background-color: white;
+  font-family: times,serif;
+  font-weight: normal;
+  font-size: 12pt;
+  font-variant: normal
+}
+
+/* general link colors */
+a {
+  color: blue;
+  text-decoration: none
+}
+
+h2 a {
+  color: black
+}
+
+p.chapter a {
+  color: black;
+  font-family: helvetica,sans-serif;
+  font-weight: bold;
+  font-size: 18pt
+}
+
+a:active {
+  color: red
+}
+
+a:visited {
+  color: red
+}
+
+/* title bars */
+h1 {
+  font-family: helvetica,sans-serif;
+  font-weight: bold;
+  font-size: 18pt;
+  font-variant: normal;
+  text-align: center;
+  border: 1px solid black;
+  padding: 0.2em;
+  background-color: #e0e0e0;
+  display: block
+}
+
+/* secondary titles */
+h2 {
+  font-family: helvetica,sans-serif;
+  font-weight: bold;
+  font-size: 16pt;
+  font-variant: normal;
+  display: block
+}
+
+td {
+  vertical-align: top;
+  padding: 8px
+}
+
+td:first-child {
+  text-align: right
+}
+
+table {
+  margin-left: 2em
+}
+
+p.image {
+  text-align: center
+}
diff --git a/disobedience/manual/disorder-email-confirm.png b/disobedience/manual/disorder-email-confirm.png
new file mode 100644 (file)
index 0000000..c535a67
Binary files /dev/null and b/disobedience/manual/disorder-email-confirm.png differ
diff --git a/disobedience/manual/disorder-web-login.png b/disobedience/manual/disorder-web-login.png
new file mode 100644 (file)
index 0000000..3155a4c
Binary files /dev/null and b/disobedience/manual/disorder-web-login.png differ
diff --git a/disobedience/manual/index.html b/disobedience/manual/index.html
new file mode 100644 (file)
index 0000000..7245024
--- /dev/null
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>Disobedience</h1>
+
+   <p>This is the manual for Disobedience, the graphical client
+   for <a href="http://www.greenend.org.uk/rjk/disorder/">DisOrder</a>.</p>
+
+   <p class=chapter><a href="intro.html">1. Introduction</a></p>
+   <ul>
+     <li>What DisOrder and Disobedience are, and how to get them</li>
+     <li>How to get a DisOrder login</li>
+     <li>How to start Disobedience</li>
+   </ul>
+
+   <p class=chapter><a href="window.html">2. Window Layout</a></p>
+
+   <ul>
+     <li>A tour of the Disobedience window</li>
+   </ul>
+
+   <p class=chapter><a href="tabs.html">3. Tabs</a></p>
+
+   <ul>
+     <li>Detailed descriptions of
+       the <b>Queue</b>, <b>Recent</b>, <b>Choose</b> and <b>Added</b>
+       tabs</li>
+   </ul>
+
+   <p class=chapter><a href="properties.html">4. Track Properties</a></p>
+
+   <ul>
+     <li>How to edit track properties</li>
+   </ul>
+
+   <p class=chapter><a href="playlists.html">5. Playlists</a></p>
+
+   <ul>
+     <li>What playlists are</li>
+     <li>Editing playlists</li>
+   </ul>
+
+   <p class=chapter><a href="misc.html">Appendix</a></p>
+
+   <ul>
+     <li>Network play</li>
+     <li>Reporting bugs</li>
+     <li>Copyright notice</li>
+   </ul>
+
+ </body>
+</html>
diff --git a/disobedience/manual/intro.html b/disobedience/manual/intro.html
new file mode 100644 (file)
index 0000000..3fb0451
--- /dev/null
@@ -0,0 +1,201 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Introduction</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>1. Introduction</h1>
+
+   <p>This chapter covers the following topics:</p>
+
+   <ul>
+     <li>What DisOrder and Disobedience are, and how to get them</li>
+     <li>How to get a DisOrder login</li>
+     <li>How to start Disobedience</li>
+   </ul>
+
+   <h2><a name=whatis>1.1 What is DisOrder?</a></h2>
+
+   <p><a href="http://www.greenend.org.uk/rjk/disorder/">DisOrder</a>
+   is a multi-user software jukebox.  It allows MP3s, OGGs, etc to be
+   played either using a single sound card or over a network to many
+   different computers, with multiple different people controlling
+   what is played.</p>
+
+   <p>DisOrder has three main user interfaces.</p>
+
+   <ul>
+     <li>It has a command-line interface, suitable for ad-hoc use and
+     scripting.</li>
+
+     <li>It has a web interface, usable with graphical web browsers
+     (Firefox, Internet Explorer etc).</li>
+
+     <li>It has a graphical client called Disobedience.</li>
+   </ul>
+
+   <p>This manual is about Disobedience, so it does not really cover
+   installation or management issues.  However in this chapter it will
+   cover a few such topics as they are necessary to getting up and
+   running with Disobedience.</p>
+
+   <!--
+
+   <p>This diagram shows an overview of one possible setup.</p>
+
+   <p class=image><img src="arch-simple.png"></p>
+
+   <p>The server and web interface run on one computer.  Disobedience
+   runs on a desktop computer and accesses the server via the network.
+   On another system the RTP player runs and plays sound received from
+   the server via its local sound card.</p>
+
+   <p>Many other configurations are possible.  For instance the server
+   could play directly to a local soundcard.  Also if Disobedience
+   runs on the same computer as the RTP player then it can be used to
+   stop and start the player.  Of course Disobedience can also be run
+   on the same computer as the server provided it can run X11
+   applications.</p>
+
+   -->
+
+   <h2><a name=getting>1.2 Getting DisOrder</a></h2>
+
+   <p>There are two ways to get DisOrder.</p>
+
+   <p>If you have a Debian system you can download the <tt>.deb</tt>
+   files <a href="http://www.greenend.org.uk/rjk/disorder/">from
+   DisOrder's home page</a> and install those.  There are four
+   packages to choose from:</p>
+
+   <ul>
+     <li><tt>disorder.deb</tt> - the base package.  You should always
+     install this.  It contains the command-line client.</li>
+
+     <li><tt>disorder-server.deb</tt> - the server and web interface.
+     Only install this if you are setting up a totally new DisOrder
+     installation.  If you just want to access an existing one, you
+     don't need this package.</li>
+
+     <li><tt>disobedience.deb</tt> - the graphical client.  If you are
+     reading this manual you want this package!</li>
+
+     <li><tt>disorder-rtp.deb</tt> - the network play client.  If your
+     server is set up to transmit sound over the network you will need
+     this.  If it uses a local sound card then this package won't be
+     useful to you.</li>
+
+   </ul>
+
+   <p>(At the time of writing, DisOrder is not included as part of
+   Debian.)</p>
+
+   <p>If you have another kind of Linux system, or a Mac, you must
+   build from source code.  See the <tt>README</tt> file included in
+   the source distribution for more details.  Note that to use
+   Disobedience on a Mac, you will need X11.app.</p>
+
+   <p>There is no Windows support (although the web interface can be
+   used from Windows computers).</p>
+
+   <h2><a name=createlogin>1.3 Getting a DisOrder login</a></h2>
+
+   <p>The easiest way to get a DisOrder login is to access the web
+   interface and set one up using that.  To do this,
+   visit <tt>http://HOSTNAME/cgi-bin/disorder</tt>,
+   where <tt>HOSTNAME</tt> is the name of the server where DisOrder is
+   installed.  You should then be able to select the <b>Login</b>
+   option at the top of the screen.</p>
+
+   <p class=image><img src="disorder-web-login.png"></p>
+
+   <p>Go to the <b>New Users</b> form and enter the username you want
+   to use, your email address, and a password.  The password must be
+   entered twice to verify you did not mistype it (since it won't be
+   displayed on the screen).  When you press <b>Register</b>, you will
+   be sent an email requiring you to confirm your registration.</p>
+
+   <p class=image><img src="disorder-email-confirm.png"></p>
+
+   <p>Your login won't be active until you click on this URL.</p>
+
+   <p>(It might be that your installation isn't set up to allow
+   automatic registration.  In that case the local sysadmin will have
+   to create your login and set your initial password by hand.)</p>
+
+   <p>Having done this you could of course just use the web interface.
+   But since this is the manual for Disobedience, it is assumed that
+   you want to take advantage of its more convenient design and extra
+   features.</p>
+
+   <h2><a name=starting>1.4 Starting Disobedience</a></h2>
+
+   <p>On Debian systems it should be possible to find Disobedience in
+   the menu system:</p>
+
+   <p class=image><img src="disobedience-debian-menu.png"></p>
+
+   <p>On other systems you will have to start it from the command line
+   by typing its name at a command prompt.  You can (optionally) use
+   an <tt>&amp;</tt> suffix to stop it tying up your terminal.</p>
+
+   <p class=image><img src="disobedience-terminal.png"></p>
+
+   <p>(Please note that Disobedience shouldn't write any messages to
+   the terminal.  If it does that probably indicates a bug, which
+   should <a href="http://code.google.com/p/disorder/issues/list">be
+   reported</a>.)</p>
+
+   <h2><a name=login>1.5 Initial Login</a></h2>
+
+   <p>The first time you run Disobedience it won't know what server to
+   connect to, your username or your password, and will therefore
+   display a login box.</p>
+
+   <p class=image><img src="login.png"></p>
+
+   <p>If Disobedience is running on a different computer to the
+   server, then you should make sure the <b>Remote</b> box is ticked
+   and fill in the host name (or IP address) and port number
+   (&ldquo;Service&rdquo;).  If you don't know what values to use
+   here, ask your local sysadmin.  If, on the other hand, Disobedience
+   is running on the <i>same</i> computer as the server then you can
+   leave the <b>Remote</b> box clear and it should be able to connect
+   to it without using the network.</p>
+
+   <p>In any case, you will need to enter your username and
+   password, as set up earlier.</p>
+
+   <p>Once you have logged in successfuly, Disobedience will remember
+   these login settings, so it should not be necessary to enter them
+   again.  If you need to change them for any reason, you can either
+   select the <b>Server > Login</b> option to bring the login window
+   back, or (if you prefer), edit the file <tt>~/.disorder/passwd</tt>
+   directly.</p>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
diff --git a/disobedience/manual/login.png b/disobedience/manual/login.png
new file mode 100644 (file)
index 0000000..9f3b074
Binary files /dev/null and b/disobedience/manual/login.png differ
diff --git a/disobedience/manual/menu-control.png b/disobedience/manual/menu-control.png
new file mode 100644 (file)
index 0000000..c2da17b
Binary files /dev/null and b/disobedience/manual/menu-control.png differ
diff --git a/disobedience/manual/menu-edit.png b/disobedience/manual/menu-edit.png
new file mode 100644 (file)
index 0000000..7cdb83b
Binary files /dev/null and b/disobedience/manual/menu-edit.png differ
diff --git a/disobedience/manual/menu-help.png b/disobedience/manual/menu-help.png
new file mode 100644 (file)
index 0000000..4ecc391
Binary files /dev/null and b/disobedience/manual/menu-help.png differ
diff --git a/disobedience/manual/menu-server.png b/disobedience/manual/menu-server.png
new file mode 100644 (file)
index 0000000..5502bf5
Binary files /dev/null and b/disobedience/manual/menu-server.png differ
diff --git a/disobedience/manual/misc.html b/disobedience/manual/misc.html
new file mode 100644 (file)
index 0000000..b65b6fa
--- /dev/null
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Appendix</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>Appendix</h1>
+
+   <h2><a name=netplay>Network Play</a></h2>
+
+   <p>Network play uses a background copy
+   of <tt>disorder-playrtp</tt>.  If you quit Disobedience the
+   player will continue playing and can be disabled from a later
+   run of Disobedience.</p>
+
+   <p>The player will log to <tt>~/.disorder/HOSTNAME-rtp.log</tt> so
+   look there if it does not seem to be working.</p>
+
+   <p>You can stop it without running Disobedience by the command
+   <tt>killall disorder-playrtp</tt>.</p>
+
+   <h2><a name=bugs>Reporting Bugs</a></h2>
+
+   <p>Please report bugs using
+   DisOrder's <a href="http://code.google.com/p/disorder/issues/list">bug
+   tracker</a>.</p>
+
+   <p>Known problems include:</p>
+
+   <ul>
+
+     <li>There is no particular provision for multiple users of the
+     same computer sharing a single <tt>disorder-playrtp</tt> process.
+     This shouldn't be too much of a problem in practice but something
+     could perhaps be done given demand.</li>
+
+     <li>Try to do remote user management when the server is
+     configured to refuse this produces rather horrible error
+     behavior.</li>
+
+     <li>Resizing columns doesn't work very well.  This is a GTK+
+     bug.</li>
+
+   </ul>
+
+   <h2><a name=copyright>Copyright Notice</a></h2>
+
+   <p>Copyright &copy; 2003-2009 <a
+   href="http://www.greenend.org.uk/rjk/">Richard Kettlewell</a><br>
+
+   Portions copyright &copy; 2007 <a
+   href="http://www.chiark.greenend.org.uk/~ryounger/">Ross
+   Younger</a><br>
+
+   Portions copyright &copy; 2007, 2008 Mark Wooding<br>
+
+   Portions extracted from <a
+   href="http://mpg321.sourceforge.net/">MPG321</a>, Copyright &copy; 2001 Joe
+   Drew, Copyright &copy; 2000-2001 Robert Leslie<br>
+
+   Portions copyright &copy; 1997-2006 <a
+   href="http://www.fsf.org/">Free Software Foundation, Inc</a><br>
+   
+   Portions Copyright &copy; 2000 <a href="http://www.redhat.com">Red Hat,
+   Inc.</a>, Jonathan Blandford <jrb@redhat.com></p>
+
+   <p>This program is free software: you can redistribute it and/or modify it
+   under the terms of the <a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU
+   General Public License</a> as published by the Free Software Foundation,
+   either version 3 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, see <a
+   href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.</p>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
diff --git a/disobedience/manual/playlist-create.png b/disobedience/manual/playlist-create.png
new file mode 100644 (file)
index 0000000..f48997d
Binary files /dev/null and b/disobedience/manual/playlist-create.png differ
diff --git a/disobedience/manual/playlist-picker-menu.png b/disobedience/manual/playlist-picker-menu.png
new file mode 100644 (file)
index 0000000..be5a65b
Binary files /dev/null and b/disobedience/manual/playlist-picker-menu.png differ
diff --git a/disobedience/manual/playlist-popup-menu.png b/disobedience/manual/playlist-popup-menu.png
new file mode 100644 (file)
index 0000000..0bb673d
Binary files /dev/null and b/disobedience/manual/playlist-popup-menu.png differ
diff --git a/disobedience/manual/playlist-window.png b/disobedience/manual/playlist-window.png
new file mode 100644 (file)
index 0000000..9ffd991
Binary files /dev/null and b/disobedience/manual/playlist-window.png differ
diff --git a/disobedience/manual/playlists.html b/disobedience/manual/playlists.html
new file mode 100644 (file)
index 0000000..bef0c18
--- /dev/null
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Playlists</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>5. Playlists</h1>
+
+   <p>The chapter describes playlist and how to use them.</p>
+
+   <h2><a name=what>5.1 What are playlists?</a></h2>
+
+   <p>A playlist is a named list of tracks stored by the server.  It
+   can be edit by either just its owner or, alternatively, by all
+   users.  It can be played as a unit when required.</p>
+
+   <p>Playlists fall into three categories:</p>
+
+   <table>
+     <tr>
+       <td><b>Shared</b></td>
+       <td>Shared playlists have no owner and can be seen and edited
+       by anybody.</td>
+     </tr>
+
+     <tr>
+       <td><b>Public</b></td>
+       <td>Public playlist are owned by their creator and can be seen
+       by anybody.  Only their creator can edit them, however.</td>
+     </tr>
+
+     <tr>
+       <td><b>Private</b></td>
+       <td>Private playlists are owned by their creator and can only
+       be seen or edited by their creator.</td>
+     </tr>
+
+   </table>
+
+   <p>To bring up the playlist window, select <b>Edit > Edit
+   Playlists</b> from the menu.</p>
+
+   <h2><a name=creating>5.2 Creating Playlists</a></h2>
+
+   <p>To create a playlist, click the <b>Add</b> button in the
+   playlist editor.  This will create a pop-up window where you can
+   enter the name of the new playlist and decide whether it is
+   shared, public or private.</p>
+
+   <p>Only Roman letters (without any accents) and digits are allowed
+   in playlist names.</p>
+
+   <p class=image><img src="playlist-create.png"></p>
+
+   <h2><a name=editing>5.3 Editing Playlists</a></h2>
+
+   <p>You can select the playlist to edit from the left side of the
+   window.  Shared playlists are listed first, then the public
+   playlists of each user (plus private playlists that you own).</p>
+
+   <p class=image><img src="playlist-window.png"></p>
+
+   <p>When a playlist is selected, you can edit in the right hand half
+   of the playlist window.  The editor is very similar in structure
+   and function to the <a href="tabs.html#queue">queue</a>.  You can
+   rearrange tracks within it by drag and drop and you can drag tracks
+   from the <b>Choose</b> tab into it.</p>
+
+   <p>Right clicking will create a pop-up menu which you can use to
+   play indivudual tracks or the whole playlist, as well as the
+   usual options as found in the <b>Queue</b> tab.</p>
+
+   <p class=image><img src="playlist-popup-menu.png"></p>
+
+   <h2><a name=playing>5.3 Playing Playlists</a></h2>
+
+   <p>There are three ways to play a playlist:</p>
+
+   <ol>
+
+     <li>You can use <b>Control > Activate Playlist</b> from the main
+     menu.</li>
+
+     <li>Right clicking in either half of the playlist editor will
+     create a pop-up menu.  There is an option to play this
+     playlist.</li>
+
+     <li>You can select all the tracks and drag them to the desired
+     point in the <b>Queue</b> tab.</li>
+
+   </ol>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
diff --git a/disobedience/manual/properties.html b/disobedience/manual/properties.html
new file mode 100644 (file)
index 0000000..34d215f
--- /dev/null
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Track Properties</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>4. Track Properties</h1>
+
+   <p>The chapter describes how to edit track properties.</p>
+
+   <p class=image><img src="track-properties.png"></p>
+
+   <p>This window can be invoked from any of the four tabs by
+   selecting one or more tracks and then either selected <b>Edit >
+   Track Properties</b> or via the right-click pop-up menu.</p>
+
+   <h2><a name=names>4.1 Track Name Parts</a></h2>
+
+   <p>The first three fields for each track are the parts of its name:
+   the artist, album and title.  These control what appear in the
+   similarly named columns in the queue and other tabs.  If they are
+   wrong then you can edit them here to correct them.</p>
+
+   <p>The double-headed arrow at the right of each field will copy the
+   current field value to all the other tracks in the window.  For
+   instance if an album name is mis-spelled or wrong then you could
+   follow the following procedure to correct it for all its
+   tracks:</p>
+
+   <ol>
+
+     <li>In the <b>Choose</b> tab, select all the tracks in the album.</li>
+
+     <li>Select <b>Edit > Track Properties</b> to bring up the track
+     properties window.</li>
+
+     <li>Edit the album name in the first track.</li>
+
+     <li>Click the arrow button to the right of the corrected version.</li>
+
+     <li>Click the <b>OK</b> button.</li>
+
+   </ol>
+
+   <h2><a name=tags>4.2 Tags</a></h2>
+
+   <p>Each track has an associated collection of tags.  These can used
+   when searching for tracks in the <b>Choose</b> tab or to control
+   which tracks are picked at random (although this functionality is
+   not readily available in current versions of Disobedience).</p>
+
+   <p>To add tags to a track enter the tags you want to apply to it in
+   the <b>Tags</b> field, separated by commas.  Tags cannot contain
+   commas and are compared without regard to whitespace.</p>
+
+   <h2><a name=weight>4.3 Track Weight</a></h2>
+
+   <p>Every track has an associated weight.  A higher weight makes the
+   track more likely to be picked at random and lower weight makes it
+   less likely to be picked at random.  (In the simplest case the
+   probability that it will be picked is equal to its weight divided
+   by the total weight of all tracks, although there are a number of
+   other factors that modify this.)</p>
+
+   <p>If no weight has been explicitly set then the track gets a
+   default weight of 90,000.</p>
+
+   <p>One way to prevent a track being picked at random would be to
+   set its weight to zero, but in fact there is a box you can untick
+   to suppress random selection of tracks too.</p>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
diff --git a/disobedience/manual/queue-menu.png b/disobedience/manual/queue-menu.png
new file mode 100644 (file)
index 0000000..af2dff9
Binary files /dev/null and b/disobedience/manual/queue-menu.png differ
diff --git a/disobedience/manual/queue.png b/disobedience/manual/queue.png
new file mode 100644 (file)
index 0000000..224fc71
Binary files /dev/null and b/disobedience/manual/queue.png differ
diff --git a/disobedience/manual/queue2.png b/disobedience/manual/queue2.png
new file mode 100644 (file)
index 0000000..ff5cbed
Binary files /dev/null and b/disobedience/manual/queue2.png differ
diff --git a/disobedience/manual/recent.png b/disobedience/manual/recent.png
new file mode 100644 (file)
index 0000000..8ee5e68
Binary files /dev/null and b/disobedience/manual/recent.png differ
diff --git a/disobedience/manual/tabs.html b/disobedience/manual/tabs.html
new file mode 100644 (file)
index 0000000..ea63a37
--- /dev/null
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Tabs</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>3. Tabs</h1>
+
+   <p>The chapter contains detailed descriptions of the Queue, Recent,
+   Choose and Added tabs.</p>
+
+   <h2><a name=queue>3.1 The Queue</a></h2>
+
+   <p>The <b>Queue</b> tab has already
+   been <a href="window.html#main">briefly described</a>, but there
+   are some more things to say about.  To start with, the meaning of
+   the columns:</p>
+
+   <table>
+     <tr>
+       <td><b>When</b></td>
+       <td>This is the server's estimate of when the track will start.
+       If play is disabled but the queue isn't empty, then it is based
+       on the assumption that play is about to be enabled.</td>
+     </tr>
+     <tr>
+       <td><b>Who</b></td>
+       <td>This is the person who selected the track.  If the track
+       was picked at random by the server it is empty.  You can
+       &ldquo;adopt&rdquo; a randomly picked track; see below.</td>
+     </tr>
+     <tr>
+       <td><b>Artist</b></td>
+       <td>The artist responsible for the track.  Like the other track
+       name columns, this depends on the server's logic for parsing
+       filename, so can be a bit wrong.  You can edit the track
+       properties to correct it.</td>
+     </tr>
+     <tr>
+       <td><b>Album</b></td>
+       <td>The album that the track came from.</td>
+     </tr>
+     <tr>
+       <td><b>Title</b></td>
+       <td>The title of the track.</td>
+     </tr>
+     <tr>
+       <td><b>Length</b></td>
+       <td>The length the track will play for.  For the playing track
+       this will include the amount of time it's been playing so
+       far.</td>
+     </tr>
+   </table>
+
+   <p>You can select tracks in the queue by clicking on them.  You can
+   select multiple tracks by clicking in a second location with SHIFT
+   depressed (to select all tracks between the first and second click)
+   or with CTRL depressed (to add a single track to the selection).</p>
+
+   <p>Having selected tracks you can drag them to a new location in
+   the queue.  Of course, you can't drag the playing track, nor can
+   you drag other tracks before it.</p>
+
+   <p>Right-clicking in the queue will create a pop-up menu:</p>
+
+   <p class=image><img src="queue-menu.png"></p>
+
+   <p><b>Track Properties</b> will create a window with editable
+   properties of each selected track.  <b>Scratch playing track</b>
+   only works if the playing track is the selected track and will stop
+   it playing.  <b>Remove track from queue</b> will remove the
+   selected (non-playing) tracks from the queue.</p>
+
+   <p><b>Adopt track</b> will apply your name to one without an entry
+   in the <b>Who</b> column, i.e. one that was randomly picked by the
+   server.  The reason you might do this is to signal to other users
+   that you did want this track to play.  (For instance, it might be
+   an accepted convention that randomly picked tracks were fair game
+   for removal but that tracks picked by a human should normally be
+   left alone.)</p>
+
+   <h2><a name=recent>3.2 Recently Played Tracks</a></h2>
+
+   <p class=image><img src="recent.png"></p>
+
+   <p>The <b>Recent</b> tab is similar in structure to the queue but
+   it shows tracks that have played recently.  The <b>When</b> column
+   indicates when the track played rather than when it will
+   played.</p>
+
+   <p>Right clicking will create a pop-up menu with similar options to
+   those found in the queue's equivalent menu.  The one additional
+   option is <b>Play track</b>, which allows a recently played track
+   to be added back to the queue.</p>
+
+   <p>The other way of adding tracks from this tab back to the queue
+   is to drag them to the queue tab and then to the desired point into
+   the queue.</p>
+
+   <h2><a name=choose>3.3 Choosing Tracks</a></h2>
+
+   <p>The <b>Choose</b> tab contains all the tracks known to the
+   server, organized into a hierarchical structure based on the
+   underlying file and directory structure.</p>
+
+   <p class=image><img src="choose.png"></p>
+
+   <p>The boxes in the <b>Queued</b> column are ticked if the track is
+   somewhere in the queue.  You can click on an unticked box to add
+   the track to the queue, but clicking an already-ticked one will
+   have no effect.</p>
+
+   <p>Directories can be expanded or collapsed by clicking on the
+   triangular signs left of their names.  Tracks can be selected and
+   deselected by clicking on them.  You can select multiple tracks by
+   clicking in a second location with SHIFT depressed (to select all
+   tracks between the first and second click) or with CTRL depressed
+   (to add a single track to the selection).</p>
+
+   <p>Right clicking will create a pop-up menu with what are hopefuly
+   now familiar options.  <b>Play track</b> will add the selected
+   track(s) to the queue and <b>Track Properties</b> will create a
+   window with editable properties of each selected track.</p>
+
+   <p>Note that when tracks are added to the queue these ways they
+   will be added before any tracks picked at random by the server, so
+   that users don't have to wait for them to play out.</p>
+
+   <p>Selected tracks can also be dragged to the queue, by dragging
+   first to the <b>Queue</b> tab itself and then to the desired
+   location in the queue.</p>
+
+   <p class=image><img src="choose-search.png"></p>
+
+   <p>To do a word search over all tracks, you can just start typing.
+   Your search terms will appear in the input box at the bottom of the
+   window.  Directories containing matching tracks are automatically
+   opened and the matches highlighted in yellow.  You can jump to the
+   previous or next search result with the up and down arrows at the
+   bottom right of the screen (or jump to the next one by pressing
+   CTRL+G).</p>
+
+   <p>If you enter more than one word at a time then only tracks which
+   match <i>both</i> words will be listed.</p>
+
+   <p>You can search for <a href="properties.html#tags">tags</a> as
+   well as words.  For instance to search for the tag
+   &ldquo;happy&rdquo; you would enter <tt>tag:happy</tt> in the
+   search box.</p>
+
+   <p>To clear the search, press the <b>Cancel</b> button.</p>
+
+   <h2><a name=added>3.4 Newly Added Tracks</a></h2>
+
+   <p>The <b>Added</b> tab shows tracks that have been newly detected
+   by the server, in order to allow those tracks to be conveniently
+   played.  In behavior it is the same as the
+   recently <a href="#recent">played tracks list</a>.</p>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
diff --git a/disobedience/manual/track-properties.png b/disobedience/manual/track-properties.png
new file mode 100644 (file)
index 0000000..27093b8
Binary files /dev/null and b/disobedience/manual/track-properties.png differ
diff --git a/disobedience/manual/volume-slider.png b/disobedience/manual/volume-slider.png
new file mode 100644 (file)
index 0000000..c7e25c5
Binary files /dev/null and b/disobedience/manual/volume-slider.png differ
diff --git a/disobedience/manual/window.html b/disobedience/manual/window.html
new file mode 100644 (file)
index 0000000..72fe5f7
--- /dev/null
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+   <title>Disobedience: Window Layout</title>
+   <link rel=stylesheet
+         type="text/css"
+         href="disobedience.css">
+ </head>
+ <body>
+   <h1>2. Window Layout</h1>
+
+   <p>This chapter contains a tour of the main Disobedience
+   window.</p>
+
+   <h2><a name=main>2.1 The Disobedience Window</a></h2>
+   
+   <p>Disobedience should look something like this when you've started
+   it up and logged in:</p>
+
+   <p class=image><img src="queue.png"></p>
+
+   <p>At the top is the menu bar, and immediately below it a toolbar
+   with some buttons and a volume control.  (In some cases the volume
+   control may be absent.)  Below that is a row of
+   tabs: <b>Queue</b>, <b>Recent</b> and so on.  The <b>Queue</b> tab
+   is selected: this displays the currently playing track and the list
+   of tracks that will play in the near future.</p>
+
+   <p>In this example nothing is actually playing.  (Apart from the
+   fact that you wouldn't hear anything) you can tell this because the
+   top row only has a single length indicator (&ldquo;3:04&rdquo;).
+   If it was playing it would show how far through the track it was
+   too.  Secondly, all the tracks were chosen at random.  You can tell
+   this because the <b>Who</b> column is empty.
+
+   <p>In the screenshot below both of these things have changed.
+   Use <tt>rjk</tt> has selected some tracks and the first of them is
+   playing.</p>
+
+   <p class=image><img src="queue2.png"></p>
+
+   <p>The playing track is highlighted with a green backgroud.  It's 4
+   minutes and 10 seconds long and so far, the first 21 seconds of it
+   has played.</p>
+
+   <h2><a name=buttons>2.2 Buttons</a></h2>
+
+   <p>The meaning of the buttons is as follows:</p>
+
+   <table>
+     <tr>
+       <td><img src="button-pause.png"></td>
+       <td>The pause button.  This only effective when a track is
+       playing.  When it is pressed the playing track is paused.</td>
+     </tr>
+
+     <tr>
+       <td><img src="button-scratch.png"></td>
+       <td>The scratch button.  As above this is only effective when
+       a track is playing.  Pressing it will stop that track playing
+       and move onto the next one.</td>
+     </tr>
+
+     <tr>
+        <td><img src="button-random.png"></td>
+
+        <td>The random play button.  Random play, which is enabled
+        when the button is depressed, means that if
+        nothing is playing the server will automatically pick a track
+        at random and play that.  Furthermore it will ensure that the
+        queue always has a minimum number of tracks, so you can see
+        ahead of time what will play next.</td>
+     </tr>
+
+     <tr>
+        <td><img src="button-playing.png"></td>
+        <td>The playing button.  Normally this would always be left
+        enabled, i.e. with the button depressed.
+         If it is disabled then nothing will be
+        played at all, regardless of whether it was randomly chosen
+        or picked by a human.</td>
+     </tr>
+
+     <tr>
+        <td><img src="button-rtp.png"></td>
+        <td>The network play button.  This is only effective if the
+        server is playing over the network (as opposed to using a
+        local sound card).  When network play is enabled,
+        Disobedience runs a client program on your computer to play
+        sound received over the network using your sound card.</td>
+     </tr>
+   </table>
+
+   <p>To the right of the buttons are two sliders:</p>
+
+   <p class=image><img src="volume-slider.png"></p>
+
+   <p>The left hand slider is the volume control.  This ranges from 0
+   (silent) to 10 (loudest).  The right hand slider is the balance
+   control, which ranges form -1.0 (left) to +1.0 (right).</p>
+
+   <h2><a name=menu>2.3 The Menu Bar</a></h2>
+
+   <p class=image><img src="menu-server.png"></p>
+
+   <p>The <b>Server</b> menu is loosely analogous to the <b>File</b>
+   menu in othe applications.  It has no file-related options though,
+   so the name would not be appropriate!  Instead the three options it
+   has are <b>Login</b>, which brings back the login window shown the
+   first time it is run, <b>Manage Users</b> which allows
+   adminstrators to do user management, and <b>Quit</b>.</p>
+
+   <!-- TODO link to user management -->
+
+   <p class=image><img src="menu-edit.png"></p>
+
+   <p>The <b>Edit</b> menu has options to select or deselect all
+   tracks.  You can also do this with CTRL-A and CTRL-SHIFT-A, and of
+   course you can select tracks in the queue (or other tabs) with the
+   mouse.</p>
+
+   <p>The <b>Track Properties</b> option will create a window with
+   editable <a href="properties.html">properties</a> of each selected
+   track and the <b>Edit Playlists</b> option will create a window
+   allowing editing of <a href="playlists.html">playlists</a>.</p>
+
+   <p class=image><img src="menu-control.png"></p>
+
+   <p>The <b>Control</b> menu options are mostly equivalent to the
+   buttons described above.  The exceptions is <b>Activate
+   Playlist</b> which allows you to play
+   a <A href="playlists.html">playlist</a>, and <b>Compact Mode</b>
+   which switches Disobedience's window to a smaller format.</p>
+
+   <p class=image><img src="menu-help.png"></p>
+
+   <p>The <b>Help</b> menu has an option to bring up the Disobedience
+   manual and an <b>About</b> option which will display a bit of
+   version information for the server and for Disobedience (which
+   might not be the same).</p>
+
+   <hr>
+
+   <a href="index.html">Back to contents</a>
+
+ </body>
+</html>
index 02431393b5343ff469acafc9cb4dcd3cccc01a29..bae0f00d7f0471c018efd2816afbef91af59f79b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 #include "disobedience.h"
 
+static void toggled_minimode(GtkCheckMenuItem *item, gpointer userdata);
+
 static GtkWidget *selectall_widget;
 static GtkWidget *selectnone_widget;
 static GtkWidget *properties_widget;
-#if PLAYLISTS
-GtkWidget *playlists_widget;
+GtkWidget *menu_playlists_widget;
 GtkWidget *playlists_menu;
-GtkWidget *editplaylists_widget;
-#endif
+GtkWidget *menu_editplaylists_widget;
+static GtkWidget *menu_minimode_widget;
 
 /** @brief Main menu widgets */
 GtkItemFactory *mainmenufactory;
 
+/** @brief Set for full mode, clear for mini mode */
+int full_mode = 1;
+
 static void about_popup_got_version(void *v,
                                     const char *err,
                                     const char *value);
@@ -136,7 +140,7 @@ static void manual_popup(gpointer attribute((unused)) callback_data,
                        GtkWidget attribute((unused)) *menu_item) {
   D(("manual_popup"));
 
-  popup_help();
+  popup_help(NULL);
 }
 
 /** @brief Called when version arrives, displays about... popup */
@@ -276,15 +280,15 @@ GtkWidget *menubar(GtkWidget *w) {
     },
     {
       (char *)"/Edit/Select all tracks", /* path */
-      0,                                /* accelerator */
+      (char *)"<CTRL>A",                /* accelerator */
       menu_tab_action,                  /* callback */
       offsetof(struct tabtype, selectall_activate), /* callback_action */
-      0,                                /* item_type */
-      0                                 /* extra_data */
+      (char *)"<StockItem>",           /* item_type */
+      GTK_STOCK_SELECT_ALL,            /* extra_data */
     },
     {
       (char *)"/Edit/Deselect all tracks", /* path */
-      0,                                /* accelerator */
+      (char *)"<CTRL><SHIFT>A",         /* accelerator */
       menu_tab_action,                  /* callback */
       offsetof(struct tabtype, selectnone_activate), /* callback_action */
       0,                                /* item_type */
@@ -295,19 +299,17 @@ GtkWidget *menubar(GtkWidget *w) {
       0,                                /* accelerator */
       menu_tab_action,                  /* callback */
       offsetof(struct tabtype, properties_activate), /* callback_action */
-      0,                                /* item_type */
-      0                                 /* extra_data */
+      (char *)"<StockItem>",            /* item_type */
+      GTK_STOCK_PROPERTIES,             /* extra_data */
     },
-#if PLAYLISTS
     {
       (char *)"/Edit/Edit playlists",   /* path */
       0,                                /* accelerator */
-      edit_playlists,                   /* callback */
+      playlist_window_create,           /* callback */
       0,                                /* callback_action */
       0,                                /* item_type */
       0                                 /* extra_data */
     },
-#endif
     
     
     {
@@ -323,8 +325,8 @@ GtkWidget *menubar(GtkWidget *w) {
       (char *)"<CTRL>S",                /* accelerator */
       0,                                /* callback */
       0,                                /* callback_action */
-      0,                                /* item_type */
-      0                                 /* extra_data */
+      (char *)"<StockItem>",            /* item_type */
+      GTK_STOCK_STOP,                   /* extra_data */
     },
     {
       (char *)"/Control/Playing",       /* path */
@@ -350,7 +352,14 @@ GtkWidget *menubar(GtkWidget *w) {
       (char *)"<CheckItem>",            /* item_type */
       0                                 /* extra_data */
     },
-#if PLAYLISTS
+    {
+      (char *)"/Control/Compact mode",  /* path */
+      (char *)"<CTRL>M",                /* accelerator */
+      0,                                /* callback */
+      0,                                /* callback_action */
+      (char *)"<CheckItem>",            /* item_type */
+      0                                 /* extra_data */
+    },
     {
       (char *)"/Control/Activate playlist", /* path */
       0,                                /* accelerator */
@@ -359,8 +368,7 @@ GtkWidget *menubar(GtkWidget *w) {
       (char *)"<Branch>",               /* item_type */
       0                                 /* extra_data */
     },
-#endif
-    
+
     {
       (char *)"/Help",                  /* path */
       0,                                /* accelerator */
@@ -370,12 +378,12 @@ GtkWidget *menubar(GtkWidget *w) {
       0                                 /* extra_data */
     },
     {
-      (char *)"/Help/Manual page",      /* path */
+      (char *)"/Help/Manual",           /* path */
       0,                                /* accelerator */
       manual_popup,                     /* callback */
       0,                                /* callback_action */
-      0,                                /* item_type */
-      0                                 /* extra_data */
+      (char *)"<StockItem>",            /* item_type */
+      GTK_STOCK_HELP,                   /* extra_data */
     },
     {
       (char *)"/Help/About DisOrder",   /* path */
@@ -404,22 +412,20 @@ GtkWidget *menubar(GtkWidget *w) {
                                                 "<GdisorderMain>/Edit/Deselect all tracks");
   properties_widget = gtk_item_factory_get_widget(mainmenufactory,
                                                  "<GdisorderMain>/Edit/Track properties");
-#if PLAYLISTS
-  playlists_widget = gtk_item_factory_get_item(mainmenufactory,
+  menu_playlists_widget = gtk_item_factory_get_item(mainmenufactory,
                                                "<GdisorderMain>/Control/Activate playlist");
   playlists_menu = gtk_item_factory_get_widget(mainmenufactory,
                                                "<GdisorderMain>/Control/Activate playlist");
-  editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
+  menu_editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
                                                      "<GdisorderMain>/Edit/Edit playlists");
-#endif
+  menu_minimode_widget = gtk_item_factory_get_widget(mainmenufactory,
+                                                     "<GdisorderMain>/Control/Compact mode");
   assert(selectall_widget != 0);
   assert(selectnone_widget != 0);
   assert(properties_widget != 0);
-#if PLAYLISTS
-  assert(playlists_widget != 0);
+  assert(menu_playlists_widget != 0);
   assert(playlists_menu != 0);
-  assert(editplaylists_widget != 0);
-#endif
+  assert(menu_editplaylists_widget != 0);
 
   GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory,
                                                        "<GdisorderMain>/Edit");
@@ -430,9 +436,21 @@ GtkWidget *menubar(GtkWidget *w) {
   m = gtk_item_factory_get_widget(mainmenufactory,
                                   "<GdisorderMain>");
   set_tool_colors(m);
+  if(menu_minimode_widget)
+    g_signal_connect(G_OBJECT(menu_minimode_widget), "toggled",
+                     G_CALLBACK(toggled_minimode), NULL);
   return m;
 }
 
+static void toggled_minimode(GtkCheckMenuItem  *item,
+                             gpointer attribute((unused)) userdata) {
+  int new_full_mode = !gtk_check_menu_item_get_active(item);
+  if(full_mode != new_full_mode) {
+    full_mode = new_full_mode;
+    event_raise("mini-mode-changed", NULL);
+  }
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 96e175030c1d5ebca495615c9e80f4e1b1035e41..a1fdf4d5f7b72d9ec2bdb96097d366f62503f788 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2008, 2010 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -27,7 +27,7 @@ struct image {
   const guint8 *data;
 };
 
-#include "images.h"
+#include "../images/images.h"
 
 /* Miscellaneous GTK+ stuff ------------------------------------------------ */
 
@@ -187,7 +187,14 @@ GtkWidget *create_buttons_box(struct button *buttons,
     gtk_widget_set_style(buttons[n].widget, tool_style);
     g_signal_connect(G_OBJECT(buttons[n].widget), "clicked",
                      G_CALLBACK(buttons[n].clicked), 0);
-    gtk_box_pack_start(GTK_BOX(box), buttons[n].widget, FALSE, FALSE, 1);
+    void (*pack)(GtkBox *box,
+                 GtkWidget *child,
+                 gboolean expand,
+                 gboolean fill,
+                 guint padding);
+    if(!(pack = buttons[n].pack))
+      pack = gtk_box_pack_start;
+    pack(GTK_BOX(box), buttons[n].widget, FALSE, FALSE, 1);
     gtk_widget_set_tooltip_text(buttons[n].widget, buttons[n].tip);
   }
   return box;
index 8b28c94e53784f1c58a33869e2fe30bb07b53cd2..f4a5a768e1ada942985cafba7ae35d2ecad8dad8 100644 (file)
@@ -92,8 +92,9 @@ static gboolean multidrag_button_press_event(GtkWidget *w,
   /* We are only interested in left-button behavior */
   if(event->button != 1)
     return FALSE;
-  /* We are only interested in unmodified clicks (not SHIFT etc) */
-  if(event->state & GDK_MODIFIER_MASK)
+  /* We are only uninterested in clicks without CTRL or SHIFT.  GTK ignores the
+   * other possible modifiers, so we do too. */
+  if(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK))
     return FALSE;
   /* We are only interested if a well-defined path is clicked */
   GtkTreePath *path = NULL;
index cd8979d19cf30638c40a4342e87617012c1fb5cf..c95db438433edb744c04448e2310b5d0941a8e54 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2008 Richard Kettlewell
+ * Copyright (C) 2008, 2009 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * USA
  */
 /** @file disobedience/playlists.c
- * @brief Playlist for Disobedience
+ * @brief Playlist support for Disobedience
  *
  * The playlists management window contains:
- * - a list of all playlists
+ * - the playlist picker (a list of all playlists) TODO should be a tree!
  * - an add button
  * - a delete button
- * - a drag+drop capable view of the playlist
- * - a close button
+ * - the playlist editor (a d+d-capable view of the currently picked playlist)
+ * - a close button   TODO
+ *
+ * This file also maintains the playlist menu, allowing playlists to be
+ * activated from the main window's menu.
+ *
+ * Internally we maintain the playlist list, which is just the current list of
+ * playlists.  Changes to this are reflected in the playlist menu and the
+ * playlist picker.
+ *
  */
 #include "disobedience.h"
+#include "queue-generic.h"
+#include "popup.h"
+#include "validity.h"
 
-#if PLAYLISTS
-
-static void playlists_updated(void *v,
-                              const char *err,
-                              int nvec, char **vec);
+static void playlist_list_received_playlists(void *v,
+                                             const char *err,
+                                             int nvec, char **vec);
+static void playlist_editor_fill(const char *event,
+                                 void *eventdata,
+                                 void *callbackdata);
+static int playlist_playall_sensitive(void *extra);
+static void playlist_playall_activate(GtkMenuItem *menuitem,
+                                      gpointer user_data);
+static int playlist_remove_sensitive(void *extra) ;
+static void playlist_remove_activate(GtkMenuItem *menuitem,
+                                     gpointer user_data);
+static void playlist_new_locked(void *v, const char *err);
+static void playlist_new_retrieved(void *v, const char *err,
+                                   int nvec,
+                                   char **vec);
+static void playlist_new_created(void *v, const char *err);
+static void playlist_new_unlocked(void *v, const char *err);
+static void playlist_new_entry_edited(GtkEditable *editable,
+                                      gpointer user_data);
+static void playlist_new_button_toggled(GtkToggleButton *tb,
+                                        gpointer userdata);
+static void playlist_new_changed(const char *event,
+                                 void *eventdata,
+                                 void *callbackdata);
+static const char *playlist_new_valid(void);
+static void playlist_new_details(char **namep,
+                                 char **fullnamep,
+                                 gboolean *sharedp,
+                                 gboolean *publicp,
+                                 gboolean *privatep);
+static void playlist_new_ok(GtkButton *button,
+                            gpointer userdata);
+static void playlist_new_cancel(GtkButton *button,
+                                gpointer userdata);
+static void playlists_editor_received_tracks(void *v,
+                                             const char *err,
+                                             int nvec, char **vec);
+static void playlist_window_destroyed(GtkWidget *widget,
+                                      GtkWidget **widget_pointer);
+static gboolean playlist_window_keypress(GtkWidget *widget,
+                                         GdkEventKey *event,
+                                         gpointer user_data);
+static int playlistcmp(const void *ap, const void *bp);
+static void playlist_modify_locked(void *v, const char *err);
+void playlist_modify_retrieved(void *v, const char *err,
+                               int nvec,
+                               char **vec);
+static void playlist_modify_updated(void *v, const char *err);
+static void playlist_modify_unlocked(void *v, const char *err);
+static void playlist_drop(struct queuelike *ql,
+                          int ntracks,
+                          char **tracks, char **ids,
+                          struct queue_entry *after_me);
+struct playlist_modify_data;
+static void playlist_drop_modify(struct playlist_modify_data *mod,
+                                 int nvec, char **vec);
+static void playlist_remove_modify(struct playlist_modify_data *mod,
+                                 int nvec, char **vec);
+static gboolean playlist_new_keypress(GtkWidget *widget,
+                                      GdkEventKey *event,
+                                      gpointer user_data);
+static gboolean playlist_picker_keypress(GtkWidget *widget,
+                                         GdkEventKey *event,
+                                         gpointer user_data);
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+                                           gpointer userdata);
+static void playlist_editor_set_buttons(const char *event,
+                                        void *eventdata,
+                                        void *callbackdata);
+static void playlist_editor_got_share(void *v,
+                                      const char *err,
+                                      const char *value);
+static void playlist_editor_share_set(void *v, const char *err);
+static void playlist_picker_update_section(const char *title, const char *key,
+                                           int start, int end);
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+                                     const char *title, const char *key,
+                                     GtkTreeIter iter[1],
+                                     gboolean create);
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+                                            char **exists,
+                                            int nexists);
+static gboolean playlist_picker_button(GtkWidget *widget,
+                                       GdkEventButton *event,
+                                       gpointer user_data);
+static gboolean playlist_editor_keypress(GtkWidget *widget,
+                                         GdkEventKey *event,
+                                         gpointer user_data);
+static void playlist_editor_ok(GtkButton *button, gpointer userdata);
+static void playlist_editor_help(GtkButton *button, gpointer userdata);
 
 /** @brief Playlist editing window */
-static GtkWidget *playlists_window;
+static GtkWidget *playlist_window;
 
-/** @brief Tree model for list of playlists */
-static GtkListStore *playlists_list;
+/** @brief Columns for the playlist editor */
+static const struct queue_column playlist_columns[] = {
+  { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
+  { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
+};
 
-/** @brief Selection for list of playlists */
-static GtkTreeSelection *playlists_selection;
+/** @brief Pop-up menu for playlist editor
+ *
+ * Status:
+ * - track properties works but, bizarrely, raises the main window
+ * - play track works
+ * - play playlist works
+ * - select/deselect all work
+ */
+static struct menuitem playlist_menuitems[] = {
+  { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+  { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+  { "Play playlist", NULL, playlist_playall_activate, playlist_playall_sensitive, 0, 0 },
+  { "Remove track from playlist", GTK_STOCK_DELETE, playlist_remove_activate, playlist_remove_sensitive, 0, 0 },
+  { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+};
 
-/** @brief Currently selected playlist */
-static const char *playlists_selected;
+static const GtkTargetEntry playlist_targets[] = {
+  {
+    PLAYLIST_TRACKS,                    /* drag type */
+    GTK_TARGET_SAME_WIDGET,             /* rearrangement within a widget */
+    PLAYLIST_TRACKS_ID                  /* ID value */
+  },
+  {
+    PLAYABLE_TRACKS,                             /* drag type */
+    GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
+    PLAYABLE_TRACKS_ID,                          /* ID value */
+  },
+  {
+    .target = NULL
+  }
+};
 
-/** @brief Delete button */
-static GtkWidget *playlists_delete_button;
+/** @brief Queuelike for editing a playlist */
+static struct queuelike ql_playlist = {
+  .name = "playlist",
+  .columns = playlist_columns,
+  .ncolumns = sizeof playlist_columns / sizeof *playlist_columns,
+  .menuitems = playlist_menuitems,
+  .nmenuitems = sizeof playlist_menuitems / sizeof *playlist_menuitems,
+  .drop = playlist_drop,
+  .drag_source_targets = playlist_targets,
+  .drag_source_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+  .drag_dest_targets = playlist_targets,
+  .drag_dest_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+};
+
+/* Maintaining the list of playlists ---------------------------------------- */
 
 /** @brief Current list of playlists or NULL */
 char **playlists;
@@ -56,11 +197,31 @@ char **playlists;
 /** @brief Count of playlists */
 int nplaylists;
 
-/** @brief Schedule an update to the list of playlists */
-static void playlists_update(const char attribute((unused)) *event,
-                             void attribute((unused)) *eventdata,
-                             void attribute((unused)) *callbackdata) {
-  disorder_eclient_playlists(client, playlists_updated, 0);
+/** @brief Schedule an update to the list of playlists
+ *
+ * Called periodically and when a playlist is created or deleted.
+ */
+static void playlist_list_update(const char attribute((unused)) *event,
+                                 void attribute((unused)) *eventdata,
+                                 void attribute((unused)) *callbackdata) {
+  disorder_eclient_playlists(client, playlist_list_received_playlists, 0);
+}
+
+/** @brief Called with a new list of playlists */
+static void playlist_list_received_playlists(void attribute((unused)) *v,
+                                             const char *err,
+                                             int nvec, char **vec) {
+  if(err) {
+    playlists = 0;
+    nplaylists = -1;
+    /* Probably means server does not support playlists */
+  } else {
+    playlists = vec;
+    nplaylists = nvec;
+    qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
+  }
+  /* Tell our consumers */
+  event_raise("playlists-updated", 0);
 }
 
 /** @brief qsort() callback for playlist name comparison */
@@ -89,244 +250,1380 @@ static int playlistcmp(const void *ap, const void *bp) {
   return strcmp(a, b);
 }
 
-/** @brief Called with a new list of playlists */
-static void playlists_updated(void attribute((unused)) *v,
-                              const char *err,
-                              int nvec, char **vec) {
+/* Playlists menu ----------------------------------------------------------- */
+
+static void playlist_menu_playing(void attribute((unused)) *v,
+                                  const char *err) {
+  if(err)
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+
+/** @brief Play received playlist contents
+ *
+ * Passed as a completion callback by menu_activate_playlist().
+ */
+static void playlist_menu_received_content(void attribute((unused)) *v,
+                                           const char *err,
+                                           int nvec, char **vec) {
   if(err) {
-    playlists = 0;
-    nplaylists = -1;
-    /* Probably means server does not support playlists */
-  } else {
-    playlists = vec;
-    nplaylists = nvec;
-    qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    return;
   }
-  /* Tell our consumers */
-  event_raise("playlists-updated", 0);
+  for(int n = 0; n < nvec; ++n)
+    disorder_eclient_play(client, vec[n], playlist_menu_playing, NULL);
 }
 
-/** @brief Called to activate a playlist */
-static void menu_activate_playlist(GtkMenuItem *menuitem,
+/** @brief Called to activate a playlist
+ *
+ * Called when the menu item for a playlist is clicked.
+ */
+static void playlist_menu_activate(GtkMenuItem *menuitem,
                                    gpointer attribute((unused)) user_data) {
   GtkLabel *label = GTK_LABEL(GTK_BIN(menuitem)->child);
   const char *playlist = gtk_label_get_text(label);
 
-  fprintf(stderr, "activate playlist %s\n", playlist); /* TODO */
+  disorder_eclient_playlist_get(client, playlist_menu_received_content,
+                                playlist, NULL);
 }
 
-/** @brief Called when the playlists change */
-static void menu_playlists_changed(const char attribute((unused)) *event,
-                                   void attribute((unused)) *eventdata,
-                                   void attribute((unused)) *callbackdata) {
+/** @brief Called when the playlists change
+ *
+ * Naively refills the menu.  The results might be unsettling if the menu is
+ * currently open, but this is hopefuly fairly rare.
+ */
+static void playlist_menu_changed(const char attribute((unused)) *event,
+                                  void attribute((unused)) *eventdata,
+                                  void attribute((unused)) *callbackdata) {
   if(!playlists_menu)
     return;                             /* OMG too soon */
   GtkMenuShell *menu = GTK_MENU_SHELL(playlists_menu);
-  /* TODO: we could be more sophisticated and only insert/remove widgets as
-   * needed.  For now that's too much effort. */
   while(menu->children)
     gtk_container_remove(GTK_CONTAINER(menu), GTK_WIDGET(menu->children->data));
   /* NB nplaylists can be -1 as well as 0 */
   for(int n = 0; n < nplaylists; ++n) {
     GtkWidget *w = gtk_menu_item_new_with_label(playlists[n]);
-    g_signal_connect(w, "activate", G_CALLBACK(menu_activate_playlist), 0);
+    g_signal_connect(w, "activate", G_CALLBACK(playlist_menu_activate), 0);
     gtk_widget_show(w);
     gtk_menu_shell_append(menu, w);
   }
-  gtk_widget_set_sensitive(playlists_widget,
+  gtk_widget_set_sensitive(menu_playlists_widget,
                            nplaylists > 0);
-  gtk_widget_set_sensitive(editplaylists_widget,
+  gtk_widget_set_sensitive(menu_editplaylists_widget,
                            nplaylists >= 0);
 }
 
-/** @brief (Re-)populate the playlist tree model */
-static void playlists_fill(void) {
-  GtkTreeIter iter[1];
+/* Popup to create a new playlist ------------------------------------------- */
+
+/** @brief New-playlist popup */
+static GtkWidget *playlist_new_window;
+
+/** @brief Text entry in new-playlist popup */
+static GtkWidget *playlist_new_entry;
+
+/** @brief Label for displaying feedback on what's wrong */
+static GtkWidget *playlist_new_info;
+
+/** @brief "Shared" radio button */
+static GtkWidget *playlist_new_shared;
+
+/** @brief "Public" radio button */
+static GtkWidget *playlist_new_public;
+
+/** @brief "Private" radio button */
+static GtkWidget *playlist_new_private;
+
+/** @brief Buttons for new-playlist popup */
+static struct button playlist_new_buttons[] = {
+  {
+    .stock = GTK_STOCK_OK,
+    .clicked = playlist_new_ok,
+    .tip = "Create new playlist"
+  },
+  {
+    .stock = GTK_STOCK_CANCEL,
+    .clicked = playlist_new_cancel,
+    .tip = "Do not create new playlist"
+  }
+};
+#define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons)
+
+/** @brief Pop up a new window to enter the playlist name and details */
+static void playlist_new_playlist(void) {
+  assert(playlist_new_window == NULL);
+  playlist_new_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  g_signal_connect(playlist_new_window, "destroy",
+                  G_CALLBACK(gtk_widget_destroyed), &playlist_new_window);
+  gtk_window_set_title(GTK_WINDOW(playlist_new_window), "Create new playlist");
+  /* Window will be modal, suppressing access to other windows */
+  gtk_window_set_modal(GTK_WINDOW(playlist_new_window), TRUE);
+  gtk_window_set_transient_for(GTK_WINDOW(playlist_new_window),
+                               GTK_WINDOW(playlist_window));
+
+  /* Window contents will use a table (grid) layout */
+  GtkWidget *table = gtk_table_new(3, 3, FALSE/*!homogeneous*/);
+
+  /* First row: playlist name */
+  gtk_table_attach_defaults(GTK_TABLE(table),
+                            gtk_label_new("Playlist name"),
+                            0, 1, 0, 1);
+  playlist_new_entry = gtk_entry_new();
+  g_signal_connect(playlist_new_entry, "changed",
+                   G_CALLBACK(playlist_new_entry_edited), NULL);
+  gtk_table_attach_defaults(GTK_TABLE(table),
+                            playlist_new_entry,
+                            1, 3, 0, 1);
+
+  /* Second row: radio buttons to choose type */
+  playlist_new_shared = gtk_radio_button_new_with_label(NULL, "shared");
+  playlist_new_public
+    = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
+                                                  "public");
+  playlist_new_private
+    = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
+                                                  "private");
+  g_signal_connect(playlist_new_shared, "toggled",
+                   G_CALLBACK(playlist_new_button_toggled), NULL);
+  g_signal_connect(playlist_new_public, "toggled",
+                   G_CALLBACK(playlist_new_button_toggled), NULL);
+  g_signal_connect(playlist_new_private, "toggled",
+                   G_CALLBACK(playlist_new_button_toggled), NULL);
+  gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_shared, 0, 1, 1, 2);
+  gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_public, 1, 2, 1, 2);
+  gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_private, 2, 3, 1, 2);
+
+  /* Third row: info bar saying why not */
+  playlist_new_info = gtk_label_new("");
+  gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_info,
+                            0, 3, 2, 3);
+
+  /* Fourth row: ok/cancel buttons */
+  GtkWidget *hbox = create_buttons_box(playlist_new_buttons,
+                                       NPLAYLIST_NEW_BUTTONS,
+                                       gtk_hbox_new(FALSE, 0));
+  gtk_table_attach_defaults(GTK_TABLE(table), hbox, 0, 3, 3, 4);
+
+  gtk_container_add(GTK_CONTAINER(playlist_new_window),
+                    frame_widget(table, NULL));
+
+  /* Set initial state of OK button */
+  playlist_new_changed(0,0,0);
+
+  g_signal_connect(playlist_new_window, "key-press-event",
+                   G_CALLBACK(playlist_new_keypress), 0);
+  
+  /* Display the window */
+  gtk_widget_show_all(playlist_new_window);
+}
 
-  if(!playlists_list)
-    playlists_list = gtk_list_store_new(1, G_TYPE_STRING);
-  gtk_list_store_clear(playlists_list);
+/** @brief Keypress handler */
+static gboolean playlist_new_keypress(GtkWidget attribute((unused)) *widget,
+                                      GdkEventKey *event,
+                                      gpointer attribute((unused)) user_data) {
+  if(event->state)
+    return FALSE;
+  switch(event->keyval) {
+  case GDK_Return:
+    playlist_new_ok(NULL, NULL);
+    return TRUE;
+  case GDK_Escape:
+    gtk_widget_destroy(playlist_new_window);
+    return TRUE;
+  default:
+    return FALSE;
+  }
+}
+
+/** @brief Called when 'ok' is clicked in new-playlist popup */
+static void playlist_new_ok(GtkButton attribute((unused)) *button,
+                            gpointer attribute((unused)) userdata) {
+  if(playlist_new_valid())
+    return;
+  gboolean shared, public, private;
+  char *name, *fullname;
+  playlist_new_details(&name, &fullname, &shared, &public, &private);
+
+  /* We need to:
+   * - lock the playlist
+   * - check it doesn't exist
+   * - set sharing (which will create it empty
+   * - unlock it
+   *
+   * TODO we should freeze the window while this is going on to stop a second
+   * click.
+   */
+  disorder_eclient_playlist_lock(client, playlist_new_locked, fullname,
+                                 fullname);
+}
+
+/** @brief Called when the proposed new playlist has been locked */
+static void playlist_new_locked(void *v, const char *err) {
+  char *fullname = v;
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    return;
+  }
+  disorder_eclient_playlist_get(client, playlist_new_retrieved,
+                                fullname, fullname);
+}
+
+/** @brief Called when the proposed new playlist's contents have been retrieved
+ *
+ * ...or rather, normally, when it's been reported that it does not exist.
+ */
+static void playlist_new_retrieved(void *v, const char *err,
+                                   int nvec,
+                                   char attribute((unused)) **vec) {
+  char *fullname = v;
+  if(!err && nvec != -1)
+    /* A rare case but not in principle impossible */
+    err = "A playlist with that name already exists.";
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname);
+    return;
+  }
+  gboolean shared, public, private;
+  playlist_new_details(0, 0, &shared, &public, &private);
+  disorder_eclient_playlist_set_share(client, playlist_new_created, fullname,
+                                      public ? "public"
+                                      : private ? "private"
+                                      : "shared",
+                                      fullname);
+}
+
+/** @brief Called when the new playlist has been created */
+static void playlist_new_created(void attribute((unused)) *v, const char *err) {
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    return;
+  }
+  disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL);
+  // TODO arrange for the new playlist to be selected
+}
+
+/** @brief Called when the newly created playlist has unlocked */
+static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) {
+  if(err)
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+  /* Pop down the creation window */
+  gtk_widget_destroy(playlist_new_window);
+}
+
+/** @brief Called when 'cancel' is clicked in new-playlist popup */
+static void playlist_new_cancel(GtkButton attribute((unused)) *button,
+                                gpointer attribute((unused)) userdata) {
+  gtk_widget_destroy(playlist_new_window);
+}
+
+/** @brief Called when some radio button in the new-playlist popup changes */
+static void playlist_new_button_toggled(GtkToggleButton attribute((unused)) *tb,
+                                        gpointer attribute((unused)) userdata) {
+  playlist_new_changed(0,0,0);
+}
+
+/** @brief Called when the text entry field in the new-playlist popup changes */
+static void playlist_new_entry_edited(GtkEditable attribute((unused)) *editable,
+                                      gpointer attribute((unused)) user_data) {
+  playlist_new_changed(0,0,0);
+}
+
+/** @brief Called to update new playlist window state
+ *
+ * This is called whenever one the text entry or radio buttons changed, and
+ * also when the set of known playlists changes.  It determines whether the new
+ * playlist would be creatable and sets the sensitivity of the OK button
+ * and info display accordingly.
+ */
+static void playlist_new_changed(const char attribute((unused)) *event,
+                                 void attribute((unused)) *eventdata,
+                                 void attribute((unused)) *callbackdata) {
+  if(!playlist_new_window)
+    return;
+  const char *reason = playlist_new_valid();
+  gtk_widget_set_sensitive(playlist_new_buttons[0].widget,
+                           !reason);
+  gtk_label_set_text(GTK_LABEL(playlist_new_info), reason);
+}
+
+/** @brief Test whether the new-playlist window settings are valid
+ * @return NULL on success or an error string if not
+ */
+static const char *playlist_new_valid(void) {
+  gboolean shared, public, private;
+  char *name, *fullname;
+  playlist_new_details(&name, &fullname, &shared, &public, &private);
+  if(!(shared || public || private))
+    return "No type set.";
+  if(!*name)
+    return "";
+  /* See if the result is valid */
+  if(!valid_username(name)
+     || playlist_parse_name(fullname, NULL, NULL))
+    return "Not a valid playlist name.";
+  /* See if the result clashes with an existing name.  This is not a perfect
+   * check, the playlist might be created after this point but before we get a
+   * chance to disable the "OK" button.  However when we try to create the
+   * playlist we will first try to retrieve it, with a lock held, so we
+   * shouldn't end up overwriting anything. */
   for(int n = 0; n < nplaylists; ++n)
-    gtk_list_store_insert_with_values(playlists_list, iter, n/*position*/,
-                                      0, playlists[n],        /* column 0 */
-                                      -1);                    /* no more cols */
-  // TODO reselect whatever was formerly selected if possible, if not then
-  // zap the contents view
+    if(!strcmp(playlists[n], fullname)) {
+      if(shared)
+        return "A shared playlist with that name already exists.";
+      else
+        return "You already have a playlist with that name.";
+    }
+  /* As far as we can tell creation would work */
+  return NULL;
+}
+
+/** @brief Get entered new-playlist details
+ * @param namep Where to store entered name (or NULL)
+ * @param fullnamep Where to store computed full name (or NULL)
+ * @param sharep Where to store 'shared' flag (or NULL)
+ * @param publicp Where to store 'public' flag (or NULL)
+ * @param privatep Where to store 'private' flag (or NULL)
+ */
+static void playlist_new_details(char **namep,
+                                 char **fullnamep,
+                                 gboolean *sharedp,
+                                 gboolean *publicp,
+                                 gboolean *privatep) {
+  gboolean shared, public, private;
+  g_object_get(playlist_new_shared, "active", &shared, (char *)NULL);
+  g_object_get(playlist_new_public, "active", &public, (char *)NULL);
+  g_object_get(playlist_new_private, "active", &private, (char *)NULL);
+  char *gname = gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry),
+                                       0, -1); /* name owned by calle */
+  char *name = xstrdup(gname);
+  g_free(gname);
+  if(sharedp) *sharedp = shared;
+  if(publicp) *publicp = public;
+  if(privatep) *privatep = private;
+  if(namep) *namep = name;
+  if(fullnamep) {
+    if(shared) *fullnamep = name;
+    else byte_xasprintf(fullnamep, "%s.%s", config->username, name);
+  }
+}
+
+/* Playlist picker ---------------------------------------------------------- */
+
+/** @brief Delete button */
+static GtkWidget *playlist_picker_delete_button;
+
+/** @brief Tree model for list of playlists
+ *
+ * This has two columns:
+ * - column 0 will be the display name
+ * - column 1 will be the sort key/playlist name (and will not be displayed)
+ */
+static GtkTreeStore *playlist_picker_list;
+
+/** @brief Selection for list of playlists */
+static GtkTreeSelection *playlist_picker_selection;
+
+/** @brief Currently selected playlist */
+static const char *playlist_picker_selected;
+
+/** @brief (Re-)populate the playlist picker tree model */
+static void playlist_picker_fill(const char attribute((unused)) *event,
+                                 void attribute((unused)) *eventdata,
+                                 void attribute((unused)) *callbackdata) {
+  if(!playlist_window)
+    return;
+  if(!playlist_picker_list)
+    playlist_picker_list = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+  /* We will accumulate a list of all the sections that exist */
+  char **sections = xcalloc(nplaylists, sizeof (char *));
+  int nsections = 0;
+  /* Make sure shared playlists are there */
+  int start = 0, end;
+  for(end = start; end < nplaylists && !strchr(playlists[end], '.'); ++end)
+    ;
+  if(start != end) {
+    playlist_picker_update_section("Shared playlists", "",
+                                   start, end);
+    sections[nsections++] = (char *)"";
+  }
+  /* Make sure owned playlists are there */
+  while((start = end) < nplaylists) {
+    const int nl = strchr(playlists[start], '.') - playlists[start];
+    char *name = xstrndup(playlists[start], nl);
+    for(end = start;
+        end < nplaylists
+          && playlists[end][nl] == '.'
+          && !strncmp(playlists[start], playlists[end], nl);
+        ++end)
+      ;
+    playlist_picker_update_section(name, name, start, end);
+    sections[nsections++] = name;
+  }
+  /* Delete obsolete sections */
+  playlist_picker_delete_obsolete(NULL, sections, nsections);
+}
+
+/** @brief Update a section in the picker tree model
+ * @param section Section name
+ * @param start First entry in @ref playlists
+ * @param end Past last entry in @ref playlists
+ */
+static void playlist_picker_update_section(const char *title, const char *key,
+                                           int start, int end) {
+  /* Find the section, creating it if necessary */
+  GtkTreeIter section_iter[1];
+  playlist_picker_find(NULL, title, key, section_iter, TRUE);
+  /* Add missing rows */
+  for(int n = start; n < end; ++n) {
+    GtkTreeIter child[1];
+    char *name;
+    if((name = strchr(playlists[n], '.')))
+      ++name;
+    else
+      name = playlists[n];
+    playlist_picker_find(section_iter,
+                         name, playlists[n],
+                         child,
+                         TRUE);
+  }
+  /* Delete anything that shouldn't exist. */
+  playlist_picker_delete_obsolete(section_iter, playlists + start, end - start);
+}
+
+/** @brief Find and maybe create a row in the picker tree model
+ * @param parent Parent iterator (or NULL for top level)
+ * @param title Display name of section
+ * @param key Key to search for
+ * @param iter Iterator to point at key
+ * @param create If TRUE, key will be created if it doesn't exist
+ * @param compare Row comparison function
+ * @return TRUE if key exists else FALSE
+ *
+ * If the @p key exists then @p iter will point to it and TRUE will be
+ * returned.
+ *
+ * If the @p key does not exist and @p create is TRUE then it will be created.
+ * @p iter wil point to it and TRUE will be returned.
+ *
+ * If the @p key does not exist and @p create is FALSE then FALSE will be
+ * returned.
+ */
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+                                     const char *title,
+                                     const char *key,
+                                     GtkTreeIter iter[1],
+                                     gboolean create) {
+  gchar *candidate;
+  GtkTreeIter next[1];
+  gboolean it;
+  int row = 0;
+
+  it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+                                    next,
+                                    parent);
+  while(it) {
+    /* Find the value at row 'next' */
+    gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+                       next,
+                       1, &candidate,
+                       -1);
+    /* See how it compares with @p key */
+    int c = strcmp(key, candidate);
+    g_free(candidate);
+    if(!c) {
+      *iter = *next;
+      return TRUE;                      /* we found our key */
+    }
+    if(c < 0) {
+      /* @p key belongs before row 'next' */
+      if(create) {
+        gtk_tree_store_insert_with_values(playlist_picker_list,
+                                          iter,
+                                          parent,
+                                          row,     /* insert here */
+                                          0, title, 1, key, -1);
+        return TRUE;
+      } else
+        return FALSE;
+      ++row;
+    }
+    it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list), next);
+  }
+  /* We have reached the end and not found a row that should be later than @p
+   * key. */
+  if(create) {
+    gtk_tree_store_insert_with_values(playlist_picker_list,
+                                      iter,
+                                      parent,
+                                      INT_MAX, /* insert at end */
+                                      0, title, 1, key, -1);
+    return TRUE;
+  } else
+    return FALSE;
+}
+
+/** @brief Delete obsolete rows
+ * @param parent Parent or NULL
+ * @param exists List of rows that should exist (by key)
+ * @param nexists Length of @p exists
+ */
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+                                            char **exists,
+                                            int nexists) {
+  /* Delete anything that shouldn't exist. */
+  GtkTreeIter iter[1];
+  gboolean it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+                                             iter,
+                                             parent);
+  while(it) {
+    /* Find the value at row 'next' */
+    gchar *candidate;
+    gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+                       iter,
+                       1, &candidate,
+                       -1);
+    gboolean found = FALSE;
+    for(int n = 0; n < nexists; ++n)
+      if((found = !strcmp(candidate, exists[n])))
+        break;
+    if(!found)
+      it = gtk_tree_store_remove(playlist_picker_list, iter);
+    else
+      it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list),
+                                    iter);
+    g_free(candidate);
+  }
 }
 
 /** @brief Called when the selection might have changed */
-static void playlists_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
-                                        gpointer attribute((unused)) user_data) {
+static void playlist_picker_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
+                                              gpointer attribute((unused)) user_data) {
   GtkTreeIter iter;
   char *gselected, *selected;
   
   /* Identify the current selection */
-  if(gtk_tree_selection_get_selected(playlists_selection, 0, &iter)) {
-    gtk_tree_model_get(GTK_TREE_MODEL(playlists_list), &iter,
-                       0, &gselected, -1);
+  if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)
+     && gtk_tree_store_iter_depth(playlist_picker_list, &iter) > 0) {
+    gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), &iter,
+                       1, &gselected, -1);
     selected = xstrdup(gselected);
     g_free(gselected);
   } else
     selected = 0;
+  /* Set button sensitivity according to the new state */
+  int deletable = FALSE;
+  if(selected) {
+    if(strchr(selected, '.')) {
+      if(!strncmp(selected, config->username, strlen(config->username)))
+        deletable = TRUE;
+    } else
+      deletable = TRUE;
+  }
+  gtk_widget_set_sensitive(playlist_picker_delete_button, deletable);
   /* Eliminate no-change cases */
-  if(!selected && !playlists_selected)
+  if(!selected && !playlist_picker_selected)
     return;
-  if(selected && playlists_selected && !strcmp(selected, playlists_selected))
+  if(selected
+     && playlist_picker_selected
+     && !strcmp(selected, playlist_picker_selected))
     return;
-  /* There's been a change */
-  playlists_selected = selected;
-  if(playlists_selected) {
-    fprintf(stderr, "playlists selection changed\n'"); /* TODO */
-    gtk_widget_set_sensitive(playlists_delete_button, 1);
-  } else
-    gtk_widget_set_sensitive(playlists_delete_button, 0);
+  /* Record the new state */
+  playlist_picker_selected = selected;
+  /* Re-initalize the queue */
+  ql_new_queue(&ql_playlist, NULL);
+  /* Synthesize a playlist-modified to re-initialize the editor etc */
+  event_raise("playlist-modified", (void *)playlist_picker_selected);
 }
 
 /** @brief Called when the 'add' button is pressed */
-static void playlists_add(GtkButton attribute((unused)) *button,
-                          gpointer attribute((unused)) userdata) {
-  /* Unselect whatever is selected */
-  gtk_tree_selection_unselect_all(playlists_selection);
-  fprintf(stderr, "playlists_add\n");/* TODO */
+static void playlist_picker_add(GtkButton attribute((unused)) *button,
+                                gpointer attribute((unused)) userdata) {
+  /* Unselect whatever is selected TODO why?? */
+  gtk_tree_selection_unselect_all(playlist_picker_selection);
+  playlist_new_playlist();
+}
+
+/** @brief Called when playlist deletion completes */
+static void playlists_picker_delete_completed(void attribute((unused)) *v,
+                                              const char *err) {
+  if(err)
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
 }
 
 /** @brief Called when the 'Delete' button is pressed */
-static void playlists_delete(GtkButton attribute((unused)) *button,
-                        gpointer attribute((unused)) userdata) {
+static void playlist_picker_delete(GtkButton attribute((unused)) *button,
+                                   gpointer attribute((unused)) userdata) {
   GtkWidget *yesno;
   int res;
 
-  if(!playlists_selected)
-    return;                             /* shouldn't happen */
-  yesno = gtk_message_dialog_new(GTK_WINDOW(playlists_window),
+  if(!playlist_picker_selected)
+    return;
+  yesno = gtk_message_dialog_new(GTK_WINDOW(playlist_window),
                                  GTK_DIALOG_MODAL,
                                  GTK_MESSAGE_QUESTION,
                                  GTK_BUTTONS_YES_NO,
-                                 "Do you really want to delete user %s?"
+                                 "Do you really want to delete playlist %s?"
                                  " This action cannot be undone.",
-                                 playlists_selected);
+                                 playlist_picker_selected);
   res = gtk_dialog_run(GTK_DIALOG(yesno));
   gtk_widget_destroy(yesno);
   if(res == GTK_RESPONSE_YES) {
     disorder_eclient_playlist_delete(client,
-                                     NULL/*playlists_delete_completed*/,
-                                     playlists_selected,
+                                     playlists_picker_delete_completed,
+                                     playlist_picker_selected,
                                      NULL);
   }
 }
 
 /** @brief Table of buttons below the playlist list */
-static struct button playlists_buttons[] = {
+static struct button playlist_picker_buttons[] = {
   {
     GTK_STOCK_ADD,
-    playlists_add,
+    playlist_picker_add,
     "Create a new playlist",
-    0
+    0,
+    NULL,
   },
   {
     GTK_STOCK_REMOVE,
-    playlists_delete,
+    playlist_picker_delete,
     "Delete a playlist",
+    0,
+    NULL,
+  },
+};
+#define NPLAYLIST_PICKER_BUTTONS (sizeof playlist_picker_buttons / sizeof *playlist_picker_buttons)
+
+/** @brief Create the list of playlists for the edit playlists window */
+static GtkWidget *playlist_picker_create(void) {
+  /* Create the list of playlist and populate it */
+  playlist_picker_fill(NULL, NULL, NULL);
+  /* Create the tree view */
+  GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlist_picker_list));
+  /* ...and the renderers for it */
+  GtkCellRenderer *cr = gtk_cell_renderer_text_new();
+  GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes("Playlist",
+                                                                    cr,
+                                                                    "text", 0,
+                                                                    NULL);
+  gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
+  /* Get the selection for the view; set its mode; arrange for a callback when
+   * it changes */
+  playlist_picker_selected = NULL;
+  playlist_picker_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+  gtk_tree_selection_set_mode(playlist_picker_selection, GTK_SELECTION_BROWSE);
+  g_signal_connect(playlist_picker_selection, "changed",
+                   G_CALLBACK(playlist_picker_selection_changed), NULL);
+
+  /* Create the control buttons */
+  GtkWidget *buttons = create_buttons_box(playlist_picker_buttons,
+                                          NPLAYLIST_PICKER_BUTTONS,
+                                          gtk_hbox_new(FALSE, 1));
+  playlist_picker_delete_button = playlist_picker_buttons[1].widget;
+
+  playlist_picker_selection_changed(NULL, NULL);
+
+  /* Buttons live below the list */
+  GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+
+  g_signal_connect(tree, "key-press-event",
+                   G_CALLBACK(playlist_picker_keypress), 0);
+  g_signal_connect(tree, "button-press-event",
+                   G_CALLBACK(playlist_picker_button), 0);
+
+  return vbox;
+}
+
+static gboolean playlist_picker_keypress(GtkWidget attribute((unused)) *widget,
+                                         GdkEventKey *event,
+                                         gpointer attribute((unused)) user_data) {
+  if(event->state)
+    return FALSE;
+  switch(event->keyval) {
+  case GDK_BackSpace:
+  case GDK_Delete:
+    playlist_picker_delete(NULL, NULL);
+    return TRUE;
+  default:
+    return FALSE;
+  }
+}
+
+static void playlist_picker_select_activate(GtkMenuItem attribute((unused)) *item,
+                                            gpointer attribute((unused)) userdata) {
+  /* nothing */
+}
+
+static int playlist_picker_select_sensitive(void *extra) {
+  GtkTreeIter *iter = extra;
+  return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_play_activate(GtkMenuItem attribute((unused)) *item,
+                                          gpointer attribute((unused)) userdata) {
+  /* Re-use the menu-based activation callback */
+  disorder_eclient_playlist_get(client, playlist_menu_received_content,
+                                playlist_picker_selected, NULL);
+}
+
+static int playlist_picker_play_sensitive(void *extra) {
+  GtkTreeIter *iter = extra;
+  return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_remove_activate(GtkMenuItem attribute((unused)) *item,
+                                            gpointer attribute((unused)) userdata) {
+  /* Re-use the 'Remove' button' */
+  playlist_picker_delete(NULL, NULL);
+}
+
+static int playlist_picker_remove_sensitive(void *extra) {
+  GtkTreeIter *iter = extra;
+  if(gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0) {
+    if(strchr(playlist_picker_selected, '.')) {
+      if(!strncmp(playlist_picker_selected, config->username,
+                  strlen(config->username)))
+        return TRUE;
+    } else
+      return TRUE;
+  }
+  return FALSE;
+}
+
+/** @brief Pop-up menu for picker */
+static struct menuitem playlist_picker_menuitems[] = {
+  {
+    "Select playlist",
+    NULL,
+    playlist_picker_select_activate,
+    playlist_picker_select_sensitive,
+    0,
+    0
+  },
+  {
+    "Play playlist",
+    GTK_STOCK_MEDIA_PLAY, 
+    playlist_picker_play_activate,
+    playlist_picker_play_sensitive,
+    0,
+    0
+  },
+  {
+    "Remove playlist",
+    GTK_STOCK_DELETE,
+    playlist_picker_remove_activate,
+    playlist_picker_remove_sensitive,
+    0,
     0
   },
 };
-#define NPLAYLISTS_BUTTONS (sizeof playlists_buttons / sizeof *playlists_buttons)
 
-/** @brief Keypress handler */
-static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget,
-                                   GdkEventKey *event,
-                                   gpointer attribute((unused)) user_data) {
+static gboolean playlist_picker_button(GtkWidget *widget,
+                                       GdkEventButton *event,
+                                       gpointer attribute((unused)) user_data) {
+  if(event->type == GDK_BUTTON_PRESS && event->button == 3) {
+    static GtkWidget *playlist_picker_menu;
+
+    /* Right click press pops up a menu */
+    ensure_selected(GTK_TREE_VIEW(widget), event);
+    /* Find the selected row */
+    GtkTreeIter iter[1];
+    if(!gtk_tree_selection_get_selected(playlist_picker_selection, 0, iter))
+      return TRUE;
+    popup(&playlist_picker_menu, event,
+          playlist_picker_menuitems,
+          sizeof playlist_picker_menuitems / sizeof *playlist_picker_menuitems,
+          iter);
+    return TRUE;
+  }
+  return FALSE;
+}
+
+static void playlist_picker_destroy(void) {
+  playlist_picker_delete_button = NULL;
+  g_object_unref(playlist_picker_list);
+  playlist_picker_list = NULL;
+  playlist_picker_selection = NULL;
+  playlist_picker_selected = NULL;
+}
+
+/* Playlist editor ---------------------------------------------------------- */
+
+static GtkWidget *playlist_editor_shared;
+static GtkWidget *playlist_editor_public;
+static GtkWidget *playlist_editor_private;
+static int playlist_editor_setting_buttons;
+
+/** @brief Buttons for the playlist window */
+static struct button playlist_editor_buttons[] = {
+  {
+    GTK_STOCK_OK,
+    playlist_editor_ok,
+    "Close window",
+    0,
+    gtk_box_pack_end,
+  },
+  {
+    GTK_STOCK_HELP,
+    playlist_editor_help,
+    "Go to manual",
+    0,
+    gtk_box_pack_end,
+  },
+};
+
+#define NPLAYLIST_EDITOR_BUTTONS (int)(sizeof playlist_editor_buttons / sizeof *playlist_editor_buttons)
+
+static GtkWidget *playlists_editor_create(void) {
+  assert(ql_playlist.view == NULL);     /* better not be set up already */
+
+  GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
+  playlist_editor_shared = gtk_radio_button_new_with_label(NULL, "shared");
+  playlist_editor_public
+    = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+                                                  "public");
+  playlist_editor_private
+    = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+                                                  "private");
+  g_signal_connect(playlist_editor_public, "toggled",
+                   G_CALLBACK(playlist_editor_button_toggled),
+                   (void *)"public");
+  g_signal_connect(playlist_editor_private, "toggled",
+                   G_CALLBACK(playlist_editor_button_toggled),
+                   (void *)"private");
+  gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_shared,
+                     FALSE/*expand*/, FALSE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_public,
+                     FALSE/*expand*/, FALSE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_private,
+                     FALSE/*expand*/, FALSE/*fill*/, 0);
+  playlist_editor_set_buttons(0,0,0);
+  create_buttons_box(playlist_editor_buttons,
+                     NPLAYLIST_EDITOR_BUTTONS,
+                     hbox);
+
+  GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
+  GtkWidget *view = init_queuelike(&ql_playlist);
+  gtk_box_pack_start(GTK_BOX(vbox), view,
+                     TRUE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(vbox), hbox,
+                     FALSE/*expand*/, FALSE/*fill*/, 0);
+  g_signal_connect(view, "key-press-event",
+                   G_CALLBACK(playlist_editor_keypress), 0);
+  return vbox;
+}
+
+static gboolean playlist_editor_keypress(GtkWidget attribute((unused)) *widget,
+                                         GdkEventKey *event,
+                                         gpointer attribute((unused)) user_data) {
   if(event->state)
     return FALSE;
   switch(event->keyval) {
-  case GDK_Escape:
-    gtk_widget_destroy(playlists_window);
+  case GDK_BackSpace:
+  case GDK_Delete:
+    playlist_remove_activate(NULL, NULL);
     return TRUE;
   default:
     return FALSE;
   }
 }
 
-void edit_playlists(gpointer attribute((unused)) callback_data,
-                     guint attribute((unused)) callback_action,
-                     GtkWidget attribute((unused)) *menu_item) {
-  GtkWidget *tree, *hbox, *vbox, *buttons;
-  GtkCellRenderer *cr;
-  GtkTreeViewColumn *col;
+/** @brief Called when the public/private buttons are set */
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+                                           gpointer userdata) {
+  const char *state = userdata;
+  if(!gtk_toggle_button_get_active(tb)
+     || !playlist_picker_selected
+     || playlist_editor_setting_buttons)
+    return;
+  disorder_eclient_playlist_set_share(client, playlist_editor_share_set,
+                                      playlist_picker_selected, state, NULL);
+}
+
+static void playlist_editor_share_set(void attribute((unused)) *v,
+                                      const attribute((unused)) char *err) {
+  if(err)
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+  
+/** @brief Set the editor button state and sensitivity */
+static void playlist_editor_set_buttons(const char attribute((unused)) *event,
+                                        void *eventdata,
+                                        void attribute((unused)) *callbackdata) {
+  /* If this event is for a non-selected playlist do nothing */
+  if(eventdata
+     && playlist_picker_selected
+     && strcmp(eventdata, playlist_picker_selected))
+    return;
+  if(playlist_picker_selected) {
+    if(strchr(playlist_picker_selected, '.'))
+      disorder_eclient_playlist_get_share(client,
+                                          playlist_editor_got_share,
+                                          playlist_picker_selected,
+                                          (void *)playlist_picker_selected);
+    else
+      playlist_editor_got_share((void *)playlist_picker_selected, NULL,
+                                "shared");
+  } else
+    playlist_editor_got_share(NULL, NULL, NULL);
+}
+
+/** @brief Called with playlist sharing details */
+static void playlist_editor_got_share(void *v,
+                                      const char *err,
+                                      const char *value) {
+  const char *playlist = v;
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    value = NULL;
+  }
+  /* Set the currently active button */
+  ++playlist_editor_setting_buttons;
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_shared),
+                               value && !strcmp(value, "shared"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_public),
+                               value && !strcmp(value, "public"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_private),
+                               value && !strcmp(value, "private"));
+  /* Set button sensitivity */
+  gtk_widget_set_sensitive(playlist_editor_shared, FALSE);
+  int sensitive = (playlist
+                   && strchr(playlist, '.')
+                   && !strncmp(playlist, config->username,
+                               strlen(config->username)));
+  gtk_widget_set_sensitive(playlist_editor_public, sensitive);
+  gtk_widget_set_sensitive(playlist_editor_private, sensitive);
+  --playlist_editor_setting_buttons;
+}
+
+/** @brief (Re-)populate the playlist tree model */
+static void playlist_editor_fill(const char attribute((unused)) *event,
+                                 void *eventdata,
+                                 void attribute((unused)) *callbackdata) {
+  const char *modified_playlist = eventdata;
+  if(!playlist_window)
+    return;
+  if(!playlist_picker_selected)
+    return;
+  if(!strcmp(playlist_picker_selected, modified_playlist))
+    disorder_eclient_playlist_get(client, playlists_editor_received_tracks,
+                                  playlist_picker_selected,
+                                  (void *)playlist_picker_selected);
+}
+
+/** @brief Called with new tracks for the playlist */
+static void playlists_editor_received_tracks(void *v,
+                                             const char *err,
+                                             int nvec, char **vec) {
+  const char *playlist = v;
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    return;
+  }
+  if(!playlist_picker_selected
+     || strcmp(playlist, playlist_picker_selected)) {
+    /* The tracks are for the wrong playlist - something must have changed
+     * while the fetch command was in flight.  We just ignore this callback,
+     * the right answer will be requested and arrive in due course. */
+    return;
+  }
+  if(nvec == -1)
+    /* No such playlist, presumably we'll get a deleted event shortly */
+    return;
+  /* Translate the list of tracks into queue entries */
+  struct queue_entry *newq, **qq = &newq, *qprev = NULL;
+  hash *h = hash_new(sizeof(int));
+  for(int n = 0; n < nvec; ++n) {
+    struct queue_entry *q = xmalloc(sizeof *q);
+    q->prev = qprev;
+    q->track = vec[n];
+    /* Synthesize a unique ID so that the selection survives updates.  Tracks
+     * can appear more than once in the queue so we can't use raw track names,
+     * so we add a serial number to the start. */
+    int *serialp = hash_find(h, vec[n]), serial = serialp ? *serialp : 0;
+    byte_xasprintf((char **)&q->id, "%d-%s", serial++, vec[n]);
+    hash_add(h, vec[n], &serial, HASH_INSERT_OR_REPLACE);
+    *qq = q;
+    qq = &q->next;
+    qprev = q;
+  }
+  *qq = NULL;
+  ql_new_queue(&ql_playlist, newq);
+}
+
+static void playlist_editor_ok(GtkButton attribute((unused)) *button, 
+                               gpointer attribute((unused)) userdata) {
+  gtk_widget_destroy(playlist_window);
+}
+
+static void playlist_editor_help(GtkButton attribute((unused)) *button, 
+                                 gpointer attribute((unused)) userdata) {
+  popup_help("playlists.html");
+}
+
+/* Playlist mutation -------------------------------------------------------- */
+
+/** @brief State structure for guarded playlist modification
+ *
+ * To safely move, insert or delete rows we must:
+ * - take a lock
+ * - fetch the playlist
+ * - verify it's not changed
+ * - update the playlist contents
+ * - store the playlist
+ * - release the lock
+ *
+ * The playlist_modify_ functions do just that.
+ *
+ * To kick things off create one of these and disorder_eclient_playlist_lock()
+ * with playlist_modify_locked() as its callback.  @c modify will be called; it
+ * should disorder_eclient_playlist_set() to set the new state with
+ * playlist_modify_updated() as its callback.
+ */
+struct playlist_modify_data {
+  /** @brief Affected playlist */
+  const char *playlist;
+  /** @brief Modification function
+   * @param mod Pointer back to state structure
+   * @param ntracks Length of playlist
+   * @param tracks Tracks in playlist
+   */
+  void (*modify)(struct playlist_modify_data *mod,
+                int ntracks, char **tracks);
+
+  /** @brief Number of tracks dropped */
+  int ntracks;
+  /** @brief Track names dropped */
+  char **tracks;
+  /** @brief Track IDs dropped */
+  char **ids;
+  /** @brief Drop after this point */
+  struct queue_entry *after_me;
+};
+
+/** @brief Called with playlist locked
+ *
+ * This is the entry point for guarded modification ising @ref
+ * playlist_modify_data.
+ */
+static void playlist_modify_locked(void *v, const char *err) {
+  struct playlist_modify_data *mod = v;
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    return;
+  }
+  disorder_eclient_playlist_get(client, playlist_modify_retrieved,
+                                mod->playlist, mod);
+}
+
+/** @brief Called with current playlist contents
+ * Checks that the playlist is still current and has not changed.
+ */
+void playlist_modify_retrieved(void *v, const char *err,
+                               int nvec,
+                               char **vec) {
+  struct playlist_modify_data *mod = v;
+  if(err) {
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+    disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+    return;
+  }
+  if(nvec < 0
+     || !playlist_picker_selected
+     || strcmp(mod->playlist, playlist_picker_selected)) {
+    disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+    return;
+  }
+  /* We check that the contents haven't changed.  If they have we just abandon
+   * the operation.  The user will have to try again. */
+  struct queue_entry *q;
+  int n;
+  for(n = 0, q = ql_playlist.q; q && n < nvec; ++n, q = q->next)
+    if(strcmp(q->track, vec[n]))
+      break;
+  if(n != nvec || q != NULL)  {
+    disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+    return;
+  }
+  mod->modify(mod, nvec, vec);
+}
+
+/** @brief Called when the playlist has been updated */
+static void playlist_modify_updated(void attribute((unused)) *v,
+                                    const char *err) {
+  if(err) 
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+  disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+}
+
+/** @brief Called when the playlist has been unlocked */
+static void playlist_modify_unlocked(void attribute((unused)) *v,
+                                     const char *err) {
+  if(err) 
+    popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+
+/* Drop tracks into a playlist ---------------------------------------------- */
+
+static void playlist_drop(struct queuelike attribute((unused)) *ql,
+                          int ntracks,
+                          char **tracks, char **ids,
+                          struct queue_entry *after_me) {
+  struct playlist_modify_data *mod = xmalloc(sizeof *mod);
+
+  mod->playlist = playlist_picker_selected;
+  mod->modify = playlist_drop_modify;
+  mod->ntracks = ntracks;
+  mod->tracks = tracks;
+  mod->ids = ids;
+  mod->after_me = after_me;
+  disorder_eclient_playlist_lock(client, playlist_modify_locked,
+                                 mod->playlist, mod);
+}
+
+/** @brief Return true if track @p i is in the moved set */
+static int playlist_drop_is_moved(struct playlist_modify_data *mod,
+                                  int i) {
+  struct queue_entry *q;
+
+  /* Find the q corresponding to i, so we can get the ID */
+  for(q = ql_playlist.q; i; q = q->next, --i)
+    ;
+  /* See if track i matches any of the moved set by ID */
+  for(int n = 0; n < mod->ntracks; ++n)
+    if(!strcmp(q->id, mod->ids[n]))
+      return 1;
+  return 0;
+}
+
+static void playlist_drop_modify(struct playlist_modify_data *mod,
+                                 int nvec, char **vec) {
+  char **newvec;
+  int nnewvec;
+
+  //fprintf(stderr, "\nplaylist_drop_modify\n");
+  /* after_me is the queue_entry to insert after, or NULL to insert at the
+   * beginning (including the case when the playlist is empty) */
+  //fprintf(stderr, "after_me = %s\n",
+  //        mod->after_me ? mod->after_me->track : "NULL");
+  struct queue_entry *q = ql_playlist.q;
+  int ins = 0;
+  if(mod->after_me) {
+    ++ins;
+    while(q && q != mod->after_me) {
+      q = q->next;
+      ++ins;
+    }
+  }
+  /* Now ins is the index to insert at; equivalently, the row to insert before,
+   * and so equal to nvec to append. */
+#if 0
+  fprintf(stderr, "ins = %d = %s\n",
+          ins, ins < nvec ? vec[ins] : "NULL");
+  for(int n = 0; n < nvec; ++n)
+    fprintf(stderr, "%d: %s %s\n", n, n == ins ? "->" : "  ", vec[n]);
+  fprintf(stderr, "nvec = %d\n", nvec);
+#endif
+  if(mod->ids) {
+    /* This is a rearrangement */
+    /* We have:
+     * - vec[], the current layout
+     * - ins, pointing into vec
+     * - mod->tracks[], a subset of vec[] which is to be moved
+     *
+     * ins is the insertion point BUT it is in terms of the whole
+     * array, i.e. before mod->tracks[] have been removed.  The first
+     * step then is to remove everything in mod->tracks[] and adjust
+     * ins downwards as necessary.
+     */
+    /* First zero out anything that's moved */
+    int before_ins = 0;
+    for(int n = 0; n < nvec; ++n) {
+      if(playlist_drop_is_moved(mod, n)) {
+        vec[n] = NULL;
+        if(n < ins)
+          ++before_ins;
+      }
+    }
+    /* Now collapse down the array */
+    int i = 0;
+    for(int n = 0; n < nvec; ++n) {
+      if(vec[n])
+        vec[i++] = vec[n];
+    }
+    assert(i + mod->ntracks == nvec);
+    nvec = i;
+    /* Adjust the insertion point to take account of things moved from before
+     * it */
+    ins -= before_ins;
+    /* The effect is now the same as an insertion */
+  }
+  /* This is (now) an insertion */
+  nnewvec = nvec + mod->ntracks;
+  newvec = xcalloc(nnewvec, sizeof (char *));
+  memcpy(newvec, vec,
+         ins * sizeof (char *));
+  memcpy(newvec + ins, mod->tracks,
+         mod->ntracks * sizeof (char *));
+  memcpy(newvec + ins + mod->ntracks, vec + ins,
+         (nvec - ins) * sizeof (char *));
+  disorder_eclient_playlist_set(client, playlist_modify_updated, mod->playlist,
+                                newvec, nnewvec, mod);
+}
+
+/* Playlist editor right-click menu ---------------------------------------- */
+
+/** @brief Called to determine whether the playlist is playable */
+static int playlist_playall_sensitive(void attribute((unused)) *extra) {
+  /* If there's no playlist obviously we can't play it */
+  if(!playlist_picker_selected)
+    return FALSE;
+  /* If it's empty we can't play it */
+  if(!ql_playlist.q)
+    return FALSE;
+  /* Otherwise we can */
+  return TRUE;
+}
+
+/** @brief Called to play the selected playlist */
+static void playlist_playall_activate(GtkMenuItem attribute((unused)) *menuitem,
+                                      gpointer attribute((unused)) user_data) {
+  if(!playlist_picker_selected)
+    return;
+  /* Re-use the menu-based activation callback */
+  disorder_eclient_playlist_get(client, playlist_menu_received_content,
+                                playlist_picker_selected, NULL);
+}
+
+/** @brief Called to determine whether the playlist is playable */
+static int playlist_remove_sensitive(void attribute((unused)) *extra) {
+  /* If there's no playlist obviously we can't remove from it */
+  if(!playlist_picker_selected)
+    return FALSE;
+  /* If no tracks are selected we cannot remove them */
+  if(!gtk_tree_selection_count_selected_rows(ql_playlist.selection))
+    return FALSE;
+  /* We're good to go */
+  return TRUE;
+}
+
+/** @brief Called to remove the selected playlist */
+static void playlist_remove_activate(GtkMenuItem attribute((unused)) *menuitem,
+                                     gpointer attribute((unused)) user_data) {
+  if(!playlist_picker_selected)
+    return;
+  struct playlist_modify_data *mod = xmalloc(sizeof *mod);
+
+  mod->playlist = playlist_picker_selected;
+  mod->modify = playlist_remove_modify;
+  disorder_eclient_playlist_lock(client, playlist_modify_locked,
+                                 mod->playlist, mod);
+}
+
+static void playlist_remove_modify(struct playlist_modify_data *mod,
+                                   int attribute((unused)) nvec, char **vec) {
+  GtkTreeIter iter[1];
+  gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql_playlist.store),
+                                              iter);
+  int n = 0, m = 0;
+  while(it) {
+    if(!gtk_tree_selection_iter_is_selected(ql_playlist.selection, iter))
+      vec[m++] = vec[n++];
+    else
+      n++;
+    it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_playlist.store), iter);
+  }
+  disorder_eclient_playlist_set(client, playlist_modify_updated, mod->playlist,
+                                vec, m, mod);
+}
+
+/* Playlists window --------------------------------------------------------- */
 
+/** @brief Pop up the playlists window
+ *
+ * Called when the playlists menu item is selected
+ */
+void playlist_window_create(gpointer attribute((unused)) callback_data,
+                            guint attribute((unused)) callback_action,
+                            GtkWidget attribute((unused)) *menu_item) {
   /* If the window already exists, raise it */
-  if(playlists_window) {
-    gtk_window_present(GTK_WINDOW(playlists_window));
+  if(playlist_window) {
+    gtk_window_present(GTK_WINDOW(playlist_window));
     return;
   }
   /* Create the window */
-  playlists_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-  gtk_widget_set_style(playlists_window, tool_style);
-  g_signal_connect(playlists_window, "destroy",
-                  G_CALLBACK(gtk_widget_destroyed), &playlists_window);
-  gtk_window_set_title(GTK_WINDOW(playlists_window), "Playlists Management");
+  playlist_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  gtk_widget_set_style(playlist_window, tool_style);
+  g_signal_connect(playlist_window, "destroy",
+                  G_CALLBACK(playlist_window_destroyed), &playlist_window);
+  gtk_window_set_title(GTK_WINDOW(playlist_window), "Playlists Management");
   /* TODO loads of this is very similar to (copied from!) users.c - can we
    * de-dupe? */
   /* Keyboard shortcuts */
-  g_signal_connect(playlists_window, "key-press-event",
-                   G_CALLBACK(playlists_keypress), 0);
+  g_signal_connect(playlist_window, "key-press-event",
+                   G_CALLBACK(playlist_window_keypress), 0);
   /* default size is too small */
-  gtk_window_set_default_size(GTK_WINDOW(playlists_window), 240, 240);
-  /* Create the list of playlist and populate it */
-  playlists_fill();
-  /* Create the tree view */
-  tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlists_list));
-  /* ...and the renderers for it */
-  cr = gtk_cell_renderer_text_new();
-  col = gtk_tree_view_column_new_with_attributes("Playlist",
-                                                cr,
-                                                "text", 0,
-                                                NULL);
-  gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
-  /* Get the selection for the view; set its mode; arrange for a callback when
-   * it changes */
-  playlists_selected = NULL;
-  playlists_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
-  gtk_tree_selection_set_mode(playlists_selection, GTK_SELECTION_BROWSE);
-  g_signal_connect(playlists_selection, "changed",
-                   G_CALLBACK(playlists_selection_changed), NULL);
+  gtk_window_set_default_size(GTK_WINDOW(playlist_window), 640, 320);
 
-  /* Create the control buttons */
-  buttons = create_buttons_box(playlists_buttons,
-                              NPLAYLISTS_BUTTONS,
-                              gtk_hbox_new(FALSE, 1));
-  playlists_delete_button = playlists_buttons[1].widget;
+  GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), playlist_picker_create(),
+                     FALSE/*expand*/, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(),
+                     FALSE/*expand*/, FALSE, 2);
+  gtk_box_pack_start(GTK_BOX(hbox), playlists_editor_create(),
+                     TRUE/*expand*/, TRUE/*fill*/, 0);
 
-  /* Buttons live below the list */
-  vbox = gtk_vbox_new(FALSE, 0);
-  gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
-  gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+  gtk_container_add(GTK_CONTAINER(playlist_window), frame_widget(hbox, NULL));
+  gtk_widget_show_all(playlist_window);
+}
+
+/** @brief Keypress handler */
+static gboolean playlist_window_keypress(GtkWidget attribute((unused)) *widget,
+                                         GdkEventKey *event,
+                                         gpointer attribute((unused)) user_data) {
+  if(event->state)
+    return FALSE;
+  switch(event->keyval) {
+  case GDK_Escape:
+    gtk_widget_destroy(playlist_window);
+    return TRUE;
+  default:
+    return FALSE;
+  }
+}
 
-  hbox = gtk_hbox_new(FALSE, 0);
-  gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE/*expand*/, FALSE, 0);
-  gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(), FALSE/*expand*/, FALSE, 2);
-  // TODO something to edit the playlist in
-  //gtk_box_pack_start(GTK_BOX(hbox), vbox2, TRUE/*expand*/, TRUE/*fill*/, 0);
-  gtk_container_add(GTK_CONTAINER(playlists_window), frame_widget(hbox, NULL));
-  gtk_widget_show_all(playlists_window);
+/** @brief Called when the playlist window is destroyed */
+static void playlist_window_destroyed(GtkWidget attribute((unused)) *widget,
+                                      GtkWidget **widget_pointer) {
+  destroy_queuelike(&ql_playlist);
+  playlist_picker_destroy();
+  *widget_pointer = NULL;
 }
 
 /** @brief Initialize playlist support */
 void playlists_init(void) {
   /* We re-get all playlists upon any change... */
-  event_register("playlist-created", playlists_update, 0);
-  event_register("playlist-modified", playlists_update, 0);
-  event_register("playlist-deleted", playlists_update, 0);
+  event_register("playlist-created", playlist_list_update, 0);
+  event_register("playlist-deleted", playlist_list_update, 0);
   /* ...and on reconnection */
-  event_register("log-connected", playlists_update, 0);
+  event_register("log-connected", playlist_list_update, 0);
   /* ...and from time to time */
-  event_register("periodic-slow", playlists_update, 0);
+  event_register("periodic-slow", playlist_list_update, 0);
   /* ...and at startup */
-  event_register("playlists-updated", menu_playlists_changed, 0);
-  playlists_update(0, 0, 0);
-}
+  playlist_list_update(0, 0, 0);
 
-#endif
+  /* Update the playlists menu when the set of playlists changes */
+  event_register("playlists-updated", playlist_menu_changed, 0);
+  /* Update the new-playlist OK button when the set of playlists changes */
+  event_register("playlists-updated", playlist_new_changed, 0);
+  /* Update the list of playlists in the edit window when the set changes */
+  event_register("playlists-updated", playlist_picker_fill, 0);
+  /* Update the displayed playlist when it is modified */
+  event_register("playlist-modified", playlist_editor_fill, 0);
+  /* Update the shared/public/etc buttons when a playlist is modified */
+  event_register("playlist-modified", playlist_editor_set_buttons, 0);
+}
 
 /*
 Local Variables:
index 6613cfc4fb7f0691e71621cafb8b03458ec37093..d7e91f34117a6c9a8007867ac0725e308d6c18ea 100644 (file)
@@ -34,7 +34,17 @@ void popup(GtkWidget **menup,
     g_signal_connect(menu, "destroy",
                      G_CALLBACK(gtk_widget_destroyed), menup);
     for(int n = 0; n < nitems; ++n) {
-      items[n].w = gtk_menu_item_new_with_label(items[n].name);
+      if(items[n].stock) {
+        GtkWidget *image = gtk_image_new_from_stock(items[n].stock,
+                                                    GTK_ICON_SIZE_MENU);
+        items[n].w = gtk_image_menu_item_new_with_label(items[n].name);
+        gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(items[n].w),
+                                      image);
+      } else
+        items[n].w = gtk_menu_item_new_with_label(items[n].name);
+      /* TODO accelerators would be useful here.  There might be some
+       * interaction with the main menu accelerators, _except_ for playlist
+       * case!  */
       gtk_menu_attach(GTK_MENU(menu), items[n].w, 0, 1, n, n + 1);
     }
     set_tool_colors(menu);
index 9706e0398b2de45bbe2db7b5dabf1fa0298ed148..25726b2fa451e96ee86bba14626a9c3f4acb8afa 100644 (file)
@@ -26,6 +26,9 @@ struct menuitem {
   /** @brief Menu item name */
   const char *name;
 
+  /** @brief Stock icon name */
+  const char *stock;
+
   /** @brief Called to activate the menu item */
   void (*activate)(GtkMenuItem *menuitem,
                    gpointer user_data);
index 2e31603f7b563d2c69893a9f5e03b689b70ff680..33dbfc979001f674f9ea68679e1a21d6a4bbe8cf 100644 (file)
@@ -30,12 +30,14 @@ struct progress_window {
 };
 
 /** @brief Create a progress window */
-struct progress_window *progress_window_new(const char *title) {
+struct progress_window *progress_window_new(const char *title,
+                                            GtkWidget *parent) {
   struct progress_window *pw = xmalloc(sizeof *pw);
 
   pw->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-  gtk_window_set_transient_for(GTK_WINDOW(pw->window),
-                               GTK_WINDOW(toplevel));
+  if(parent)
+    gtk_window_set_transient_for(GTK_WINDOW(pw->window),
+                                 GTK_WINDOW(parent));
   g_signal_connect(pw->window, "destroy",
                   G_CALLBACK(gtk_widget_destroyed), &pw->window);
   gtk_window_set_default_size(GTK_WINDOW(pw->window), 360, -1);
index 62ea22f5bae6013908e98c9360c75a2176c539d5..6600cf947ed88b0b7f0ed1caccd7e7cf212cf15d 100644 (file)
@@ -46,6 +46,7 @@ static void prefdata_completed(void *v, const char *err, 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);
+static void properties_help(GtkButton *button, gpointer userdata);
 
 static void properties_logged_in(const char *event,
                                  void *eventdata,
@@ -125,23 +126,33 @@ static const struct pref {
 
 /* Buttons that appear at the bottom of the window */
 static struct button buttons[] = {
+  {
+    GTK_STOCK_HELP,
+    properties_help,
+    "Go to manual",
+    0,
+    gtk_box_pack_start,
+  },
   {
     GTK_STOCK_OK,
     properties_ok,
     "Apply all changes and close window",
-    0
-  },
-  {
-    GTK_STOCK_APPLY,
-    properties_apply,
-    "Apply all changes and keep window open",
-    0
+    0,
+    gtk_box_pack_end,
   },
   {
     GTK_STOCK_CANCEL,
     properties_cancel,
     "Discard all changes and close window",
-    0
+    0,
+    gtk_box_pack_end
+  },
+  {
+    GTK_STOCK_APPLY,
+    properties_apply,
+    "Apply all changes and keep window open",
+    0,
+    gtk_box_pack_end,
   },
 };
 
@@ -186,7 +197,8 @@ static gboolean properties_keypress(GtkWidget attribute((unused)) *widget,
   }
 }
 
-void properties(int ntracks, const char **tracks) {
+void properties(int ntracks, const char **tracks,
+                GtkWidget *parent) {
   int n, m;
   struct prefdata *f;
   GtkWidget *buttonbox, *vbox, *label, *entry, *propagate;
@@ -299,7 +311,9 @@ void properties(int ntracks, const char **tracks) {
   if(pw)
     progress_window_progress(pw, 0, 0);
   /* Pop up a progress bar while we're waiting */
-  pw = progress_window_new("Fetching Track Properties");
+  while(parent->parent)
+    parent = parent->parent;
+  pw = progress_window_new("Fetching Track Properties", parent);
 }
 
 /* Everything is filled in now */
@@ -487,6 +501,11 @@ static void properties_cancel(GtkButton attribute((unused)) *button,
   properties_event = 0;
 }
 
+static void properties_help(GtkButton attribute((unused)) *button,
+                            gpointer attribute((unused)) userdata) {
+  popup_help("properties.html");
+}
+
 /** @brief Called when we've just logged in
  *
  * Destroys the current properties window.
index 82bd9398f161acff6c65cf5713425d0f05916467..333187aad785e8a0e62bf6a5d9e58bdf6ace4e1f 100644 (file)
 #include "multidrag.h"
 #include "autoscroll.h"
 
-static const GtkTargetEntry queuelike_targets[] = {
-  {
-    (char *)"text/x-disorder-queued-tracks", /* drag type */
-    GTK_TARGET_SAME_WIDGET,             /* rearrangement within a widget */
-    0                                   /* ID value */
-  },
-  {
-    (char *)"text/x-disorder-playable-tracks", /* drag type */
-    GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
-    1                                     /* ID value */
-  },
-};
-
 /* Track detail lookup ----------------------------------------------------- */
 
 static void queue_lookups_completed(const char attribute((unused)) *event,
@@ -255,22 +242,27 @@ static void record_queue_map(hash *h,
     hash_add(h, id, empty, HASH_INSERT);
     nqd = hash_find(h, id);
   }
-  if(old)
+  if(old) {
+#if DEBUG_QUEUE
+    fprintf(stderr, " old: %s\n", id);
+#endif
     nqd->old = old;
-  if(new)
+  }
+  if(new) {
+#if DEBUG_QUEUE
+    fprintf(stderr, " new: %s\n", id);
+#endif
     nqd->new = new;
+  }
 }
 
-#if 0
+#if DEBUG_QUEUE
 static void dump_queue(struct queue_entry *head, struct queue_entry *mark) {
   for(struct queue_entry *q = head; q; q = q->next) {
     if(q == mark)
-      fprintf(stderr, "!");
-    fprintf(stderr, "%s", q->id);
-    if(q->next)
-      fprintf(stderr, " ");
+      fprintf(stderr, " !");
+    fprintf(stderr, " %s\n", q->id);
   }
-  fprintf(stderr, "\n");
 }
 
 static void dump_rows(struct queuelike *ql) {
@@ -280,11 +272,8 @@ static void dump_rows(struct queuelike *ql) {
   while(it) {
     struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
     it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
-    fprintf(stderr, "%s", q->id);
-    if(it)
-      fprintf(stderr, " ");
+    fprintf(stderr, " %s\n", q->id);
   }
-  fprintf(stderr, "\n");
 }
 #endif
 
@@ -300,11 +289,15 @@ void ql_new_queue(struct queuelike *ql,
   ++suppress_actions;
 
   /* Tell every queue entry which queue owns it */
-  //fprintf(stderr, "%s: filling in q->ql\n", ql->name);
+#if DEBUG_QUEUE
+  fprintf(stderr, "%s: filling in q->ql\n", ql->name);
+#endif
   for(struct queue_entry *q = newq; q; q = q->next)
     q->ql = ql;
 
-  //fprintf(stderr, "%s: constructing h\n", ql->name);
+#if DEBUG_QUEUE
+  fprintf(stderr, "%s: constructing h\n", ql->name);
+#endif
   /* Construct map from id to new and old structures */
   hash *h = hash_new(sizeof(struct newqueue_data));
   for(struct queue_entry *q = ql->q; q; q = q->next)
@@ -314,7 +307,9 @@ void ql_new_queue(struct queuelike *ql,
 
   /* The easy bit: delete rows not present any more.  In the same pass we
    * update the secret column containing the queue_entry pointer. */
-  //fprintf(stderr, "%s: deleting rows...\n", ql->name);
+#if DEBUG_QUEUE
+  fprintf(stderr, "%s: deleting rows...\n", ql->name);
+#endif
   GtkTreeIter iter[1];
   gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
                                               iter);
@@ -328,10 +323,14 @@ void ql_new_queue(struct queuelike *ql,
                          ql->ncolumns + QUEUEPOINTER_COLUMN, nqd->new,
                          -1);
       it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+      /* We'll need the new start time */
+      nqd->new->when = q->when;
       ++kept;
     } else {
       /* Delete this row (and move iter to the next one) */
-      //fprintf(stderr, " delete %s", q->id);
+#if DEBUG_QUEUE
+      fprintf(stderr, " delete %s\n", q->id);
+#endif
       it = gtk_list_store_remove(ql->store, iter);
       ++deleted;
     }
@@ -342,7 +341,9 @@ void ql_new_queue(struct queuelike *ql,
 
   /* We're going to have to support arbitrary rearrangements, so we might as
    * well add new elements at the end. */
-  //fprintf(stderr, "%s: adding rows...\n", ql->name);
+#if DEBUG_QUEUE
+  fprintf(stderr, "%s: adding rows...\n", ql->name);
+#endif
   struct queue_entry *after = 0;
   for(struct queue_entry *q = newq; q; q = q->next) {
     const struct newqueue_data *nqd = hash_find(h, q->id);
@@ -363,7 +364,9 @@ void ql_new_queue(struct queuelike *ql,
       gtk_list_store_set(ql->store, iter,
                          ql->ncolumns + QUEUEPOINTER_COLUMN, q,
                          -1);
-      //fprintf(stderr, " add %s", q->id);
+#if DEBUG_QUEUE
+      fprintf(stderr, " add %s\n", q->id);
+#endif
       ++inserted;
     }
     after = newq;
@@ -376,49 +379,63 @@ void ql_new_queue(struct queuelike *ql,
    * The current code is simple but amounts to a bubble-sort - we might easily
    * called gtk_tree_model_iter_next a couple of thousand times.
    */
-  //fprintf(stderr, "%s: rearranging rows\n", ql->name);
-  //fprintf(stderr, "%s: queue state: ", ql->name);
-  //dump_queue(newq, 0);
-  //fprintf(stderr, "%s: row state: ", ql->name);
-  //dump_rows(ql);
-  it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
-                                              iter);
-  struct queue_entry *rq = newq;        /* r for 'right, correct' */
+#if DEBUG_QUEUE
+  fprintf(stderr, "%s: rearranging rows\n", ql->name);
+  fprintf(stderr, "%s: target state:\n", ql->name);
+  dump_queue(newq, 0);
+  fprintf(stderr, "%s: current state:\n", ql->name);
+  dump_rows(ql);
+#endif
+  it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
+  struct queue_entry *tq = newq;        /* t-for-target */
   int swaps = 0, searches = 0;
+  int row = 0;
   while(it) {
-    struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
-    //fprintf(stderr, " rq = %p, q = %p\n", rq, q);
-    //fprintf(stderr, " rq->id = %s, q->id = %s\n", rq->id, q->id);
-
-    if(q != rq) {
-      //fprintf(stderr, "  mismatch\n");
+    struct queue_entry *cq = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
+    /* c-for-current */
+
+    /* Everything has the right queue pointer (see above) so it's sufficient to
+     * compare pointers to detect mismatches */
+    if(cq != tq) {
+#if DEBUG_QUEUE
+      fprintf(stderr, "  pointer mismatch at row %d\n", row);
+      fprintf(stderr, "   target id %s\n", tq->id);
+      fprintf(stderr, "   actual id %s\n", cq->id);
+#endif
+      /* Start looking for the target row fromn the next row */
       GtkTreeIter next[1] = { *iter };
       gboolean nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
       while(nit) {
         struct queue_entry *nq = ql_iter_to_q(GTK_TREE_MODEL(ql->store), next);
-        //fprintf(stderr, "   candidate: %s\n", nq->id);
-        if(nq == rq)
+#if DEBUG_QUEUE
+        fprintf(stderr, "   candidate: %s\n", nq->id);
+#endif
+        if(nq == tq)
           break;
         nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
         ++searches;
       }
+      /* Note that this assertion will fail in the face of duplicate IDs.
+       * q->id really does need to be unique. */
       assert(nit);
-      //fprintf(stderr, "  found it\n");
       gtk_list_store_swap(ql->store, iter, next);
       *iter = *next;
-      //fprintf(stderr, "%s: new row state: ", ql->name);
-      //dump_rows(ql);
+#if DEBUG_QUEUE
+      fprintf(stderr, "%s: found it.  new row state:\n", ql->name);
+      dump_rows(ql);
+#endif
       ++swaps;
     }
     /* ...and onto the next one */
     it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
-    rq = rq->next;
+    tq = tq->next;
+    ++row;
   }
-#if 0
+#if DEBUG_QUEUE
   fprintf(stderr, "%6s: %3d kept %3d inserted %3d deleted %3d swaps %4d searches\n", ql->name,
           kept, inserted, deleted, swaps, searches);
+  fprintf(stderr, "done\n");
 #endif
-  //fprintf(stderr, "done\n");
   ql->q = newq;
   /* Set the rest of the columns in new rows */
   ql_update_list_store(ql);
@@ -490,6 +507,28 @@ static GtkTreePath *ql_drop_path(GtkWidget *w,
   return path;
 }
 
+#if 0
+static const char *act(GdkDragAction action) {
+  struct dynstr d[1];
+
+  dynstr_init(d);
+  if(action & GDK_ACTION_DEFAULT)
+    dynstr_append_string(d, "|DEFAULT");
+  if(action & GDK_ACTION_COPY)
+    dynstr_append_string(d, "|COPY");
+  if(action & GDK_ACTION_MOVE)
+    dynstr_append_string(d, "|MOVE");
+  if(action & GDK_ACTION_LINK)
+    dynstr_append_string(d, "|LINK");
+  if(action & GDK_ACTION_PRIVATE)
+    dynstr_append_string(d, "|PRIVATE");
+  if(action & GDK_ACTION_ASK)
+    dynstr_append_string(d, "|ASK");
+  dynstr_terminate(d);
+  return d->nvec ? d->vec + 1 : "";
+}
+#endif
+
 /** @brief Called when a drag moves within a candidate destination
  * @param w Destination widget
  * @param dc Drag context
@@ -524,8 +563,13 @@ static gboolean ql_drag_motion(GtkWidget *w,
     action = GDK_ACTION_MOVE;
   else if(dc->actions & GDK_ACTION_COPY)
     action = GDK_ACTION_COPY;
-  /*fprintf(stderr, "suggested %#x actions %#x result %#x\n",
-    dc->suggested_action, dc->actions, action);*/
+  /* TODO this comes up with the wrong answer sometimes.  If we are in the
+   * middle of a rearrange then the suggested action will be COPY, which we'll
+   * take, even though MOVE would actually be appropriate.  The drag still
+   * seems to work, but it _is_ wrong.  The answer is to take the target into
+   * account. */
+  /*fprintf(stderr, "suggested %s actions %s result %s\n",
+          act(dc->suggested_action), act(dc->actions), act(action));*/
   if(action) {
     // If the action is acceptable then we see if this widget is acceptable
     if(gtk_drag_dest_find_target(w, dc, NULL) == GDK_NONE)
@@ -608,12 +652,13 @@ static void ql_drag_data_get_collect(GtkTreeModel *model,
 static void ql_drag_data_get(GtkWidget attribute((unused)) *w,
                              GdkDragContext attribute((unused)) *dc,
                              GtkSelectionData *data,
-                             guint attribute((unused)) info_,
+                             guint attribute((unused)) info,
                              guint attribute((unused)) time_,
                              gpointer user_data) {
   struct queuelike *const ql = user_data;
   struct dynstr result[1];
 
+  //fprintf(stderr, "ql_drag_data_get %s info=%d\n", ql->name, info);
   dynstr_init(result);
   gtk_tree_selection_selected_foreach(ql->selection,
                                       ql_drag_data_get_collect,
@@ -646,7 +691,7 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
                                   gint x,
                                   gint y,
                                   GtkSelectionData *data,
-                                  guint attribute((unused)) info_,
+                                  guint info_,
                                   guint attribute((unused)) time_,
                                   gpointer user_data) {
   struct queuelike *const ql = user_data;
@@ -654,7 +699,7 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
   struct vector ids[1], tracks[1];
   int parity = 0;
 
-  //fprintf(stderr, "drag-data-received: %d,%d info_=%u\n", x, y, info_);
+  //fprintf(stderr, "drag-data-received: %d,%d info=%u\n", x, y, info_);
   /* Get the selection string */
   p = result = (char *)gtk_selection_data_get_text(data);
   if(!result) {
@@ -687,18 +732,21 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
   GtkTreePath *path = ql_drop_path(w, GTK_TREE_MODEL(ql->store), x, y, &pos);
   if(path) {
     q = ql_path_to_q(GTK_TREE_MODEL(ql->store), path);
+    //fprintf(stderr, "  drop path: %s q=%p pos=%d\n",
+    //        gtk_tree_path_to_string(path), q, pos);
   } else {
     /* This generally means a drop past the end of the queue.  We find the last
      * element in the queue and ask to move after that. */
     for(q = ql->q; q && q->next; q = q->next)
       ;
+    //fprintf(stderr, "  after end.  q=%p.  pos=%d\n", q, pos);
   }
   switch(pos) {
   case GTK_TREE_VIEW_DROP_BEFORE:
   case GTK_TREE_VIEW_DROP_INTO_OR_BEFORE:
     if(q) {
       q = q->prev;
-      //fprintf(stderr, "  ...but we like to drop near %s\n",
+      //fprintf(stderr, "  but we like to drop near %s\n",
       //        q ? q->id : "NULL");
     }
     break;
@@ -711,11 +759,12 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
   /* Note that q->id can match one of ids[].  This doesn't matter for
    * moveafter but TODO may matter for playlist support. */
   switch(info_) {
-  case 0:
-    /* Rearrangement.  Send ID and track data. */
+  case QUEUED_TRACKS_ID:
+  case PLAYLIST_TRACKS_ID:
+    /* Rearrangement within some widget.  Send ID and track data. */
     ql->drop(ql, tracks->nvec, tracks->vec, ids->vec, q);
     break;
-  case 1:
+  case PLAYABLE_TRACKS_ID:
     /* Copying between widgets.  IDs mean nothing so don't send them. */
     ql->drop(ql, tracks->nvec, tracks->vec, NULL, q);
     break;
@@ -724,6 +773,14 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
     gtk_tree_path_free(path);
 }
 
+static int count_drag_targets(const GtkTargetEntry *targets) {
+  const GtkTargetEntry *t = targets;
+
+  while(t->target)
+    ++t;
+  return t - targets;
+}
+
 /** @brief Initialize a @ref queuelike */
 GtkWidget *init_queuelike(struct queuelike *ql) {
   D(("init_queuelike"));
@@ -751,7 +808,7 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
       (ql->columns[n].name,
        r,
        "text", n,
-       "background", ql->ncolumns + BACKGROUND_COLUMN,
+       "cell-background", ql->ncolumns + BACKGROUND_COLUMN,
        "foreground", ql->ncolumns + FOREGROUND_COLUMN,
        (char *)0);
     gtk_tree_view_column_set_resizable(c, TRUE);
@@ -763,6 +820,7 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
 
   /* The selection should support multiple things being selected */
   ql->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ql->view));
+  g_object_ref(ql->selection);
   gtk_tree_selection_set_mode(ql->selection, GTK_SELECTION_MULTIPLE);
 
   /* Catch button presses */
@@ -791,15 +849,15 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
     /* This view will act as a drag source */
     gtk_drag_source_set(ql->view,
                         GDK_BUTTON1_MASK,
-                        queuelike_targets,
-                        sizeof queuelike_targets / sizeof *queuelike_targets,
-                        GDK_ACTION_MOVE);
+                        ql->drag_source_targets,
+                        count_drag_targets(ql->drag_source_targets),
+                        ql->drag_dest_actions);
     /* This view will act as a drag destination */
     gtk_drag_dest_set(ql->view,
                       GTK_DEST_DEFAULT_HIGHLIGHT|GTK_DEST_DEFAULT_DROP,
-                      queuelike_targets,
-                      sizeof queuelike_targets / sizeof *queuelike_targets,
-                      GDK_ACTION_MOVE|GDK_ACTION_COPY);
+                      ql->drag_dest_targets,
+                      count_drag_targets(ql->drag_dest_targets),
+                      ql->drag_dest_actions);
     g_signal_connect(ql->view, "drag-motion",
                      G_CALLBACK(ql_drag_motion), ql);
     g_signal_connect(ql->view, "drag-leave",
@@ -814,9 +872,9 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
     /* For queues that cannot accept a drop we still accept a copy out */
     gtk_drag_source_set(ql->view,
                         GDK_BUTTON1_MASK,
-                        queuelike_targets,
-                        sizeof queuelike_targets / sizeof *queuelike_targets,
-                        GDK_ACTION_COPY);
+                        ql->drag_source_targets,
+                        count_drag_targets(ql->drag_source_targets),
+                        ql->drag_source_actions);
     g_signal_connect(ql->view, "drag-data-get",
                      G_CALLBACK(ql_drag_data_get), ql);
     make_treeview_multidrag(ql->view, NULL);
@@ -824,7 +882,8 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
   
   /* TODO style? */
 
-  ql->init(ql);
+  if(ql->init)
+    ql->init(ql);
 
   /* Update display text when lookups complete */
   event_register("lookups-completed", queue_lookups_completed, ql);
@@ -834,6 +893,31 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
   return scrolled;
 }
 
+/** @brief Destroy a queuelike
+ * @param ql Queuelike to destroy
+ *
+ * Returns @p ql to its initial state.
+ */
+void destroy_queuelike(struct queuelike *ql) {
+  if(ql->store) {
+    g_object_unref(ql->store);
+    ql->store = NULL;
+  }
+  if(ql->view) {
+    gtk_object_destroy(GTK_OBJECT(ql->view));
+    ql->view = NULL;
+  }
+  if(ql->menu) {
+    gtk_object_destroy(GTK_OBJECT(ql->menu));
+    ql->menu = NULL;
+  }
+  if(ql->selection) {
+    g_object_unref(ql->selection);
+    ql->selection = NULL;
+  }
+  ql->q = NULL;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index c15b3834b6fb4facf9758d88ded2dd585f40f218..b774f226f838d4b5f2b726970d4b06d741ee8d25 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -104,10 +104,30 @@ struct queuelike {
   void (*drop)(struct queuelike *ql, int ntracks, char **tracks, char **ids,
                struct queue_entry *after_me);
 
-  /** @brief Stashed drag target row */
-  GtkTreePath *drag_target;
+  /** @brief Source target list */
+  const GtkTargetEntry *drag_source_targets;
+
+  /** @brief Drag source actions */
+  GdkDragAction drag_source_actions;
+  
+  /** @brief Destination target list */
+  const GtkTargetEntry *drag_dest_targets;
+
+  /** @brief Drag destination actions */
+  GdkDragAction drag_dest_actions;
+  
 };
 
+enum {
+  PLAYABLE_TRACKS_ID,
+  QUEUED_TRACKS_ID,
+  PLAYLIST_TRACKS_ID
+};
+
+#define PLAYABLE_TRACKS (char *)"text/x-disorder-playable-tracks"
+#define QUEUED_TRACKS (char *)"text/x-disorder-queued-tracks"
+#define PLAYLIST_TRACKS (char *)"text/x-disorder-playlist-tracks"
+
 enum {
   QUEUEPOINTER_COLUMN,
   FOREGROUND_COLUMN,
@@ -116,15 +136,8 @@ enum {
   EXTRA_COLUMNS
 };
 
-/* TODO probably need to set "horizontal-separator" to 0, but can't find any
- * coherent description of how to set style properties in isolation. */
-#define BG_PLAYING 0
-#define FG_PLAYING 0
-
-#ifndef BG_PLAYING
-# define BG_PLAYING "#e0ffe0"
-# define FG_PLAYING "black"
-#endif
+#define BG_PLAYING "#e0ffe0"
+#define FG_PLAYING "black"
 
 extern struct queuelike ql_queue;
 extern struct queuelike ql_recent;
@@ -157,6 +170,7 @@ gboolean ql_button_release(GtkWidget *widget,
                            GdkEventButton *event,
                            gpointer user_data);
 GtkWidget *init_queuelike(struct queuelike *ql);
+void destroy_queuelike(struct queuelike *ql);
 void ql_update_list_store(struct queuelike *ql) ;
 void ql_update_row(struct queue_entry *q,
                    GtkTreeIter *iter);
index beb40f5b550ec42e0fb1d2e03af332ebe96cbdfc..ebf85553e688539f994318dad9cb2e669c0504d9 100644 (file)
@@ -71,7 +71,7 @@ void ql_properties_activate(GtkMenuItem attribute((unused)) *menuitem,
     gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
   }
   if(v->nvec)
-    properties(v->nvec, (const char **)v->vec);
+    properties(v->nvec, (const char **)v->vec, ql->view);
 }
 
 /* Scratch */
index 80b163a8e16072da63288cbcc849a350a34ca3f2..c495bd85cc6471fb47ee58f0f8cbf6c3363d36a7 100644 (file)
@@ -72,7 +72,7 @@ static void queue_playing_changed(void) {
   ql_new_queue(&ql_queue, q);
   /* Tell anyone who cares */
   event_raise("queue-list-changed", q);
-  event_raise("playing-track-changed", q);
+  event_raise("playing-track-changed", playing_track);
 }
 
 /** @brief Update the queue itself */
@@ -135,6 +135,21 @@ static gboolean playing_periodic(gpointer attribute((unused)) data) {
   /* If there's a track playing, update its row */
   if(playing_track)
     ql_update_row(playing_track, 0);
+  /* If the first (nonplaying) track starts in the past, update the queue to
+   * get new expected start times; but rate limit this checking.  (If we only
+   * do it once a minute then the rest of the queue can get out of date too
+   * easily.) */
+  struct queue_entry *q = ql_queue.q;
+  if(q) {
+    if(q == playing_track)
+      q = q->next;
+    if(q) {
+      time_t now;
+      time(&now);
+      if(q->expected / 15 < now / 15)
+        queue_changed(0,0,0);
+    }
+  }
   return TRUE;
 }
 
@@ -142,6 +157,7 @@ static gboolean playing_periodic(gpointer attribute((unused)) data) {
 static void queue_init(struct queuelike attribute((unused)) *ql) {
   /* Arrange a callback whenever the playing state changes */ 
   event_register("playing-changed", playing_changed, 0);
+  event_register("playing-started", playing_changed, 0);
   /* We reget both playing track and queue at pause/resume so that start times
    * can be computed correctly */
   event_register("pause-changed", playing_changed, 0);
@@ -217,12 +233,28 @@ static const struct queue_column queue_columns[] = {
 
 /** @brief Pop-up menu for queue */
 static struct menuitem queue_menuitems[] = {
-  { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
-  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
-  { "Scratch playing track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
-  { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 },
-  { "Adopt track", ql_adopt_activate, ql_adopt_sensitive, 0, 0 },
+  { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+  { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+  { "Scratch playing track", GTK_STOCK_STOP, ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
+  { "Remove track from queue", GTK_STOCK_DELETE, ql_remove_activate, ql_remove_sensitive, 0, 0 },
+  { "Adopt track", NULL, ql_adopt_activate, ql_adopt_sensitive, 0, 0 },
+};
+
+static const GtkTargetEntry queue_targets[] = {
+  {
+    QUEUED_TRACKS,                      /* drag type */
+    GTK_TARGET_SAME_WIDGET,             /* rearrangement within a widget */
+    QUEUED_TRACKS_ID                    /* ID value */
+  },
+  {
+    PLAYABLE_TRACKS,                             /* drag type */
+    GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
+    PLAYABLE_TRACKS_ID,                          /* ID value */
+  },
+  {
+    .target = NULL
+  }
 };
 
 struct queuelike ql_queue = {
@@ -232,7 +264,11 @@ struct queuelike ql_queue = {
   .ncolumns = sizeof queue_columns / sizeof *queue_columns,
   .menuitems = queue_menuitems,
   .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems,
-  .drop = queue_drop
+  .drop = queue_drop,
+  .drag_source_targets = queue_targets,
+  .drag_source_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+  .drag_dest_targets = queue_targets,
+  .drag_dest_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
 };
 
 /** @brief Called when a key is pressed in the queue tree view */
@@ -274,6 +310,45 @@ int queued(const char *track) {
   return 0;
 }
 
+/* Playing widget for mini-mode */
+
+static void queue_set_playing_widget(const char attribute((unused)) *event,
+                                     void attribute((unused)) *eventdata,
+                                     void *callbackdata) {
+  GtkLabel *w = callbackdata;
+
+  if(playing_track) {
+    const char *artist = namepart(playing_track->track, "display", "artist");
+    const char *album = namepart(playing_track->track, "display", "album");
+    const char *title = namepart(playing_track->track, "display", "title");
+    const char *ldata = column_length(playing_track, NULL);
+    if(!ldata)
+      ldata = "";
+    char *text;
+    byte_xasprintf(&text, "%s/%s/%s %s", artist, album, title, ldata);
+    gtk_label_set_text(w, text);
+  } else
+    gtk_label_set_text(w, "");
+}
+
+GtkWidget *playing_widget(void) {
+  GtkWidget *w = gtk_label_new("");
+  gtk_misc_set_alignment(GTK_MISC(w), 1.0, 0);
+  /* Spot changes to the playing track */
+  event_register("playing-track-changed",
+                 queue_set_playing_widget,
+                 w);
+  /* Use the best-known name for it */
+  event_register("lookups-complete",
+                 queue_set_playing_widget,
+                 w);
+  /* Keep the amount played so far up to date */
+  event_register("periodic-fast",
+                 queue_set_playing_widget,
+                 w);
+  return frame_widget(w, NULL);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index f53e6313f654a7535e52104c3b9ac9da4e77cdb8..510aac901c964519133f966816db8b80ed304843 100644 (file)
@@ -78,10 +78,10 @@ static const struct queue_column recent_columns[] = {
 
 /** @brief Pop-up menu for recently played list */
 static struct menuitem recent_menuitems[] = {
-  { "Track properties", ql_properties_activate, ql_properties_sensitive,0, 0 },
-  { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
-  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+  { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive,0, 0 },
+  { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+  { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
 };
 
 struct queuelike ql_recent = {
@@ -91,6 +91,8 @@ struct queuelike ql_recent = {
   .ncolumns = sizeof recent_columns / sizeof *recent_columns,
   .menuitems = recent_menuitems,
   .nmenuitems = sizeof recent_menuitems / sizeof *recent_menuitems,
+  .drag_source_targets = choose_targets,
+  .drag_source_actions = GDK_ACTION_COPY,
 };
 
 GtkWidget *recent_widget(void) {
index cbedb18aaf8eb4f07b0675f64a6f22df31cf39b9..46931736b42665a205229aafd0289da15d7ef26f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of Disobedience
- * Copyright (C) 2007 Richard Kettlewell
+ * Copyright (C) 2007-2010 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -108,7 +108,7 @@ void start_rtp(void) {
   if(!(pid = xfork())) {
     if(setsid() < 0)
       disorder_fatal(errno, "error calling setsid");
-    if(!(pid = xfork())) {
+    if(!xfork()) {
       /* grandchild */
       exitfn = _exit;
       /* log errors and output somewhere reasonably sane.  rtp_running()
index 1d2a169c3910c7134a85df084b128cb81e5e1591..b07869842e24249ed8b6610c16948891b9189529 100644 (file)
@@ -660,13 +660,15 @@ static struct button users_buttons[] = {
     GTK_STOCK_ADD,
     users_add,
     "Create a new user",
-    0
+    0,
+    NULL,
   },
   {
     GTK_STOCK_REMOVE,
     users_delete,
     "Delete a user",
-    0
+    0,
+    NULL,
   },
 };
 #define NUSERS_BUTTONS (sizeof users_buttons / sizeof *users_buttons)
index eaad306eb16f9c015d2daa52ce4df87c68f1a9a5..b3d892604f46a1ccdf9c9854e25e615ebaf0f1a7 100644 (file)
@@ -35,19 +35,31 @@ SEDFILES=disorder.1 disorderd.8 disorder_config.5 \
 
 include ${top_srcdir}/scripts/sedfiles.make
 
-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 $@
+HTMLMAN=disorderd.8.html disorder.1.html disorder.3.html               \
+disorder_config.5.html disorder-dump.8.html disorder_protocol.5.html   \
+disorder-deadlock.8.html disorder-rescan.8.html disobedience.1.html    \
+disorderfm.1.html disorder-speaker.8.html disorder-playrtp.1.html      \
+disorder-normalize.8.html disorder-decode.8.html disorder-stats.8.html \
+disorder-dbupgrade.8.html disorder_templates.5.html                    \
+disorder_actions.5.html disorder_options.5.html disorder.cgi.8.html    \
+disorder_preferences.5.html disorder-choose.8.html
 
-TMPLMAN=$(foreach man,$(man_MANS),$(man).tmpl)
-$(TMPLMAN) : %.tmpl : % $(top_srcdir)/scripts/htmlman
-       rm -f $@.new
-       $(top_srcdir)/scripts/htmlman -stdhead $< >$@.new
-       chmod 444 $@.new
-       mv -f $@.new $@
+$(wordlist 2,9999,$(HTMLMAN)): disorderd.8.html
+disorderd.8.html: $(man_MANS)
+       $(top_srcdir)/scripts/htmlman -- $^
+
+TMPLMAN=disorderd.8.tmpl disorder.1.tmpl disorder.3.tmpl               \
+disorder_config.5.tmpl disorder-dump.8.tmpl disorder_protocol.5.tmpl   \
+disorder-deadlock.8.tmpl disorder-rescan.8.tmpl disobedience.1.tmpl    \
+disorderfm.1.tmpl disorder-speaker.8.tmpl disorder-playrtp.1.tmpl      \
+disorder-normalize.8.tmpl disorder-decode.8.tmpl disorder-stats.8.tmpl \
+disorder-dbupgrade.8.tmpl disorder_templates.5.tmpl                    \
+disorder_actions.5.tmpl disorder_options.5.tmpl disorder.cgi.8.tmpl    \
+disorder_preferences.5.tmpl disorder-choose.8.tmpl
+
+$(wordlist 2,9999,$(TMPLMAN)): disorderd.8.tmpl
+disorderd.8.tmpl: $(man_MANS)
+       $(top_srcdir)/scripts/htmlman -stdhead -extension tmpl -- $^
 
 disorder_templates.5.in: disorder_templates.5.head disorder_templates.5.tail \
                $(top_srcdir)/lib/macros-builtin.c \
index f7fb5c2386f9ac38993623500ad1d6827f6dc551..4462237d8eda3efdaca58dfa0ac3f8ce98f175bc 100644 (file)
@@ -1,5 +1,5 @@
 .\"
-.\" Copyright (C) 2004-2008 Richard Kettlewell
+.\" Copyright (C) 2004-2009 Richard Kettlewell
 .\"
 .\" This program is free software: you can redistribute it and/or modify
 .\" it under the terms of the GNU General Public License as published by
@@ -23,337 +23,9 @@ disobedience \- GUI client for DisOrder jukebox
 .SH DESCRIPTION
 .B disobedience
 is a graphical client for DisOrder.
-.SH "WINDOWS AND ICONS"
-.SS "Server Menu"
-This has the following options:
-.TP
-.B Login
-Brings up the \fBLogin Details Window\fR; see below.
-.TP
-.B "Manage Users"
-Brings up the \fBUser Management Window\fR; see below.
-.TP
-.B Quit
-Terminates the program.
-.SS "Edit Menu"
-This has the following options:
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.SS "Control Menu"
-This has the following options:
-.TP
-.B Scratch
-Interrupts the currently playing track.
-.TP
-.B Playing
-Pause and resume the current track.
-.TP
-.B "Random play"
-Enable and disable random play.
-Does not take effect until the currently playing track finishes.
-.TP
-.B "Network player"
-Enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-.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.
-This button shows either a pause symbol (two vertical bars) or a resume symbol
-(a right-pointing arrow).
-.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.
-When the button is green, random play is enabled.
-When it is grey, random play is disabled.
-.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.
-When the button is green, play is enabled.
-When it is grey, play is disabled.
-.TP
-.B "Network play button"
-The network play buttons enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-When the button is green, network play is enabled.
-When it is grey, network play is disabled.
-.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 can be distinguished by
-the constantly updating timer.
-Queued tracks appear below it.
-.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
-Tracks can be moved within the queue by dragging them to a new position with
-the left button.
-.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 Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect 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 track from queue"
-Remove the selected tracks from the queue.
-.TP
-.B "Adopt track"
-Sets the submitter of a randomly picked track to you.
-.SS "Recent Tab"
-This displays recently played tracks, the most recent at the top.
-.PP
-The left button functions as above, except that drag-and-drop rearrangement
-is not possible.
-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 "Play track"
-Play the select track(s);
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect 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
-Files may be played by dragging them to the queue tab and thence to a
-destination position in the queue.
-.PP
-The text box at the bottom is a search form.
-If you enter search terms here then tracks containing all those words will be
-highlighted.
-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.
-The up and down arrows will scroll the window to make the previous or next
-search result visible.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B Play
-Play selected tracks.
-.TP
-.B Properties
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.PP
-A middle click on a track will add it to the queue.
 .PP
-Right clicking over a directory will pop up a menu with the following options:
-.TP
-.B "Play all tracks"
-Play all the tracks in the directory, in the order they appear on screen.
-.TP
-.B "Track properties"
-Edit properties of all tracks in the directory.
-.TP
-.B "Select children"
-Select all the tracks in the directory (and deselect everything else).
-.TP
-.B "Deselect all tracks"
-Deselect everything.
-.SS "Added Tab"
-This displays a list of tracks recently added to the server's database.
-The most recently added track is at the top.
-.PP
-Left clicking a track will select it.
-CTRL and SHIFT work as above to select muliple files.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B "Track properties"
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Play track"
-Play selected tracks.
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.SS "Login Details Window"
-The login details window allows you to edit the connection details and
-authorization information used by Disobedience.
-.PP
-At the top is a 'remote' switch.
-If this is enabled then you can use the \fBHostname\fR and \fBService\fR
-fields to connect to a remote server.
-If it is disabled then then Disobedience will connect to a local server
-instead.
-.PP
-Below this are four text entry fields:
-.TP
-.B Hostname
-The host to connect to.
-.TP
-.B Service
-The service name or port number to connect to.
-.TP
-.B "User name"
-The user name to log in as.
-.TP
-.B Password
-The password to use when logging in.
-Note that this is NOT your login password but is your password to the
-DisOrder server.
-.PP
-It has two buttons:
-.TP
-.B Login
-This button attempts to (re-)connect to the server with the currently displayed
-settings.
-The settings are saved in
-.IR $HOME/.disorder/passwd .
-on success.
-.TP
-.B Close
-This button closes the window, discarding any unsaved changes.
-.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 Weight field determines the track weight.  Tracks with higher weights are
-proportionately more likely to be picked at random.  The default weight is
-90000, and the maximum weight is 2147483647.
-.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
-The double-headed arrow to the right of each preference will propagate its
-value to all the other tracks in the window.
-For instance, this can be used to efficiently correct the artist or album
-fields, or bulk-disable random play for many tracks.
-.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.
-.SS "User Management Window"
-This window is primarily of interest to adminstrators, and will not be
-available to users without admin rights.  The left hand side is a list of all
-users; the right hand side contains the editable details of the currently
-selected user.
-.PP
-When you select any user you can edit their email address or change their
-password.  It is also possible to edit the individual user rights.  Click on
-the "Apply" button to commit any changes you make.
-.PP
-The "Add" button creates a new user.  You must enter at least a username.
-Default rights are setting according to local configuration, \fInot\fR server
-configuration (but this may be changed in the future).  As above, click on
-"Apply" to actually create the new user.
-.PP
-The "Delete" button deletes the selected user.  This operation cannot be
-undone.
-.SH "KEYBOARD SHORTCUTS"
-.TP
-.B CTRL+A
-Select all tracks (queue/recent)
-.TP
-.B CTRL+L
-Brings up the \fBLogin Details Window\fR.
-.TP
-.B CTRL+Q
-Quit.
-.SH "NETWORK PLAY"
-Network play uses a background
-.BR disorder\-playrtp (1)
-process.
-If you quit Disobedience the player will continue playing and can be
-disabled from a later run of Disobedience.
-.PP
-The player will log to
-.I ~/.disorder/HOSTNAME\-rtp.log
-so look there if it does not seem to be working.
-.PP
-You can stop it without running Disobedience by the command
-.BR "killall disorder\-playrtp" .
+Please refer to Disobedience's HTML manual for further information.  This can
+be found at dochtmldir/index.html.
 .SH OPTIONS
 .TP
 .B \-\-config \fIPATH\fR, \fB\-c \fIPATH
@@ -383,41 +55,6 @@ The screen number to use.
 .\" .TP
 .\" .B \-\-sync
 .\" Make all X requests synchronously.
-.SH CONFIGURATION
-If you are using
-.B disobedience
-on the same host as the server then no additional configuration should be
-required.
-.PP
-If it is running on a different host then the easiest way to set it up is to
-use the login details window in Disobedience.
-Enter the connection details, use Login to connect to the server, and then
-use Save to store them for future sessions.
-.PP
-The other clients read their configuration from the same location so after
-setting up with Disobedience, tools such as
-.BR disorder (1)
-should work as well.
-.SH BUGS
-There is no particular provision for multiple users of the same computer
-sharing a single \fBdisorder\-playrtp\fR process.
-This shouldn't be too much of a problem in practice but something could
-perhaps be done given demand.
-.PP
-Try to do remote user management when the server is configured to refuse this
-produces rather horrible error behavior.
-.PP
-Only one track can be dragged at a time.
-.PP
-Resizing columns doesn't work very well.
-This is a GTK+ bug.
-.SH FILES
-.TP
-.I ~/.disorder/HOSTNAME\-rtp
-Socket for communication with RTP player.
-.TP
-.I ~/.disorder/HOSTNAME\-rtp.log
-Log file for RTP player.
 .SH "SEE ALSO"
 .BR disorder\-playrtp (1),
 .BR disorder_config (5)
index 2b71bf90ad4ed15daa60e7e500be2e88559d3382..cbd227802f00cb658ca3452c24d3e054fc059663 100644 (file)
@@ -318,8 +318,7 @@ 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.
+A player that writes raw samples to \fB$DISORDER_RAW_FD\fR.
 .RE
 .IP
 Known capabilities are:
index ef3a70faee4cf0ed0bb8b3b60e8709a1b22fb441..0c8f7c8f490025a5693b64724cc50a27906da047 100644 (file)
@@ -325,7 +325,6 @@ For \fBapi oss\fR the possible values are:
 .RS
 .TP 8
 .B pcm
-
 Output level for the audio device.
 This is probably what you want and is the default.
 .TP
@@ -457,6 +456,10 @@ The default is 0.
 .IP
 For \fBapi coreaudio\fR, volume setting is not currently supported.
 .TP
+.B mount_rescan yes\fR|\fBno
+Determines whether mounts and unmounts will cause an automatic rescan.
+The default is \fByes\fR.
+.TP
 .B multicast_loop yes\fR|\fBno
 Determines whether multicast packets are loop backed to the sending host.
 The default is \fByes\fR.
@@ -609,9 +612,6 @@ Identical to the \fBexec\fR except that the player is expected to use the
 DisOrder raw player protocol.
 .BR disorder-decode (8)
 can decode several common audio file formats to this format.
-If your favourite format is not supported, but you have a player
-which uses libao, there is also a libao driver which supports this format;
-see below for more information about this.
 .TP
 .B shell \fR[\fISHELL\fR] \fICOMMAND\fR
 The command is executed using the shell.
@@ -808,8 +808,14 @@ This must be set if you have online registration enabled.
 .TP
 .B refresh \fISECONDS\fR
 Specifies the maximum refresh period in seconds.
+The refresh period is the time after which the web interface's queue and manage
+pages will automatically reload themselves.
 Default 15.
 .TP
+.B refresh_min \fISECONDS\fR
+Specifies the minimum refresh period in seconds.
+Default 1.
+.TP
 .B sendmail \fIPATH\fR
 The path to the Sendmail executable.
 This must support the \fB-bs\fR option (Postfix, Exim and Sendmail should all
@@ -864,25 +870,6 @@ longer needs to be specified.
 .IP
 This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not
 \fB/cgi-bin/jukebox\fR.
-.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 "REGEXP SUBSTITUTION RULES"
 Regexps are PCRE regexps, as defined in \fBpcrepattern\fR(3).
 The only option used is \fBPCRE_UTF8\fR.
index 6bc9c475461db24f626572f6bcc0931121c68317..bb95ff01e584ebaa68b842e23e1f26769ae28263 100644 (file)
@@ -122,7 +122,7 @@ List 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
-Getsa preference value.
+Gets a preference value.
 On success the second field of the response line will have the value.
 .IP
 If the track or preference do not exist then the response code is 555.
@@ -171,7 +171,7 @@ depending on how the tracks came to be added to the queue.
 .TP
 .B new \fR[\fIMAX\fR]
 Send the most recently added \fIMAX\fR tracks in a response body.
-If the argument is ommitted, the \fBnew_max\fR most recent tracks are
+If the argument is omitted, the \fBnew_max\fR most recent tracks are
 listed (see \fBdisorder_config\fR(5)).
 .TP
 .B nop
@@ -232,6 +232,7 @@ Requires permission to modify that playlist and the \fBplay\fR right.
 .B playlist-get \fIPLAYLIST\fR
 Get the contents of a playlist, in a response body.
 Requires permission to read that playlist and the \fBread\fR right.
+If the playlist does not exist the response is 555.
 .TP
 .B playlist-get-share \fIPLAYLIST\fR
 Get the sharing status of a playlist.
@@ -475,7 +476,7 @@ With two parameters sets each side independently.
 Setting the volume requires the \fBvolume\fR right.
 .SH RESPONSES
 Responses are three-digit codes.
-The first digit distinguishes errors from succesful responses:
+The first digit distinguishes errors from successful responses:
 .TP
 .B 2
 Operation succeeded.
diff --git a/driver/Makefile.am b/driver/Makefile.am
deleted file mode 100644 (file)
index df8fe1f..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-#
-# This file is part of DisOrder
-# Copyright (C) 2005, 2007, 2008 Richard Kettlewell
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-
-aolib_LTLIBRARIES=libdisorder.la
-aolibdir=${libdir}/ao/plugins-2
-finkaolibdir=${finkdir}/lib/ao/plugins-2
-usraolibdir=/usr/lib/ao/plugins-2
-AM_CPPFLAGS=-I${top_srcdir}/lib
-
-libdisorder_la_SOURCES=disorder.c
-libdisorder_la_LDFLAGS=-module
-
-install-data-hook:
-
-# Link ao driver into right location.  If you have some other location then
-# you'll need to modify this or link it manually.
-#
-# We don't mess with this for now; since disorder-decode covers some common
-# cases, the libao driver is less useful than it was.
-link-ao-driver:
-       @if test -d ${DESTDIR}${finkaolibdir} \
-          && test ${finkaolibdir} != ${aolibdir}; then \
-         echo rm -f ${DESTDIR}${finkaolibdir}/libdisorder.*; \
-         rm -f ${DESTDIR}${finkaolibdir}/libdisorder.*; \
-         echo ln ${aolibdir}/libdisorder.* ${DESTDIR}${finkaolibdir}; \
-         ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${finkaolibdir}; \
-       fi
-       @if test -d ${DESTDIR}${usraolibdir} \
-          && test ${usraolibdir} != ${aolibdir}; then \
-         echo rm -f ${DESTDIR}${usraolibdir}/libdisorder.*; \
-         rm -f ${DESTDIR}${usraolibdir}/libdisorder.*; \
-         echo ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${usraolibdir}; \
-         ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${usraolibdir}; \
-       fi
diff --git a/driver/disorder.c b/driver/disorder.c
deleted file mode 100644 (file)
index e6b6575..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * This file is part of DisOrder.
- * Copyright (C) 2005, 2007 Richard Kettlewell
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-/** @file driver/disorder.c
- * @brief libao driver used by DisOrder
- *
- * The output from this driver is expected to be fed to @c
- * disorder-normalize to convert to the confnigured target format.
- */
-
-#include "common.h"
-
-#include <errno.h>
-#include <unistd.h>
-#include <poll.h>
-#include <ao/ao.h>
-#include <ao/plugin.h>
-
-#include "speaker-protocol.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);
-
-/** @brief Private data structure for this driver */
-struct internal {
-  int fd;                              /* output file descriptor */
-  int exit_on_error;                   /* exit on write error */
-
-  /** @brief Record of sample format */
-  struct stream_header header;
-
-};
-
-/* 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;
-  i->header.rate = format->rate;
-  i->header.channels = format->channels;
-  i->header.bits = format->bits;
-  i->header.endian = ENDIAN_NATIVE;
-  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;
-
-  /* Fill in and write the header */
-  i->header.nbytes = num_bytes;
-  if(do_write(i->fd, &i->header, sizeof i->header) < 0) {
-    if(i->exit_on_error) _exit(-1);
-    return 0;
-  }
-
-  /* Write the sample data */
-  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:
-*/
index ffe393c3f80b388d48f2d6a57828b779f749c6bb..36a6ad5cd6047f5756ddb4758ed7f4be2dc44f75 100644 (file)
@@ -1,6 +1,6 @@
 #
 # This file is part of DisOrder.
-# Copyright (C) 2005-2008 Richard Kettlewell
+# Copyright (C) 2005-2010 Richard Kettlewell
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -27,6 +27,29 @@ disobedience32x32.xpm cross.svg go.svg notes.svg noteson.svg pause.svg       \
 query.svg queryon.svg speaker.svg speakeron.svg cross32.png            \
 pause32.png play32.png playdisabled32.png playenabled32.png            \
 randomdisabled32.png randomenabled32.png rtpdisabled32.png             \
-rtpenabled32.png duck55.png
+rtpenabled32.png duck55.png cards24.png cards48.png                    \
+cards-simple-fanned.svg cards-thin.svg
 
-CLEANFILES=$(SEDFILES)
+DISOBEDIENCE_IMAGES=up.png down.png cards24.png logo256.png duck.png   \
+propagate.png
+
+if GTK
+noinst_HEADERS=images.h
+
+images.h: $(DISOBEDIENCE_IMAGES)
+       set -e;                                                         \
+       exec > @$.new;                                                  \
+       for png in $^; do                                               \
+         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
+         gdk-pixbuf-csource --raw --name=image_$$name $$png;           \
+       done;                                                           \
+       echo "static const struct image images[] = {";                  \
+       for png in `echo $^`; do                                        \
+         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
+         echo "  { \"$$name.png\", image_$$name },";                   \
+       done|sort;                                                      \
+       echo "};"
+       mv @$.new $@
+endif
+
+CLEANFILES=$(SEDFILES) images.h
diff --git a/images/cards-simple-fanned.svg b/images/cards-simple-fanned.svg
new file mode 100644 (file)
index 0000000..9ae9730
--- /dev/null
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   version="1.0"
+   sodipodi:docname="cards-simple-fanned.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   inkscape:export-filename="/home/richard/cards/cards-simple-fanned-24.png"
+   inkscape:export-xdpi="45"
+   inkscape:export-ydpi="45">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="17.25"
+     inkscape:cx="24"
+     inkscape:cy="24"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:snap-intersection-line-segments="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-nodes="false"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:window-width="1301"
+     inkscape:window-height="1034"
+     inkscape:window-x="122"
+     inkscape:window-y="20">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2383"
+       visible="true"
+       enabled="true"
+       empspacing="8" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3227">
+      <rect
+         transform="matrix(0.9659258,-0.2588191,0.2588191,0.9659258,0,0)"
+         y="8.3028555"
+         x="1.4500779"
+         height="34.000004"
+         width="24.000002"
+         id="rect3292"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.00000024;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         transform="matrix(0.9659258,-0.2588191,0.2588191,0.9659258,0,0)"
+         y="9.3028555"
+         x="2.450078"
+         height="32.000004"
+         width="22.000002"
+         id="rect3294"
+         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.00000023999999987;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         transform="matrix(0.9659258,-0.2588191,0.2588191,0.9659258,0,0)"
+         y="13.302856"
+         x="6.4500785"
+         height="24.000002"
+         width="14.000001"
+         id="rect3296"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ff0000;stroke-width:2.00000023999999987;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <g
+       id="g3222">
+      <rect
+         y="6.5112176"
+         x="12.457951"
+         height="34"
+         width="24"
+         id="rect3267"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="7.5112176"
+         x="13.457951"
+         height="32"
+         width="22"
+         id="rect2387"
+         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="11.511218"
+         x="17.457951"
+         height="24"
+         width="14"
+         id="rect3159"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ff0000;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <g
+       id="g3217">
+      <rect
+         transform="matrix(0.9659258,0.2588191,-0.2588191,0.9659258,0,0)"
+         y="1.7532461"
+         x="22.488214"
+         height="34.000004"
+         width="24.000002"
+         id="rect3276"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.00000024;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         transform="matrix(0.9659258,0.2588191,-0.2588191,0.9659258,0,0)"
+         y="2.7532461"
+         x="23.488214"
+         height="32.000004"
+         width="22.000002"
+         id="rect3278"
+         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.00000023999999987;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         transform="matrix(0.9659258,0.2588191,-0.2588191,0.9659258,0,0)"
+         y="6.7532463"
+         x="27.488214"
+         height="24.000002"
+         width="14.000001"
+         id="rect3280"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ff0000;stroke-width:2.00000023999999987;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
diff --git a/images/cards-thin.svg b/images/cards-thin.svg
new file mode 100644 (file)
index 0000000..94e1255
--- /dev/null
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   version="1.0"
+   sodipodi:docname="cards-thin.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   inkscape:export-filename="/home/richard/cards48.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="17.1875"
+     inkscape:cx="24"
+     inkscape:cy="24"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:snap-intersection-line-segments="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-nodes="false"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:window-width="1639"
+     inkscape:window-height="1031"
+     inkscape:window-x="122"
+     inkscape:window-y="20">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2383"
+       visible="true"
+       enabled="true"
+       empspacing="8" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3290"
+       transform="matrix(0.9848078,-0.1736482,0.1736482,0.9848078,3.6830881,3.8942155)">
+      <rect
+         y="2"
+         x="2"
+         height="34"
+         width="24"
+         id="rect3292"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="3"
+         x="3"
+         height="32"
+         width="22"
+         id="rect3294"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="7"
+         x="7"
+         height="24"
+         width="14"
+         id="rect3296"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <g
+       id="g3269"
+       transform="translate(10,3)">
+      <rect
+         y="2"
+         x="2"
+         height="34"
+         width="24"
+         id="rect3267"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="3"
+         x="3"
+         height="32"
+         width="22"
+         id="rect2387"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="7"
+         x="7"
+         height="24"
+         width="14"
+         id="rect3159"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+    <g
+       id="g3274"
+       transform="matrix(0.9848078,0.1736482,-0.1736482,0.9848078,16.28172,3.0320659)">
+      <rect
+         y="2"
+         x="2"
+         height="34"
+         width="24"
+         id="rect3276"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="3"
+         x="3"
+         height="32"
+         width="22"
+         id="rect3278"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         y="7"
+         x="7"
+         height="24"
+         width="14"
+         id="rect3280"
+         style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
diff --git a/images/cards24.png b/images/cards24.png
new file mode 100644 (file)
index 0000000..24e90fb
Binary files /dev/null and b/images/cards24.png differ
diff --git a/images/cards48.png b/images/cards48.png
new file mode 100644 (file)
index 0000000..e929b75
Binary files /dev/null and b/images/cards48.png differ
index ec674f6fcdd0cc3b752bd2e4ee5e2f12be1e2bfb..eea622032829866a1af95a9ed135fd8d5d140d74 100644 (file)
@@ -52,6 +52,7 @@ libdisorder_a_SOURCES=charset.c charsetf.c charset.h  \
        heap.h                                          \
        hex.c hex.h                                     \
        hostname.c hostname.h                           \
+       hreader.c hreader.h                             \
        ifreq.c ifreq.h                                 \
        inputline.c inputline.h                         \
        kvp.c kvp.h                                     \
@@ -91,6 +92,7 @@ libdisorder_a_SOURCES=charset.c charsetf.c charset.h  \
        unicode.h unicode.c                             \
        unidata.h unidata.c                             \
        vacopy.h                                        \
+       validity.c validity.h                           \
        vector.c vector.h                               \
        version.c version.h                             \
        wav.h wav.c                                     \
@@ -121,6 +123,7 @@ definitions.h: Makefile
        echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new
        echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new
        echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new
+       echo "#define DOCHTMLDIR \"${dochtmldir}\"" >> $@.new
        echo "#define SBINDIR \"${sbindir}/\"" >> $@.new
        echo "#define BINDIR \"${bindir}/\"" >> $@.new
        echo "#define FINKBINDIR \"${finkbindir}/\"" >> $@.new
@@ -134,10 +137,6 @@ defs.lo: definitions.h versionstring.h
 rebuild-unicode:
        cd ${srcdir} && ${top_srcdir}/scripts/make-unidata
 
-%.i: %.c
-       $(CPP) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) -c $< > $@.new
-       mv $@.new $@
-
 CLEANFILES=definitions.h definitions.h.new version-string versionstring.h \
           *.gcda *.gcov *.gcno *.c.html index.html
 
index 15b556c483cbc51915c179ee1b72e6e23ccafc4c..9fd42f1d41a929625be9dde37817f55285fbcf0f 100644 (file)
--- a/lib/cgi.c
+++ b/lib/cgi.c
@@ -226,7 +226,6 @@ void cgi_clear(void) {
  */
 char *cgi_sgmlquote(const char *src) {
   uint32_t *ucs, c;
-  int n;
   struct dynstr d[1];
   struct sink *s;
 
@@ -234,7 +233,6 @@ char *cgi_sgmlquote(const char *src) {
     exit(1);
   dynstr_init(d);
   s = sink_dynstr(d);
-  n = 1;
   /* format the string */
   while((c = *ucs++)) {
     switch(c) {
index 50f718466922f83707fd90568b5d3855fd43ec4a..2cbcfa76201324f07dc0668975b5dd5c51a98291 100644 (file)
@@ -519,7 +519,7 @@ int disorder_close(disorder_client *c) {
   c->ident = 0;
   xfree(c->user);
   c->user = 0;
-  return 0;
+  return ret;
 }
 
 /** @brief Play a track
index c059bf7718400da36c086dbfeedd7af0a067dc34..53fddd5881318e4937a798e612abfc79da0771be 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004-2009 Richard Kettlewell
+ * Copyright (C) 2004-2010 Richard Kettlewell
  * Portions copyright (C) 2007 Mark Wooding
  *
  * This program is free software: you can redistribute it and/or modify
@@ -52,7 +52,7 @@
 
 /** @brief Path to config file 
  *
- * set_configfile() sets the deafult if it is null.
+ * set_configfile() sets the default if it is null.
  */
 char *configfile;
 
@@ -835,9 +835,7 @@ static int validate_positive(const struct config_state *cs,
 static int validate_isauser(const struct config_state *cs,
                            int attribute((unused)) nvec,
                            char **vec) {
-  struct passwd *pw;
-
-  if(!(pw = getpwnam(vec[0]))) {
+  if(!getpwnam(vec[0])) {
     disorder_error(0, "%s:%d: no such user as '%s'", cs->path, cs->line, vec[0]);
     return -1;
   }
@@ -1069,8 +1067,8 @@ static const struct conf conf[] = {
   { C(checkpoint_min),   &type_integer,          validate_non_negative },
   { C(collection),       &type_collections,      validate_any },
   { C(connect),          &type_netaddress,       validate_destaddr },
-  { C(cookie_login_lifetime),  &type_integer,    validate_positive },
   { C(cookie_key_lifetime),  &type_integer,      validate_positive },
+  { C(cookie_login_lifetime),  &type_integer,    validate_positive },
   { C(dbversion),        &type_integer,          validate_positive },
   { C(default_rights),   &type_rights,           validate_any },
   { C(device),           &type_string,           validate_any },
@@ -1081,6 +1079,7 @@ static const struct conf conf[] = {
   { C(lock),             &type_boolean,          validate_any },
   { C(mail_sender),      &type_string,           validate_any },
   { C(mixer),            &type_string,           validate_any },
+  { C(mount_rescan),     &type_boolean,          validate_any },
   { C(multicast_loop),   &type_boolean,          validate_any },
   { C(multicast_ttl),    &type_integer,          validate_non_negative },
   { C(namepart),         &type_namepart,         validate_any },
@@ -1100,10 +1099,11 @@ static const struct conf conf[] = {
   { C(plugins),          &type_string_accum,     validate_isdir },
   { C(prefsync),         &type_integer,          validate_positive },
   { C(queue_pad),        &type_integer,          validate_positive },
-  { C(replay_min),       &type_integer,          validate_non_negative },
   { C(refresh),          &type_integer,          validate_positive },
+  { C(refresh_min),      &type_integer,          validate_non_negative },
   { C(reminder_interval), &type_integer,         validate_positive },
   { C(remote_userman),   &type_boolean,          validate_any },
+  { C(replay_min),       &type_integer,          validate_non_negative },
   { C2(restrict, restrictions),         &type_restrict,         validate_any },
   { C(rtp_delay_threshold), &type_integer,       validate_positive },
   { C(sample_format),    &type_sample_format,    validate_sample_format },
@@ -1340,6 +1340,7 @@ static struct config *config_default(void) {
   logname = pw->pw_name;
   c->username = xstrdup(logname);
   c->refresh = 15;
+  c->refresh_min = 1;
   c->prefsync = 0;
   c->signal = SIGKILL;
   c->alias = xstrdup("{/artist}{/album}{/title}{ext}");
@@ -1374,6 +1375,7 @@ static struct config *config_default(void) {
   c->sox_generation = DEFAULT_SOX_GENERATION;
   c->playlist_max = INT_MAX;            /* effectively no limit */
   c->playlist_lock_timeout = 10;        /* 10s */
+  c->mount_rescan = 1;
   /* Default stopwords */
   if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
     exit(1);
@@ -1732,6 +1734,19 @@ static int namepartlist_compare(const struct namepartlist *a,
     return 0;
 }
 
+/** @brief Verify configuration table.
+ * @return The number of problems found
+*/
+int config_verify(void) {
+  int fails = 0;
+  for(size_t n = 1; n < sizeof conf / sizeof *conf; ++n)
+    if(strcmp(conf[n-1].name, conf[n].name) >= 0) {
+      fprintf(stderr, "%s >= %s\n", conf[n-1].name, conf[n].name);
+      ++fails;
+    }
+  return fails;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index f1794aa1c5b92dd03d316932ea544dc5c7982cc5..8255db2e6e7268c4c411bf71ab40847abd199118 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004-2009 Richard Kettlewell
+ * Copyright (C) 2004-2010 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -215,6 +215,9 @@ struct config {
   /** @brief Maximum refresh interval for web interface (seconds) */
   long refresh;
 
+  /** @brief Minimum refresh interval for web interface (seconds) */
+  long refresh_min;
+
   /** @brief Facilities restricted to trusted users
    *
    * A bitmap of @ref RESTRICT_SCRATCH, @ref RESTRICT_REMOVE and @ref
@@ -287,7 +290,10 @@ struct config {
 
   /** @brief Maximum bias */
   long new_bias;
-  
+
+  /** @brief Rescan on (un)mount */
+  int mount_rescan;
+
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
   char **parts;                                /* name part list  */
@@ -320,6 +326,8 @@ char *config_usersysconf(const struct passwd *pw );
 char *config_private(void);
 /* get the private config file */
 
+int config_verify(void);
+
 void config_free(struct config *c);
 
 extern char *configfile;
index 633d56adb719b0785bfa01ab100e09c8cfd8fa3a..f0e35b2cd74af9a4ae6dfa4f0f93608a7c833d5f 100644 (file)
@@ -41,6 +41,9 @@ const char pkgstatedir[] = PKGSTATEDIR;
 /** @brief Package fixed data directory */
 const char pkgdatadir[] = PKGDATADIR;
 
+/** @brief Package HTML documentation directory */
+const char dochtmldir[] = DOCHTMLDIR;
+
 /** @brief Binary directory */
 const char bindir[] = BINDIR;
 
index 41b65ea584254f7b5a3fbce6ef0b1415a25c7ea0..e13b85718e37aa1eb2305149d52bce7d8f14e4ac 100644 (file)
@@ -25,6 +25,7 @@ extern const char pkglibdir[];
 extern const char pkgconfdir[];
 extern const char pkgstatedir[];
 extern const char pkgdatadir[];
+extern const char dochtmldir[];
 extern const char bindir[];
 extern const char sbindir[];
 extern const char finkbindir[];
diff --git a/lib/hreader.c b/lib/hreader.c
new file mode 100644 (file)
index 0000000..bdf3c07
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file lib/hreader.c
+ * @brief Hands-off reader - read files without keeping them open
+ */
+#include <config.h>
+#include "hreader.h"
+#include "mem.h"
+#include <string.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <unistd.h>
+
+static int hreader_fill(struct hreader *h, off_t offset);
+
+int hreader_init(const char *path, struct hreader *h) {
+  struct stat sb;
+  if(stat(path, &sb) < 0)
+    return -1;
+  memset(h, 0, sizeof *h);
+  h->path = xstrdup(path);
+  h->size = sb.st_size;
+  h->bufsize = 65536;
+  h->buffer = xmalloc_noptr(h->bufsize);
+  return 0;
+}
+
+void hreader_close(struct hreader *h) {
+  xfree(h->path);
+  xfree(h->buffer);
+}
+
+int hreader_read(struct hreader *h, void *buffer, size_t n) {
+  int r = hreader_pread(h, buffer, n, h->read_offset);
+  if(r > 0)
+    h->read_offset += r;
+  return r;
+}
+
+int hreader_pread(struct hreader *h, void *buffer, size_t n, off_t offset) {
+  size_t bytes_read = 0;
+
+  while(bytes_read < n) {
+    // If the desired byte range is outside the file, fetch new contents
+    if(offset < h->buf_offset || offset >= h->buf_offset + (off_t)h->bytes) {
+      int r = hreader_fill(h, offset);
+      if(r < 0)
+        return -1;                      /* disaster! */
+      else if(r == 0)
+        break;                          /* end of file */
+    }
+    // Figure out how much we can read this time round
+    size_t left = h->bytes - (offset - h->buf_offset);
+    // Truncate the read if we don't want that much
+    if(left > (n - bytes_read))
+      left = n - bytes_read;
+    memcpy((char *)buffer + bytes_read,
+           h->buffer + (offset - h->buf_offset),
+           left);
+    offset += left;
+    bytes_read += left;
+  }
+  return bytes_read;
+}
+
+static int hreader_fill(struct hreader *h, off_t offset) {
+  int fd = open(h->path, O_RDONLY);
+  if(fd < 0)
+    return -1;
+  int n = pread(fd, h->buffer, h->bufsize, offset);
+  close(fd);
+  if(n < 0)
+    return -1;
+  h->buf_offset = offset;
+  h->bytes = n;
+  return n;
+}
+
+off_t hreader_seek(struct hreader *h, off_t offset, int whence) {
+  switch(whence) {
+  case SEEK_SET: break;
+  case SEEK_CUR: offset += h->read_offset; break;
+  case SEEK_END: offset += h->size; break;
+  default: einval: errno = EINVAL; return -1;
+  }
+  if(offset < 0) goto einval;
+  h->read_offset = offset;
+  return offset;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/hreader.h b/lib/hreader.h
new file mode 100644 (file)
index 0000000..90431c1
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file lib/hreader.h
+ * @brief Hands-off reader - read files without keeping them open
+ */
+#ifndef HREADER_H
+#define HREADER_H
+
+#include <unistd.h>
+
+/** @brief A hands-off reader
+ *
+ * Allows files to be read without holding them open.
+ */
+struct hreader {
+  char *path;                   /* file to read */
+  off_t size;                   /* file size */
+  off_t read_offset;            /* for next hreader_read() */
+  off_t buf_offset;             /* offset of start of buffer */
+  char *buffer;                        /* input buffer */
+  size_t bufsize;              /* buffer size */
+  size_t bytes;                        /* size of last read */
+};
+
+/** @brief Initialize a hands-off reader
+ * @param path File to read
+ * @param h Reader to initialize
+ * @return 0 on success, -1 on error
+ */
+int hreader_init(const char *path, struct hreader *h);
+
+/** @brief Close a hands-off reader
+ * @param h Reader to close
+ */
+void hreader_close(struct hreader *h);
+
+/** @brief Read some bytes
+ * @param h Reader to read from
+ * @param buffer Where to store bytes
+ * @param n Maximum bytes to read
+ * @return Bytes read, or 0 at EOF, or -1 on error
+ */
+int hreader_read(struct hreader *h, void *buffer, size_t n);
+
+/** @brief Read some bytes at a given offset
+ * @param h Reader to read from
+ * @param offset Offset to read at
+ * @param buffer Where to store bytes
+ * @param n Maximum bytes to read
+ * @return Bytes read, or 0 at EOF, or -1 on error
+ */
+int hreader_pread(struct hreader *h, void *buffer, size_t n, off_t offset);
+
+/** @brief Seek within a file
+ * @param h Reader to seek
+ * @param offset Offset
+ * @param whence SEEK_*
+ * @return Result offset
+ */
+off_t hreader_seek(struct hreader *h, off_t offset, int whence);
+
+/** @brief Return file size
+ * @param h Reader to find size of
+ * @return Size in bytes
+ */
+static inline off_t hreader_size(const struct hreader *h) {
+  return h->size;
+}
+
+/** @brief Test for end of file
+ * @param h Reader to test
+ * @return 1 at eof, 0 otherwise
+ *
+ * This tells you whether the next read will return 0 bytes, rather than
+ * whether the last read reached end of file.  So it is slightly different to
+ * feof().
+ */
+static inline int hreader_eof(const struct hreader *h) {
+  return h->read_offset == h->size;
+}
+
+#endif /* HREADER_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 9cc54d64fc9c9e382b586a6f5629661d19685f86..48354b3d7ed636958bc2765cbfa3f1c97f99da44 100644 (file)
@@ -110,7 +110,7 @@ static const char *skipwhite(const char *s, int rfc822_comments) {
   int c, depth;
   
   for(;;) {
-    switch(c = *s) {
+    switch(*s) {
     case ' ':
     case '\t':
     case '\r':
@@ -402,7 +402,7 @@ int mime_multipart(const char *s,
  * @param s Start of field
  * @param dispositionp Where to store disposition
  * @param parameternamep Where to store parameter name
- * @param parametervaluep Wher to store parameter value
+ * @param parametervaluep Where to store parameter value
  * @return 0 on success, non-0 on error
  *
  * See <a href="http://tools.ietf.org/html/rfc2388#section-3">RFC 2388 s3</a>
index c7dbfd230268b653e4055b60304d893f5a77704b..2219763f6539cd5da3b83b9a01979ced7ea43872 100644 (file)
@@ -188,6 +188,13 @@ struct queue_entry {
   /** @brief How much of track has been played so far (seconds) */
   long sofar;
 
+  /** @brief True if track preparation is underway
+   *
+   * This is set when a decoder has been started and is expected to connect to
+   * the speaker, but the speaker has not sent as @ref SM_ARRIVED message back
+   * yet. */
+  int preparing;
+
   /** @brief True if decoder is connected to speaker 
    *
    * This is not a @ref playing_state for a couple of reasons
index c4c814e738c0fc16a3006fe31ce1dbbd10df2538..39514ed37c3cf6497ba05a1e530053932db39d57 100644 (file)
@@ -264,6 +264,7 @@ size_t resample_convert(const struct resampler *rs,
   if(rs->state) {
     /* A sample-rate conversion must be performed */
     SRC_DATA data;
+    memset(&data, 0, sizeof data);
     /* Compute how many frames are expected to come out. */
     size_t maxframesout = nframesin * rs->output_rate / rs->input_rate + 1;
     output = xcalloc(maxframesout * rs->output_channels, sizeof(float));
@@ -273,11 +274,16 @@ size_t resample_convert(const struct resampler *rs,
     data.output_frames = maxframesout;
     data.end_of_input = eof;
     data.src_ratio = (double)rs->output_rate / rs->input_rate;
+    D(("nframesin=%zu maxframesout=%zu eof=%d ratio=%d.%06d",
+       nframesin, maxframesout, eof,
+       (int)data.src_ratio,
+       ((int)(data.src_ratio * 1000000) % 1000000)));
     int error_ = src_process(rs->state, &data);
     if(error_)
       disorder_fatal(0, "calling src_process: %s", src_strerror(error_));
     nframesin = data.input_frames_used;
     nsamplesout = data.output_frames_gen * rs->output_channels;
+    D(("new nframesin=%zu nsamplesout=%zu", nframesin, nsamplesout));
   }
 #endif
   if(!output) {
index 524f68d97fb88449f624dba7014f48c75dac7f0e..0c5f4db0f7a70e9797a861169d561a5ea9fc79ab 100644 (file)
@@ -118,7 +118,7 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
                      const char *encoding,
                      const char *content_type,
                      const char *body) {
-  int rc, sol = 1;
+  int sol = 1;
   const char *ptr;
   uint8_t idbuf[20];
   char *id;
@@ -131,23 +131,23 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
   strftime(date, sizeof date, "%a, %d %b %Y %H:%M:%S +0000", &ut);
   gcry_create_nonce(idbuf, sizeof idbuf);
   id = mime_to_base64(idbuf, sizeof idbuf);
-  if((rc = getresponse(tag, in)) / 100 != 2)
+  if(getresponse(tag, in) / 100 != 2)
     return -1;
   if(sendcommand(tag, out, "HELO %s", local_hostname()))
     return -1;
-  if((rc = getresponse(tag, in)) / 100 != 2)
+  if(getresponse(tag, in) / 100 != 2)
     return -1;
   if(sendcommand(tag, out, "MAIL FROM:<%s>", sender))
     return -1;
-  if((rc = getresponse(tag, in)) / 100 != 2)
+  if(getresponse(tag, in) / 100 != 2)
     return -1;
   if(sendcommand(tag, out, "RCPT TO:<%s>", recipient))
     return -1;
-  if((rc = getresponse(tag, in)) / 100 != 2)
+  if(getresponse(tag, in) / 100 != 2)
     return -1;
   if(sendcommand(tag, out, "DATA", sender))
     return -1;
-  if((rc = getresponse(tag, in)) / 100 != 3)
+  if(getresponse(tag, in) / 100 != 3)
     return -1;
   if(fprintf(out, "From: %s\r\n", pubsender) < 0
      || fprintf(out, "To: %s\r\n", recipient) < 0
@@ -181,7 +181,7 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
   if(fprintf(out, ".\r\n") < 0
      || fflush(out) < 0)
     goto write_error;
-  if((rc = getresponse(tag, in)) / 100 != 2)
+  if(getresponse(tag, in) / 100 != 2)
     return -1;
   return 0;
 }
index 3b21f0a8ef8821f21f5a25a3fa844e2a1964c520..24fc970587d7f748e1b6aa3db3e23a9fe2b58d69 100644 (file)
@@ -43,6 +43,7 @@ struct speaker_message {
    * - @ref SM_FINISHED
    * - @ref SM_PLAYING
    * - @ref SM_UNKNOWN
+   * - @ref SM_ARRIVED
    */
   int type;
 
@@ -102,6 +103,9 @@ struct speaker_message {
 /** @brief Cancelled track @c id which wasn't playing */
 #define SM_STILLBORN 133
 
+/** @brief A connection for track @c id arrived */
+#define SM_ARRIVED 134
+
 void speaker_send(int fd, const struct speaker_message *sm);
 /* Send a message. */
 
index b21b5d9f83349a8f95d3ee3d1c90cee391dbe704..3be725e32d4d9106a4faa5a6bf59e4d56ddc0191 100644 (file)
@@ -153,7 +153,6 @@ int trackdb_get_global_tid(const char *name,
 
 char **parsetags(const char *s);
 int tag_intersection(char **a, char **b);
-int valid_username(const char *user);
 
 #endif /* TRACKDB_INT_H */
 
index fd333938968eff847f2d9e7f9a45d06fc0b27897..27d33193feb7f6db8e7898785467178ed084a199 100644 (file)
@@ -33,6 +33,7 @@
 #include "configuration.h"
 #include "vector.h"
 #include "eventlog.h"
+#include "validity.h"
 
 static int trackdb_playlist_get_tid(const char *name,
                                     const char *who,
@@ -54,43 +55,6 @@ static int trackdb_playlist_delete_tid(const char *name,
                                        const char *who,
                                        DB_TXN *tid);
 
-/** @brief Parse a playlist name
- * @param name Playlist name
- * @param ownerp Where to put owner, or NULL
- * @param sharep Where to put default sharing, or NULL
- * @return 0 on success, -1 on error
- *
- * Playlists take the form USER.PLAYLIST or just PLAYLIST.  The PLAYLIST part
- * is alphanumeric and nonempty.  USER is a username (see valid_username()).
- */
-int playlist_parse_name(const char *name,
-                        char **ownerp,
-                        char **sharep) {
-  const char *dot = strchr(name, '.'), *share;
-  char *owner;
-
-  if(dot) {
-    /* Owned playlist */
-    owner = xstrndup(name, dot - name);
-    if(!valid_username(owner))
-      return -1;
-    if(!valid_username(dot + 1))
-      return -1;
-    share = "private";
-  } else {
-    /* Shared playlist */
-    if(!valid_username(name))
-      return -1;
-    owner = 0;
-    share = "shared";
-  }
-  if(ownerp)
-    *ownerp = owner;
-  if(sharep)
-    *sharep = xstrdup(share);
-  return 0;
-}
-
 /** @brief Check read access rights
  * @param name Playlist name
  * @param who Who wants to read
index d2ebe070687dc187d657e8f59af42687634fe294..da5685add564c439c620df41ff3b99d2706462e6 100644 (file)
@@ -59,6 +59,7 @@
 #include "unidata.h"
 #include "base64.h"
 #include "sendmail.h"
+#include "validity.h"
 
 #define RESCAN "disorder-rescan"
 #define DEADLOCK "disorder-deadlock"
@@ -839,7 +840,7 @@ static char **dedupe(char **vec, int nvec) {
   int m, n;
 
   qsort(vec, nvec, sizeof (char *), wordcmp);
-  m = n = 0;
+  m = 0;
   if(nvec) {
     vec[m++] = vec[0];
     for(n = 1; n < nvec; ++n)
@@ -1822,7 +1823,7 @@ int trackdb_set(const char *track,
         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;
+      if(compute_alias(&newalias, track, p, tid)) goto fail;
       /* check whether alias has changed */
       if(!(oldalias == newalias
            || (oldalias && newalias && !strcmp(oldalias, newalias)))) {
@@ -2175,13 +2176,13 @@ const char *trackdb_getpart(const char *track,
   DB_TXN *tid;
   char *pref;
   const char *actual;
-  int used_db, err;
+  int used_db;
 
   /* 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)
+    if(gettrackdata(track, 0, &p, &actual, 0, tid) == DB_LOCK_DEADLOCK)
       goto fail;
     break;
 fail:
@@ -2492,6 +2493,9 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
     }
     if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
     cursor = 0;
+    if(err)
+      goto fail;
+    cursor = 0;
     /* do a naive search over that (hopefuly fairly small) list of tracks */
     u.nvec = 0;
     for(n = 0; n < v.nvec; ++n) {
@@ -2737,12 +2741,11 @@ void trackdb_set_global(const char *name,
                         const char *value,
                         const char *who) {
   DB_TXN *tid;
-  int err;
   int state;
 
   for(;;) {
     tid = trackdb_begin_transaction();
-    if(!(err = trackdb_set_global_tid(name, value, tid)))
+    if(!trackdb_set_global_tid(name, value, tid))
       break;
     trackdb_abort_transaction(tid);
   }
@@ -2799,12 +2802,11 @@ int trackdb_set_global_tid(const char *name,
  */
 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)))
+    if(!trackdb_get_global_tid(name, tid, &r))
       break;
     trackdb_abort_transaction(tid);
   }
@@ -2908,7 +2910,7 @@ static char **trackdb_new_tid(int *ntracksp,
   default:
     disorder_fatal(0, "error reading noticed.db: %s", db_strerror(err));
   }
-  if((err = trackdb_closecursor(c)))
+  if(trackdb_closecursor(c))
     return 0;                           /* deadlock */
   vector_terminate(tracks);
   if(ntracksp)
@@ -3013,32 +3015,6 @@ static int trusted(const char *user) {
   return n < config->trust.n;
 }
 
-/** @brief Return non-zero for a valid username
- * @param user Candidate username
- * @return Nonzero if it's valid
- *
- * Currently we only allow the letters and digits in ASCII.  We could be more
- * liberal than this but it is a nice simple test.  It is critical that
- * semicolons are never allowed.
- *
- * NB also used by playlist_parse_name() to validate playlist names!
- */
-int valid_username(const char *user) {
-  if(!*user)
-    return 0;
-  while(*user) {
-    const uint8_t c = *user++;
-    /* For now we are very strict */
-    if((c >= 'a' && c <= 'z')
-       || (c >= 'A' && c <= 'Z')
-       || (c >= '0' && c <= '9'))
-      /* ok */;
-    else
-      return 0;
-  }
-  return 1;
-}
-
 /** @brief Add a user
  * @param user Username
  * @param password Initial password or NULL
index 901a74a2b66aacf8c6cec1ba7c20b68d93f70efb..1d7c8e97cf02d94e25498a32527eee21622cef79 100644 (file)
@@ -184,9 +184,6 @@ void trackdb_add_rescanned(void (*rescanned)(void *ru),
                            void *ru);
 int trackdb_rescan_underway(void);
 
-int playlist_parse_name(const char *name,
-                        char **ownerp,
-                        char **sharep);
 int trackdb_playlist_get(const char *name,
                          const char *who,
                          char ***tracksp,
index 4e2e06e22e32487ceea76ac08e70750d710399da..aa11e2e819286fc38ec8ea84b354e44e47180918 100644 (file)
@@ -51,6 +51,10 @@ const char *find_track_root(const char *track) {
   const struct collection *c = find_track_collection(track);
   if(c)
     return c->root;
+  /* Suppress this message for scratches */
+  for(int n = 0; n < config->scratch.n; ++n)
+    if(!strcmp(track, config->scratch.s[n]))
+      return 0;
   disorder_error(0, "found track in no collection '%s'", track);
   return 0;
 }
index 63b881bb836c523ad962b81f26118e4d47129c9f..56f933ee95682d21911e856f2d3255b7018585b7 100644 (file)
@@ -51,7 +51,14 @@ int compare_path_raw(const unsigned char *ap, size_t an,
 /* Comparison function for path names that groups all entries in a directory
  * together */
 
-/* Convenient wrapper for compare_path_raw */
+/** @brief Compare two paths
+ * @param ap First path
+ * @param bp Second path
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ * A wrapper around 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));
index d0ae4486f597927de06222858fa65863ce959a0d..94d2bd5e9cde7dece533e64728a8cc83c5f8fbad 100644 (file)
 #include "log.h"
 #include "unicode.h"
 
+/** @brief Compare two tracks
+ * @param sa First sort key
+ * @param sb Second sort key
+ * @param da First display string
+ * @param db Second display string
+ * @param ta First raw track
+ * @param tb Second raw track
+ * @return -ve, 0 or +ve for a <, = or > b
+ *
+ * Tries the following comparisons until a difference is found:
+ * - case-independent comparison of sort keys
+ * - case-dependent comparison of sort keys
+ * - case-independent comparison of display strings
+ * - case-dependent comparison of display strings
+ * - case-dependent comparison of paths (see compare_path())
+ */
 int compare_tracks(const char *sa, const char *sb,
                   const char *da, const char *db,
                   const char *ta, const char *tb) {
@@ -43,6 +59,17 @@ int compare_tracks(const char *sa, const char *sb,
   return compare_path(ta, tb);
 }
 
+/** @brief Compare two paths
+ * @param ap First path
+ * @param an Length of @p ap
+ * @param bp Second path
+ * @param bn Length @p bp
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ *
+ * See also compare_path().
+ */
 int compare_path_raw(const unsigned char *ap, size_t an,
                     const unsigned char *bp, size_t bn) {
   /* Don't change this function!  The database sort order depends on it */
index 2f81739098583802ff2f382ebfa6e57470c64531..b1fcc3af87d01f0134613e4fa2b19455d7a2cac9 100644 (file)
@@ -32,6 +32,16 @@ static int tracksort_compare(const void *a, const void *b) {
                        ea->track, eb->track);
 }
 
+/** @brief Sort tracks
+ * @param ntracks Number of tracks to sort
+ * @param tracks List of tracks
+ * @param type Comparison type
+ * @return Sorted track data
+ *
+ * Tracks are compared using compare_tracks(), with the sort key and display
+ * string set according to @p type, which should be "track" if the tracks are
+ * really tracks and "dir" if they are directories.
+ */
 struct tracksort_data *tracksort_init(int ntracks,
                                       char **tracks,
                                       const char *type) {
diff --git a/lib/validity.c b/lib/validity.c
new file mode 100644 (file)
index 0000000..408c4d5
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2009 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/validity.c
+ * @brief Various validity checks
+ */
+#include "common.h"
+#include "validity.h"
+
+#include "mem.h"
+
+/** @brief Parse a playlist name
+ * @param name Playlist name
+ * @param ownerp Where to put owner, or NULL
+ * @param sharep Where to put default sharing, or NULL
+ * @return 0 on success, -1 on error
+ *
+ * Playlists take the form USER.PLAYLIST or just PLAYLIST.  The PLAYLIST part
+ * is alphanumeric and nonempty.  USER is a username (see valid_username()).
+ */
+int playlist_parse_name(const char *name,
+                        char **ownerp,
+                        char **sharep) {
+  const char *dot = strchr(name, '.'), *share;
+  char *owner;
+
+  if(dot) {
+    /* Owned playlist */
+    owner = xstrndup(name, dot - name);
+    if(!valid_username(owner))
+      return -1;
+    if(!valid_username(dot + 1))
+      return -1;
+    share = "private";
+  } else {
+    /* Shared playlist */
+    if(!valid_username(name))
+      return -1;
+    owner = 0;
+    share = "shared";
+  }
+  if(ownerp)
+    *ownerp = owner;
+  if(sharep)
+    *sharep = xstrdup(share);
+  return 0;
+}
+
+/** @brief Return non-zero for a valid username
+ * @param user Candidate username
+ * @return Nonzero if it's valid
+ *
+ * Currently we only allow the letters and digits in ASCII.  We could be more
+ * liberal than this but it is a nice simple test.  It is critical that
+ * semicolons are never allowed.
+ *
+ * NB also used by playlist_parse_name() to validate playlist names!
+ */
+int valid_username(const char *user) {
+  if(!*user)
+    return 0;
+  while(*user) {
+    const uint8_t c = *user++;
+    /* For now we are very strict */
+    if((c >= 'a' && c <= 'z')
+       || (c >= 'A' && c <= 'Z')
+       || (c >= '0' && c <= '9'))
+      /* ok */;
+    else
+      return 0;
+  }
+  return 1;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/validity.h b/lib/validity.h
new file mode 100644 (file)
index 0000000..8ce2505
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2009 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/validity.c
+ * @brief Various validity checks
+ */
+#ifndef VALIDITY_H
+#define VALIDITY_H
+
+#include "common.h"
+#include "validity.h"
+
+int playlist_parse_name(const char *name,
+                        char **ownerp,
+                        char **sharep);
+int valid_username(const char *user);
+
+#endif /* VALIDITY_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 455bb729275ea66bcfae8efb4080aff735fc132d..fe7ffbbf0443289018bf2852dbc5c364f5f5e764 100644 (file)
--- a/lib/wav.c
+++ b/lib/wav.c
@@ -128,9 +128,8 @@ int wav_init(struct wavfile *f, const char *path) {
   off_t where;
   
   memset(f, 0, sizeof *f);
-  f->fd = -1;
   f->data = -1;
-  if((f->fd = open(path, O_RDONLY)) < 0) goto error_errno;
+  if(hreader_init(path, f->input)) goto error_errno;
   /* Read the file header
    *
    *  offset  size  meaning
@@ -138,7 +137,7 @@ int wav_init(struct wavfile *f, const char *path) {
    *  04      4     length of rest of file
    *  08      4     'WAVE'
    * */
-  if((n = pread(f->fd, header, 12, 0)) < 0) goto error_errno;
+  if((n = hreader_pread(f->input, header, 12, 0)) < 0) goto error_errno;
   else if(n < 12) goto einval;
   if(strncmp(header, "RIFF", 4) || strncmp(header + 8, "WAVE", 4))
     goto einval;
@@ -151,7 +150,7 @@ int wav_init(struct wavfile *f, const char *path) {
      *  00      4     chunk ID
      *  04      4     length of rest of chunk
      */
-    if((n = pread(f->fd, header, 8, where)) < 0) goto error_errno;
+    if((n = hreader_pread(f->input, header, 8, where)) < 0) goto error_errno;
     else if(n < 8) goto einval;
     if(!strncmp(header,"fmt ", 4)) {
       /* This is the format chunk
@@ -168,7 +167,8 @@ int wav_init(struct wavfile *f, const char *path) {
        *  18      ?     extra undocumented rubbish
        */
       if(get32(header + 4) < 16) goto einval;
-      if((n = pread(f->fd, header + 8, 16, where + 8)) < 0) goto error_errno;
+      if((n = hreader_pread(f->input, header + 8, 16, where + 8)) < 0)
+        goto error_errno;
       else if(n < 16) goto einval;
       f->channels = get16(header + 0x0A);
       f->rate = get32(header + 0x0C);
@@ -197,13 +197,7 @@ error:
 
 /** @brief Close a WAV file */
 void wav_destroy(struct wavfile *f) {
-  if(f) {
-    const int save_errno = errno;
-
-    if(f->fd >= 0)
-      close(f->fd);
-    errno = save_errno;
-  }
+  hreader_close(f->input);
 }
 
 /** @brief Visit all the data in a WAV file
@@ -227,7 +221,7 @@ int wav_data(struct wavfile *f,
     size_t want = (off_t)sizeof buffer > left ? (size_t)left : sizeof buffer;
 
     want -= want % bytes_per_frame;
-    if((n = pread(f->fd, buffer, want, where)) < 0) return errno;
+    if((n = hreader_pread(f->input, buffer, want, where)) < 0) return errno;
     if((size_t)n < want) return EINVAL;
     if((err = callback(f, buffer, n, u))) return err;
     where += n;
index d2de912b280ff9b16d8d4d9a96784bbe7ce25311..3c9c477bc5a2975559891126bd010cdb7a6b153c 100644 (file)
--- a/lib/wav.h
+++ b/lib/wav.h
 #ifndef WAV_H
 #define WAV_H
 
+#include "hreader.h"
+
 /** @brief WAV file access structure */
 struct wavfile {
-  /** @brief File descriptor onto file */
-  int fd;
+  /** @brief File read handle */
+  struct hreader input[1];
 
   /** @brief File length */
   off_t length;
index 7be6c510063f1ebfdaf388969523409b7c5e5087..de3562b15506c47eaaaa89dea206597f289e29f2 100644 (file)
@@ -20,7 +20,8 @@ TESTS=t-addr t-arcfour t-basen t-bits t-cache t-casefold t-charset    \
        t-cookies t-dateparse t-event t-filepart t-hash t-heap t-hex    \
        t-kvp t-mime t-printf t-regsub t-selection t-signame t-sink     \
        t-split t-syscalls t-trackname t-unicode t-url t-utf8 t-vector  \
-       t-words t-wstat t-macros t-cgi t-eventdist t-resample
+       t-words t-wstat t-macros t-cgi t-eventdist t-resample           \
+       t-configuration
 
 noinst_PROGRAMS=$(TESTS)
 
@@ -63,6 +64,8 @@ t_wstat_SOURCES=t-wstat.c test.c test.h
 t_eventdist_SOURCES=t-eventdist.c test.c test.h
 t_resample_SOURCES=t-resample.c test.c test.h
 t_resample_LDFLAGS=$(LIBSAMPLERATE)
+t_configuration_SOURCES=t-configuration.c test.c test.h
+t_configuration_LDFLAGS=$(LIBGCRYPT)
 
 check-report: before-check check make-coverage-reports
 before-check:
diff --git a/libtests/t-configuration.c b/libtests/t-configuration.c
new file mode 100644 (file)
index 0000000..4ff7b18
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "test.h"
+
+static void test_configuration(void) {
+  insist(config_verify() == 0);
+}
+
+TEST(configuration);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 4d8b68aaa4afa348367aaef3f397e85bcef7d35d..e23ee1a961879a55445ab3560f52d53f01bd0e40 100644 (file)
@@ -50,8 +50,10 @@ static void test_hash(void) {
   
   for(i = 0; i < 10000; ++i) {
     insist((ip = hash_find(h, do_printf("%d", i))) != 0);
-    check_integer(*ip, i);
-    insist(hash_add(h, do_printf("%d", i), &i, HASH_REPLACE) == 0);
+    if(ip) {
+      check_integer(*ip, i);
+      insist(hash_add(h, do_printf("%d", i), &i, HASH_REPLACE) == 0);
+    }
   }
   check_integer(hash_count(h), 10000);
   keys = hash_keys(h);
index a6f1b32c6f9a67518041d8871f63b01249075f22..f2d599408ea151d5cfa8e09a06814006c359720a 100644 (file)
@@ -52,12 +52,15 @@ static void test_hex(void) {
   check_string(hex(h, sizeof h), "00ff807f");
   check_string(hex(0, 0), "");
   u = unhex("00ff807f", &ul);
+  insist(u != 0);
   insist(ul == 4);
   insist(memcmp(u, h, 4) == 0);
   u = unhex("00FF807F", &ul);
+  insist(u != 0);
   insist(ul == 4);
   insist(memcmp(u, h, 4) == 0);
   u = unhex("", &ul);
+  insist(u != 0);
   insist(ul == 0);
   fprintf(stderr, "2 ERROR reports expected {\n");
   insist(unhex("F", 0) == 0);
index cada395ad03dcf55c8f0fcbc73d4775d928d7d7b..87f0224cf60de01d6bb23504b0277b198275546b 100644 (file)
@@ -95,7 +95,7 @@ extern int skipped;
   const char *got = GOT;                                                \
   const char *want = WANT;                                              \
                                                                         \
-  if(want == 0) {                                                       \
+  if(got == 0) {                                                       \
     fprintf(stderr, "%s:%d: %s returned 0\n",                           \
             __FILE__, __LINE__, #GOT);                                  \
     count_error();                                                      \
index 7852152a2a81f30d09fafac385ea89dcda784828..76d792331da87483acdfab71c81984ebc1e06b37 100644 (file)
@@ -23,7 +23,10 @@ AM_CPPFLAGS=-I${top_srcdir}/lib
 notify_la_SOURCES=notify.c
 notify_la_LDFLAGS=-module
 
-disorder_tracklength_la_SOURCES=tracklength.c mad.c madshim.h ../lib/wav.h ../lib/wav.c
+disorder_tracklength_la_SOURCES=tracklength.c tracklength.h    \
+tracklength-mp3.c tracklength-ogg.c tracklength-wav.c          \
+tracklength-flac.c mad.c madshim.h ../lib/wav.h ../lib/wav.c   \
+../lib/hreader.h ../lib/hreader.c
 disorder_tracklength_la_LDFLAGS=-module
 disorder_tracklength_la_LIBADD=$(LIBVORBISFILE) $(LIBMAD) $(LIBFLAC) -lm
 
index f6ed8b37d4c8b1eeb6e75658a74b19bc67121826..e2e712c9b4f32eb171c8fe97d3f77f642839ea78 100644 (file)
@@ -39,7 +39,6 @@ void disorder_play_track(const char *const *parameters,
   const char **vec;
 
   vec = disorder_malloc((nparameters + 2) * sizeof (char *));
-  i = 0;
   j = 0;
   for(i = 0; i < nparameters; ++i)
     vec[j++] = parameters[i];
diff --git a/plugins/tracklength-flac.c b/plugins/tracklength-flac.c
new file mode 100644 (file)
index 0000000..a838966
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file plugins/tracklength-flac.c
+ * @brief Compute track lengths for FLAC files
+ */
+#include "tracklength.h"
+#include <FLAC/stream_decoder.h>
+
+/* libFLAC's "simplified" interface is rather heavyweight... */
+
+struct flac_state {
+  long duration;
+  const char *path;
+};
+
+static void flac_metadata(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                         const FLAC__StreamMetadata *metadata,
+                         void *client_data) {
+  struct flac_state *const state = client_data;
+  const FLAC__StreamMetadata_StreamInfo *const stream_info
+    = &metadata->data.stream_info;
+
+  if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
+    /* FLAC uses 0 to mean unknown and conveniently so do we */
+    state->duration = (stream_info->total_samples
+                      + stream_info->sample_rate - 1)
+           / stream_info->sample_rate;
+}
+
+static void flac_error(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                      FLAC__StreamDecoderErrorStatus status,
+                      void *client_data) {
+  const struct flac_state *const state = client_data;
+
+  disorder_error(0, "error decoding %s: %s", state->path,
+                FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+static FLAC__StreamDecoderWriteStatus flac_write
+    (const FLAC__StreamDecoder attribute((unused)) *decoder,
+     const FLAC__Frame attribute((unused)) *frame,
+     const FLAC__int32 attribute((unused)) *const buffer_[],
+     void attribute((unused)) *client_data) {
+  const struct flac_state *const state = client_data;
+
+  if(state->duration >= 0)
+    return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
+  else
+    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+long tl_flac(const char *path) {
+  FLAC__StreamDecoder *sd = 0;
+  FLAC__StreamDecoderInitStatus is;
+  struct flac_state state[1];
+
+  state->duration = -1;                        /* error */
+  state->path = path;
+  if(!(sd = FLAC__stream_decoder_new())) {
+    disorder_error(0, "FLAC__stream_decoder_new failed");
+    goto fail;
+  }
+  if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
+                                         flac_error, state))) {
+    disorder_error(0, "FLAC__stream_decoder_init_file %s: %s",
+                  path, FLAC__StreamDecoderInitStatusString[is]);
+    goto fail;
+  }
+  FLAC__stream_decoder_process_until_end_of_metadata(sd);
+fail:
+  if(sd)
+    FLAC__stream_decoder_delete(sd);
+  return state->duration;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-mp3.c b/plugins/tracklength-mp3.c
new file mode 100644 (file)
index 0000000..ecd1c1e
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file plugins/tracklength-mp3.c
+ * @brief Compute track lengths for MP3 files
+ */
+#include "tracklength.h"
+#include <mad.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;
+}
+
+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;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-ogg.c b/plugins/tracklength-ogg.c
new file mode 100644 (file)
index 0000000..c5c90c6
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file plugins/tracklength-ogg.c
+ * @brief Compute track lengths for OGG files
+ */
+#include "tracklength.h"
+#include <vorbis/vorbisfile.h>
+
+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;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-wav.c b/plugins/tracklength-wav.c
new file mode 100644 (file)
index 0000000..bf501c5
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file plugins/tracklength-wav.c
+ * @brief Compute track lengths for WAV files
+ */
+#include "tracklength.h"
+#include "wav.h"
+
+long tl_wav(const char *path) {
+  struct wavfile f[1];
+  int err, sample_frame_size;
+  long duration;
+
+  if((err = wav_init(f, path))) {
+    disorder_error(err, "error opening %s", path); 
+    return -1;
+  }
+  sample_frame_size = (f->bits + 7) / 8 * f->channels;
+  if(sample_frame_size) {
+    const long long n_samples = f->datasize / sample_frame_size;
+    duration = (n_samples + f->rate - 1) / f->rate;
+  } else
+    duration = -1;
+  wav_destroy(f);
+  return duration;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
index 86a1fc8a26667b94ef5393be51536c37342557d9..24d22b64b709f3a0fcde2e913812ca1c7c7967d5 100644 (file)
  *
  * Currently implements MP3, OGG, FLAC and WAV.
  */
-
-#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>
-/* libFLAC has had an API change and stupidly taken away the old API */
-#if HAVE_FLAC_FILE_DECODER_H
-# include <FLAC/file_decoder.h>
-#else
-# include <FLAC/stream_decoder.h>
-#define FLAC__FileDecoder FLAC__StreamDecoder
-#define FLAC__FileDecoderState FLAC__StreamDecoderState
-#endif
-
-
-#include <disorder.h>
-
-#include "madshim.h"
-#include "wav.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) {
-  struct wavfile f[1];
-  int err, sample_frame_size;
-  long duration;
-
-  if((err = wav_init(f, path))) {
-    disorder_error(err, "error opening %s", path); 
-    return -1;
-  }
-  sample_frame_size = (f->bits + 7) / 8 * f->channels;
-  if(sample_frame_size) {
-    const long long n_samples = f->datasize / sample_frame_size;
-    duration = (n_samples + f->rate - 1) / f->rate;
-  } else
-    duration = -1;
-  wav_destroy(f);
-  return duration;
-}
-
-/* libFLAC's "simplified" interface is rather heavyweight... */
-
-struct flac_state {
-  long duration;
-  const char *path;
-};
-
-static void flac_metadata(const FLAC__FileDecoder attribute((unused)) *decoder,
-                         const FLAC__StreamMetadata *metadata,
-                         void *client_data) {
-  struct flac_state *const state = client_data;
-  const FLAC__StreamMetadata_StreamInfo *const stream_info
-    = &metadata->data.stream_info;
-
-  if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
-    /* FLAC uses 0 to mean unknown and conveniently so do we */
-    state->duration = (stream_info->total_samples
-                      + stream_info->sample_rate - 1)
-           / stream_info->sample_rate;
-}
-
-static void flac_error(const FLAC__FileDecoder attribute((unused)) *decoder,
-                      FLAC__StreamDecoderErrorStatus status,
-                      void *client_data) {
-  const struct flac_state *const state = client_data;
-
-  disorder_error(0, "error decoding %s: %s", state->path,
-                FLAC__StreamDecoderErrorStatusString[status]);
-}
-
-static FLAC__StreamDecoderWriteStatus flac_write
-    (const FLAC__FileDecoder attribute((unused)) *decoder,
-     const FLAC__Frame attribute((unused)) *frame,
-     const FLAC__int32 attribute((unused)) *const buffer_[],
-     void attribute((unused)) *client_data) {
-  const struct flac_state *const state = client_data;
-
-  if(state->duration >= 0)
-    return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
-  else
-    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
-}
-
-static long tl_flac(const char *path) {
-  struct flac_state state[1];
-
-  state->duration = -1;                        /* error */
-  state->path = path;
-#if HAVE_FLAC_FILE_DECODER_H 
-  {
-    FLAC__FileDecoder *fd = 0;
-    FLAC__FileDecoderState fs;
-    
-    if(!(fd = FLAC__file_decoder_new())) {
-      disorder_error(0, "FLAC__file_decoder_new failed");
-      goto fail;
-    }
-    if(!(FLAC__file_decoder_set_filename(fd, path))) {
-      disorder_error(0, "FLAC__file_set_filename failed");
-      goto fail;
-    }
-    FLAC__file_decoder_set_metadata_callback(fd, flac_metadata);
-    FLAC__file_decoder_set_error_callback(fd, flac_error);
-    FLAC__file_decoder_set_write_callback(fd, flac_write);
-    FLAC__file_decoder_set_client_data(fd, state);
-    if((fs = FLAC__file_decoder_init(fd))) {
-      disorder_error(0, "FLAC__file_decoder_init: %s",
-                    FLAC__FileDecoderStateString[fs]);
-      goto fail;
-    }
-    FLAC__file_decoder_process_until_end_of_metadata(fd);
-fail:
-    if(fd)
-      FLAC__file_decoder_delete(fd);
-  }
-#else
-  {
-    FLAC__StreamDecoder *sd = 0;
-    FLAC__StreamDecoderInitStatus is;
-    
-    if(!(sd = FLAC__stream_decoder_new())) {
-      disorder_error(0, "FLAC__stream_decoder_new failed");
-      goto fail;
-    }
-    if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
-                                           flac_error, state))) {
-      disorder_error(0, "FLAC__stream_decoder_init_file %s: %s",
-                    path, FLAC__StreamDecoderInitStatusString[is]);
-      goto fail;
-    }
-    FLAC__stream_decoder_process_until_end_of_metadata(sd);
-fail:
-    if(sd)
-      FLAC__stream_decoder_delete(sd);
-  }
-#endif
-  return state->duration;
-}
+#include "tracklength.h"
 
 static const struct {
   const char *ext;
diff --git a/plugins/tracklength.h b/plugins/tracklength.h
new file mode 100644 (file)
index 0000000..c8e22a4
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TRACKLENGTH_H
+#define TRACKLENGTH_H
+
+#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 <disorder.h>
+
+long tl_ogg(const char *path);
+long tl_wav(const char *path);
+long tl_mp3(const char *path);
+long tl_flac(const char *path);
+
+#endif /* TRACKLENGTH_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 21452dce500c0df6a45e6cba9ff887befc6ecd34..5290d1bf70373d0e8d5b9e2a9c618aff1191b4b1 100755 (executable)
@@ -1,7 +1,7 @@
 #! /bin/sh
 #
 # This file is part of DisOrder
-# Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007, 2008, 2010 Richard Kettlewell
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 set -e
 
 stdhead=false
+extension="html"
 
 while test $# -gt 0; do
   case "$1" in
   -stdhead )
     stdhead=true
     ;;
+  -extension )
+    shift
+    extension=$1
+    ;;
+  -- )
+    shift
+    break
+    ;;
   -* )
     echo >&2 "ERROR: unknown option $1"
     exit 1
     ;;
   * )
     break
+    ;;
   esac
   shift
 done
 
-title=$(basename $1)
-
-echo "<html>"
-echo " <head>"
-if $stdhead; then
-  echo "@quiethead@#"
-fi
-echo "  <title>$title</title>"
-echo " </head>"
-echo " <body>"
-if $stdhead; then
-  echo "@stdmenu{}@#"
-fi
-printf "   <pre class=manpage>"
-# this is kind of painful using only BREs
-nroff -Tascii -man "$1" | ${GNUSED} \
-                      '1d;$d;
-                       1,/./{/^$/d};
-                       s/&/\&amp;/g;
-                       s/</\&lt;/g;
-                       s/>/\&gt;/g;
-                       s/@/\&#64;/g;
-                       s!\(.\)\b\1!<b>\1</b>!g;
-                       s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
-                       s!_\b\(.\)!<i>\1</i>!g;
-                       s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
-                       s!</\([bi]\)><\1>!!g'
-echo "</pre>"
-if $stdhead; then
-  echo "@credits"
-fi
-echo " </body>"
-echo "</html>"
+for page; do
+  title=$(basename $page)
+  output=$(basename $page).$extension
+  echo "$page -> $output" >&2
+  exec > $output.new
+  echo "<html>"
+  echo " <head>"
+  if $stdhead; then
+    echo "@quiethead@#"
+  fi
+  echo "  <title>$title</title>"
+  echo " </head>"
+  echo " <body>"
+  if $stdhead; then
+    echo "@stdmenu{}@#"
+  fi
+  printf "   <pre class=manpage>"
+  # this is kind of painful using only BREs
+  nroff -Tascii -man "$page" | ${GNUSED} \
+                        '1d;$d;
+                         1,/./{/^$/d};
+                         s/&/\&amp;/g;
+                         s/</\&lt;/g;
+                         s/>/\&gt;/g;
+                         s/@/\&#64;/g;
+                         s!\(.\)\b\1!<b>\1</b>!g;
+                         s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
+                         s!_\b\(.\)!<i>\1</i>!g;
+                         s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
+                         s!</\([bi]\)><\1>!!g'
+  echo "</pre>"
+  if $stdhead; then
+    echo "@credits"
+  fi
+  echo " </body>"
+  echo "</html>"
+  mv $output.new $output
+done
index 5a28face2c2102b71df0856126f40283e0742e7f..a02e7534dac6b1eafe7605cdb4db0901f98fc47a 100644 (file)
@@ -23,6 +23,7 @@ $(SEDFILES) : % : %.in Makefile
            -e 's!pkgconfdir!${sysconfdir}/disorder!g;' \
            -e 's!pkgstatedir!${localstatedir}/disorder!g;' \
            -e 's!pkgdatadir!${pkgdatadir}!g;' \
+           -e 's!dochtmldir!${dochtmldir}!g;' \
            -e 's!SENDMAIL!${SENDMAIL}!g;' \
            -e 's!_version_!${VERSION}!g;' \
                < $< > $@.new
index e73e090ef78eab0985eee7444e4e9f27f40d437f..27c8fdd5090b0e48b6f09a66423c9d6ec094542d 100644 (file)
@@ -1,6 +1,6 @@
 #
 # This file is part of DisOrder.
-# Copyright (C) 2004-2009 Richard Kettlewell
+# Copyright (C) 2004-2010 Richard Kettlewell
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -25,7 +25,7 @@ AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 
 disorderd_SOURCES=disorderd.c api.c api-server.c daemonize.c play.c    \
        server.c server-queue.c queue-ops.c state.c plugin.c            \
-       schedule.c dbparams.c background.c \
+       schedule.c dbparams.c background.c mount.c \
        exports.c ../lib/memgc.c disorder-server.h
 disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBPCRE) $(LIBDB) $(LIBAO) $(LIBGC) $(LIBGCRYPT) $(LIBICONV) \
@@ -44,7 +44,8 @@ disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBPTHREAD)
 disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
 
-disorder_decode_SOURCES=decode.c disorder-server.h
+disorder_decode_SOURCES=decode.c decode.h disorder-server.h    \
+decode-mp3.c decode-ogg.c decode-wav.c decode-flac.c
 disorder_decode_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBMAD) $(LIBVORBISFILE) $(LIBFLAC)
 disorder_decode_DEPENDENCIES=../lib/libdisorder.a
@@ -121,7 +122,7 @@ check-decode: check-wav check-flac check-mp3
 
 check-mp3: disorder-decode disorder-normalize
        ./disorder-decode ${top_srcdir}/sounds/scratch.mp3 | \
-         ./disorder-normalize --config test-config > mp3ed.raw
+         ./disorder-normalize --config ${srcdir}/test-config > mp3ed.raw
        cmp mp3ed.raw ${top_srcdir}/sounds/scratch-mp3.raw
        rm -f mp3ed.raw
 
@@ -129,19 +130,19 @@ check-mp3: disorder-decode disorder-normalize
 # or something.  Makes it tricky to test!
 check-ogg: disorder-decode disorder-normalize
        ./disorder-decode ${top_srcdir}/sounds/scratch.ogg | \
-         ./disorder-normalize --config test-config > ogged.raw
+         ./disorder-normalize --config ${srcdir}/test-config > ogged.raw
        cmp ogged.raw ${top_srcdir}/sounds/scratch.raw
        rm -f ogged.raw
 
 check-wav: disorder-decode disorder-normalize
        ./disorder-decode ${top_srcdir}/sounds/scratch.wav | \
-         ./disorder-normalize --config test-config > waved.raw
+         ./disorder-normalize --config ${srcdir}/test-config > waved.raw
        cmp waved.raw ${top_srcdir}/sounds/scratch.raw
        rm -rf waved.raw
 
 check-flac: disorder-decode disorder-normalize
        ./disorder-decode ${top_srcdir}/sounds/scratch.flac | \
-         ./disorder-normalize --config test-config > flacced.raw
+         ./disorder-normalize --config ${srcdir}/test-config > flacced.raw
        cmp flacced.raw ${top_srcdir}/sounds/scratch.raw
        rm -f flacced.raw
 
diff --git a/server/decode-flac.c b/server/decode-flac.c
new file mode 100644 (file)
index 0000000..f1399fb
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/decode.c
+ * @brief General-purpose decoder for use by speaker process
+ */
+#include "decode.h"
+#include <FLAC/stream_decoder.h>
+
+/** @brief Metadata callback for FLAC decoder
+ *
+ * This is a no-op here.
+ */
+static void flac_metadata(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                         const FLAC__StreamMetadata attribute((unused)) *metadata,
+                         void attribute((unused)) *client_data) {
+}
+
+/** @brief Error callback for FLAC decoder */
+static void flac_error(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                      FLAC__StreamDecoderErrorStatus status,
+                      void attribute((unused)) *client_data) {
+  disorder_fatal(0, "error decoding %s: %s", path,
+                 FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+/** @brief Write callback for FLAC decoder */
+static FLAC__StreamDecoderWriteStatus flac_write
+    (const FLAC__StreamDecoder attribute((unused)) *decoder,
+     const FLAC__Frame *frame,
+     const FLAC__int32 *const buffer[],
+     void attribute((unused)) *client_data) {
+  size_t n, c;
+
+  output_header(frame->header.sample_rate,
+                frame->header.channels,
+                frame->header.bits_per_sample,
+                (frame->header.channels * frame->header.blocksize
+                 * frame->header.bits_per_sample) / 8,
+                ENDIAN_BIG);
+  for(n = 0; n < frame->header.blocksize; ++n) {
+    for(c = 0; c < frame->header.channels; ++c) {
+      switch(frame->header.bits_per_sample) {
+      case 8: output_8(buffer[c][n]); break;
+      case 16: output_16(buffer[c][n]); break;
+      case 24: output_24(buffer[c][n]); break;
+      case 32: output_32(buffer[c][n]); break;
+      }
+    }
+  }
+  return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+static FLAC__StreamDecoderReadStatus flac_read(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                                               FLAC__byte buffer[],
+                                               size_t *bytes,
+                                               void *client_data) {
+  struct hreader *flacinput = client_data;
+  int n = hreader_read(flacinput, buffer, *bytes);
+  if(n == 0) {
+    *bytes = 0;
+    return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
+  }
+  if(n < 0) {
+    *bytes = 0;
+    return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
+  }
+  *bytes = n;
+  return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
+}
+
+static FLAC__StreamDecoderSeekStatus flac_seek(const FLAC__StreamDecoder attribute((unused)) *decoder,
+                                               FLAC__uint64 absolute_byte_offset, 
+                                               void *client_data) {
+  struct hreader *flacinput = client_data;
+  if(hreader_seek(flacinput, absolute_byte_offset, SEEK_SET) < 0)
+    return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
+  else
+    return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
+}
+
+static FLAC__StreamDecoderTellStatus flac_tell(const FLAC__StreamDecoder attribute((unused)) *decoder, 
+                                               FLAC__uint64 *absolute_byte_offset,
+                                               void *client_data) {
+  struct hreader *flacinput = client_data;
+  off_t offset = hreader_seek(flacinput, 0, SEEK_CUR);
+  if(offset < 0)
+    return FLAC__STREAM_DECODER_TELL_STATUS_ERROR;
+  *absolute_byte_offset = offset;
+  return FLAC__STREAM_DECODER_TELL_STATUS_OK;
+}
+
+static FLAC__StreamDecoderLengthStatus flac_length(const FLAC__StreamDecoder attribute((unused)) *decoder, 
+                                                   FLAC__uint64 *stream_length, 
+                                                   void *client_data) {
+  struct hreader *flacinput = client_data;
+  *stream_length = hreader_size(flacinput);
+  return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
+}
+
+static FLAC__bool flac_eof(const FLAC__StreamDecoder attribute((unused)) *decoder, 
+                           void *client_data) {
+  struct hreader *flacinput = client_data;
+  return hreader_eof(flacinput);
+}
+
+/** @brief FLAC file decoder */
+void decode_flac(void) {
+  FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
+  FLAC__StreamDecoderInitStatus is;
+  struct hreader flacinput[1];
+
+  if (!sd)
+    disorder_fatal(0, "FLAC__stream_decoder_new failed");
+  if(hreader_init(path, flacinput))
+    disorder_fatal(errno, "error opening %s", path);
+
+  if((is = FLAC__stream_decoder_init_stream(sd,
+                                            flac_read,
+                                            flac_seek,
+                                            flac_tell,
+                                            flac_length,
+                                            flac_eof,
+                                            flac_write, flac_metadata,
+                                            flac_error, 
+                                            flacinput)))
+    disorder_fatal(0, "FLAC__stream_decoder_init_stream %s: %s",
+                   path, FLAC__StreamDecoderInitStatusString[is]);
+
+  FLAC__stream_decoder_process_until_end_of_stream(sd);
+  FLAC__stream_decoder_finish(sd);
+  FLAC__stream_decoder_delete(sd);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-mp3.c b/server/decode-mp3.c
new file mode 100644 (file)
index 0000000..6837beb
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/decode-mp3.c
+ * @brief Decode MP3 files.
+ */
+#include "decode.h"
+#include <mad.h>
+
+static struct hreader input[1];
+
+/** @brief Dithering state
+ * Filched from mpg321, which credits it to Robert Leslie */
+struct audio_dither {
+  mad_fixed_t error[3];
+  mad_fixed_t random;
+};
+
+/** @brief 32-bit PRNG
+ * Filched from mpg321, which credits it to Robert Leslie */
+static inline unsigned long prng(unsigned long state)
+{
+  return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
+}
+
+/** @brief Generic linear sample quantize and dither routine
+ * Filched from mpg321, which credits it to Robert Leslie */
+static long audio_linear_dither(mad_fixed_t sample,
+                               struct audio_dither *dither) {
+  unsigned int scalebits;
+  mad_fixed_t output, mask, rnd;
+  const int bits = 16;
+
+  enum {
+    MIN = -MAD_F_ONE,
+    MAX =  MAD_F_ONE - 1
+  };
+
+  /* noise shape */
+  sample += dither->error[0] - dither->error[1] + dither->error[2];
+
+  dither->error[2] = dither->error[1];
+  dither->error[1] = dither->error[0] / 2;
+
+  /* bias */
+  output = sample + (1L << (MAD_F_FRACBITS + 1 - bits - 1));
+
+  scalebits = MAD_F_FRACBITS + 1 - bits;
+  mask = (1L << scalebits) - 1;
+
+  /* dither */
+  rnd  = prng(dither->random);
+  output += (rnd & mask) - (dither->random & mask);
+
+  dither->random = rnd;
+
+  /* clip */
+  if (output > MAX) {
+    output = MAX;
+
+    if (sample > MAX)
+      sample = MAX;
+  }
+  else if (output < MIN) {
+    output = MIN;
+
+    if (sample < MIN)
+      sample = MIN;
+  }
+
+  /* quantize */
+  output &= ~mask;
+
+  /* error feedback */
+  dither->error[0] = sample - output;
+
+  /* scale */
+  return output >> scalebits;
+}
+
+/** @brief MP3 output callback */
+static enum mad_flow mp3_output(void attribute((unused)) *data,
+                               struct mad_header const *header,
+                               struct mad_pcm *pcm) {
+  size_t n = pcm->length;
+  const mad_fixed_t *l = pcm->samples[0], *r = pcm->samples[1];
+  static struct audio_dither ld[1], rd[1];
+
+  output_header(header->samplerate,
+               pcm->channels,
+               16,
+                2 * pcm->channels * pcm->length,
+                ENDIAN_BIG);
+  switch(pcm->channels) {
+  case 1:
+    while(n--)
+      output_16(audio_linear_dither(*l++, ld));
+    break;
+  case 2:
+    while(n--) {
+      output_16(audio_linear_dither(*l++, ld));
+      output_16(audio_linear_dither(*r++, rd));
+    }
+    break;
+  }
+  return MAD_FLOW_CONTINUE;
+}
+
+/** @brief MP3 input callback */
+static enum mad_flow mp3_input(void attribute((unused)) *data,
+                              struct mad_stream *stream) {
+  int used, remain, n;
+
+  /* libmad requires its caller to do ALL the buffering work, including coping
+   * with partial frames.  Given that it appears to be completely undocumented
+   * you could perhaps be forgiven for not discovering this...  */
+  if(input_count) {
+    /* Compute total number of bytes consumed */
+    used = (char *)stream->next_frame - input_buffer;
+    /* Compute number of bytes left to consume */
+    remain = input_count - used;
+    memmove(input_buffer, input_buffer + used, remain);
+  } else {
+    remain = 0;
+  }
+  /* Read new data */
+  n = hreader_read(input,
+                   input_buffer + remain, 
+                   (sizeof input_buffer) - remain);
+  if(n < 0)
+    disorder_fatal(errno, "reading from %s", path);
+  /* Compute total number of bytes available */
+  input_count = remain + n;
+  if(input_count)
+    mad_stream_buffer(stream, (unsigned char *)input_buffer, input_count);
+  if(n)
+    return MAD_FLOW_CONTINUE;
+  else
+    return MAD_FLOW_STOP;
+}
+
+/** @brief MP3 error callback */
+static enum mad_flow mp3_error(void attribute((unused)) *data,
+                              struct mad_stream *stream,
+                              struct mad_frame attribute((unused)) *frame) {
+  if(0)
+    /* Just generates pointless verbosity l-( */
+    disorder_error(0, "decoding %s: %s (%#04x)",
+                   path, mad_stream_errorstr(stream), stream->error);
+  return MAD_FLOW_CONTINUE;
+}
+
+/** @brief MP3 decoder */
+void decode_mp3(void) {
+  struct mad_decoder mad[1];
+
+  if(hreader_init(path, input))
+    disorder_fatal(errno, "opening %s", path);
+  mad_decoder_init(mad, 0/*data*/, mp3_input, 0/*header*/, 0/*filter*/,
+                  mp3_output, mp3_error, 0/*message*/);
+  if(mad_decoder_run(mad, MAD_DECODER_MODE_SYNC))
+    exit(1);
+  mad_decoder_finish(mad);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-ogg.c b/server/decode-ogg.c
new file mode 100644 (file)
index 0000000..d499955
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/decode.c
+ * @brief General-purpose decoder for use by speaker process
+ */
+#include "decode.h"
+
+#include <vorbis/vorbisfile.h>
+
+static size_t ogg_read_func(void *ptr, size_t size, size_t nmemb, void *datasource) {
+  struct hreader *h = datasource;
+  
+  int n = hreader_read(h, ptr, size * nmemb);
+  if(n < 0) n = 0;
+  return n / size;
+}
+
+static int ogg_seek_func(void *datasource, ogg_int64_t offset, int whence) {
+  struct hreader *h = datasource;
+  
+  return hreader_seek(h, offset, whence) < 0 ? -1 : 0;
+}
+
+static int ogg_close_func(void attribute((unused)) *datasource) {
+  return 0;
+}
+
+static long ogg_tell_func(void *datasource) {
+  struct hreader *h = datasource;
+  
+  return hreader_seek(h, 0, SEEK_CUR);
+}
+
+static const ov_callbacks ogg_callbacks = {
+  ogg_read_func,
+  ogg_seek_func,
+  ogg_close_func,
+  ogg_tell_func,
+};
+
+/** @brief OGG decoder */
+void decode_ogg(void) {
+  struct hreader ogginput[1];
+  OggVorbis_File vf[1];
+  int err;
+  long n;
+  int bitstream;
+  vorbis_info *vi;
+
+  hreader_init(path, ogginput);
+  /* There doesn't seem to be any standard function for mapping the error codes
+   * to strings l-( */
+  if((err = ov_open_callbacks(ogginput, vf, 0/*initial*/, 0/*ibytes*/,
+                              ogg_callbacks)))
+    disorder_fatal(0, "ov_open_callbacks %s: %d", path, err);
+  if(!(vi = ov_info(vf, 0/*link*/)))
+    disorder_fatal(0, "ov_info %s: failed", path);
+  while((n = ov_read(vf, input_buffer, sizeof input_buffer, 1/*bigendianp*/,
+                     2/*bytes/word*/, 1/*signed*/, &bitstream))) {
+    if(n < 0)
+      disorder_fatal(0, "ov_read %s: %ld", path, n);
+    if(bitstream > 0)
+      disorder_fatal(0, "only single-bitstream ogg files are supported");
+    output_header(vi->rate, vi->channels, 16/*bits*/, n, ENDIAN_BIG);
+    if(fwrite(input_buffer, 1, n, outputfp) < (size_t)n)
+      disorder_fatal(errno, "decoding %s: writing sample data", path);
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-wav.c b/server/decode-wav.c
new file mode 100644 (file)
index 0000000..fd58a14
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/decode.c
+ * @brief General-purpose decoder for use by speaker process
+ */
+#include "decode.h"
+#include "wav.h"
+
+/** @brief Sample data callback used by decode_wav() */
+static int wav_write(struct wavfile attribute((unused)) *f,
+                     const char *data,
+                     size_t nbytes,
+                     void attribute((unused)) *u) {
+  if(fwrite(data, 1, nbytes, outputfp) < nbytes)
+    disorder_fatal(errno, "decoding %s: writing sample data", path);
+  return 0;
+}
+
+/** @brief WAV file decoder */
+void decode_wav(void) {
+  struct wavfile f[1];
+  int err;
+
+  if((err = wav_init(f, path)))
+    disorder_fatal(err, "opening %s", path);
+  output_header(f->rate, f->channels, f->bits, f->datasize, ENDIAN_LITTLE);
+  if((err = wav_data(f, wav_write, 0)))
+    disorder_fatal(err, "error decoding %s", path);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 11a05928038f86658dd8a2ef129493eee7246bc2..8a09013b23666533acbe299859cd128c9fa43ff3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2007-2009 Richard Kettlewell
+ * Copyright (C) 2007-2010 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 /** @file server/decode.c
  * @brief General-purpose decoder for use by speaker process
  */
-
-#include "disorder-server.h"
+#include "decode.h"
 
 #include <mad.h>
 #include <vorbis/vorbisfile.h>
 
-/* libFLAC has had an API change and stupidly taken away the old API */
-#if HAVE_FLAC_FILE_DECODER_H
-# include <FLAC/file_decoder.h>
-#else
-# include <FLAC/stream_decoder.h>
-#define FLAC__FileDecoder FLAC__StreamDecoder
-#define FLAC__FileDecoderState FLAC__StreamDecoderState
-#endif
+#include <FLAC/stream_decoder.h>
 
 #include "wav.h"
-#include "speaker-protocol.h"
 
 
 /** @brief Encoding lookup table type */
@@ -45,50 +36,10 @@ struct decoder {
   void (*decode)(void);
 };
 
-/** @brief Input file */
-static int inputfd;
-
-/** @brief Output file */
-static FILE *outputfp;
-
-/** @brief Filename */
-static const char *path;
-
-/** @brief Input buffer */
-static char input_buffer[1048576];
-
-/** @brief Number of bytes read into buffer */
-static int input_count;
-
-/** @brief Write an 8-bit word */
-static inline void output_8(int n) {
-  if(putc(n, outputfp) < 0)
-    disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 16-bit word in bigendian format */
-static inline void output_16(uint16_t n) {
-  if(putc(n >> 8, outputfp) < 0
-     || putc(n, outputfp) < 0)
-    disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 24-bit word in bigendian format */
-static inline void output_24(uint32_t n) {
-  if(putc(n >> 16, outputfp) < 0
-     || putc(n >> 8, outputfp) < 0
-     || putc(n, outputfp) < 0)
-    disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 32-bit word in bigendian format */
-static inline void output_32(uint32_t n) {
-  if(putc(n >> 24, outputfp) < 0
-     || putc(n >> 16, outputfp) < 0
-     || putc(n >> 8, outputfp) < 0
-     || putc(n, outputfp) < 0)
-    disorder_fatal(errno, "decoding %s: output error", path);
-}
+FILE *outputfp;
+const char *path;
+char input_buffer[INPUT_BUFFER_SIZE];
+int input_count;
 
 /** @brief Write a block header
  * @param rate Sample rate in Hz
@@ -100,11 +51,11 @@ static inline void output_32(uint32_t n) {
  * Checks that the sample format is a supported one (so other calls do not have
  * to) and calls disorder_fatal() on error.
  */
-static void output_header(int rate,
-                         int channels,
-                         int bits,
-                          int nbytes,
-                          int endian) {
+void output_header(int rate,
+                   int channels,
+                   int bits,
+                   int nbytes,
+                   int endian) {
   struct stream_header header;
 
   if(bits <= 0 || bits % 8 || bits > 64)
@@ -124,288 +75,6 @@ static void output_header(int rate,
     disorder_fatal(errno, "decoding %s: writing format header", path);
 }
 
-/** @brief Dithering state
- * Filched from mpg321, which credits it to Robert Leslie */
-struct audio_dither {
-  mad_fixed_t error[3];
-  mad_fixed_t random;
-};
-
-/** @brief 32-bit PRNG
- * Filched from mpg321, which credits it to Robert Leslie */
-static inline unsigned long prng(unsigned long state)
-{
-  return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
-}
-
-/** @brief Generic linear sample quantize and dither routine
- * Filched from mpg321, which credits it to Robert Leslie */
-static long audio_linear_dither(mad_fixed_t sample,
-                               struct audio_dither *dither) {
-  unsigned int scalebits;
-  mad_fixed_t output, mask, rnd;
-  const int bits = 16;
-
-  enum {
-    MIN = -MAD_F_ONE,
-    MAX =  MAD_F_ONE - 1
-  };
-
-  /* noise shape */
-  sample += dither->error[0] - dither->error[1] + dither->error[2];
-
-  dither->error[2] = dither->error[1];
-  dither->error[1] = dither->error[0] / 2;
-
-  /* bias */
-  output = sample + (1L << (MAD_F_FRACBITS + 1 - bits - 1));
-
-  scalebits = MAD_F_FRACBITS + 1 - bits;
-  mask = (1L << scalebits) - 1;
-
-  /* dither */
-  rnd  = prng(dither->random);
-  output += (rnd & mask) - (dither->random & mask);
-
-  dither->random = rnd;
-
-  /* clip */
-  if (output > MAX) {
-    output = MAX;
-
-    if (sample > MAX)
-      sample = MAX;
-  }
-  else if (output < MIN) {
-    output = MIN;
-
-    if (sample < MIN)
-      sample = MIN;
-  }
-
-  /* quantize */
-  output &= ~mask;
-
-  /* error feedback */
-  dither->error[0] = sample - output;
-
-  /* scale */
-  return output >> scalebits;
-}
-
-/** @brief MP3 output callback */
-static enum mad_flow mp3_output(void attribute((unused)) *data,
-                               struct mad_header const *header,
-                               struct mad_pcm *pcm) {
-  size_t n = pcm->length;
-  const mad_fixed_t *l = pcm->samples[0], *r = pcm->samples[1];
-  static struct audio_dither ld[1], rd[1];
-
-  output_header(header->samplerate,
-               pcm->channels,
-               16,
-                2 * pcm->channels * pcm->length,
-                ENDIAN_BIG);
-  switch(pcm->channels) {
-  case 1:
-    while(n--)
-      output_16(audio_linear_dither(*l++, ld));
-    break;
-  case 2:
-    while(n--) {
-      output_16(audio_linear_dither(*l++, ld));
-      output_16(audio_linear_dither(*r++, rd));
-    }
-    break;
-  }
-  return MAD_FLOW_CONTINUE;
-}
-
-/** @brief MP3 input callback */
-static enum mad_flow mp3_input(void attribute((unused)) *data,
-                              struct mad_stream *stream) {
-  int used, remain, n;
-
-  /* libmad requires its caller to do ALL the buffering work, including coping
-   * with partial frames.  Given that it appears to be completely undocumented
-   * you could perhaps be forgiven for not discovering this...  */
-  if(input_count) {
-    /* Compute total number of bytes consumed */
-    used = (char *)stream->next_frame - input_buffer;
-    /* Compute number of bytes left to consume */
-    remain = input_count - used;
-    memmove(input_buffer, input_buffer + used, remain);
-  } else {
-    remain = 0;
-  }
-  /* Read new data */
-  n = read(inputfd, input_buffer + remain, (sizeof input_buffer) - remain);
-  if(n < 0)
-    disorder_fatal(errno, "reading from %s", path);
-  /* Compute total number of bytes available */
-  input_count = remain + n;
-  if(input_count)
-    mad_stream_buffer(stream, (unsigned char *)input_buffer, input_count);
-  if(n)
-    return MAD_FLOW_CONTINUE;
-  else
-    return MAD_FLOW_STOP;
-}
-
-/** @brief MP3 error callback */
-static enum mad_flow mp3_error(void attribute((unused)) *data,
-                              struct mad_stream *stream,
-                              struct mad_frame attribute((unused)) *frame) {
-  if(0)
-    /* Just generates pointless verbosity l-( */
-    disorder_error(0, "decoding %s: %s (%#04x)",
-                   path, mad_stream_errorstr(stream), stream->error);
-  return MAD_FLOW_CONTINUE;
-}
-
-/** @brief MP3 decoder */
-static void decode_mp3(void) {
-  struct mad_decoder mad[1];
-
-  if((inputfd = open(path, O_RDONLY)) < 0)
-    disorder_fatal(errno, "opening %s", path);
-  mad_decoder_init(mad, 0/*data*/, mp3_input, 0/*header*/, 0/*filter*/,
-                  mp3_output, mp3_error, 0/*message*/);
-  if(mad_decoder_run(mad, MAD_DECODER_MODE_SYNC))
-    exit(1);
-  mad_decoder_finish(mad);
-}
-
-/** @brief OGG decoder */
-static void decode_ogg(void) {
-  FILE *fp;
-  OggVorbis_File vf[1];
-  int err;
-  long n;
-  int bitstream;
-  vorbis_info *vi;
-
-  if(!(fp = fopen(path, "rb")))
-    disorder_fatal(errno, "cannot open %s", path);
-  /* There doesn't seem to be any standard function for mapping the error codes
-   * to strings l-( */
-  if((err = ov_open(fp, vf, 0/*initial*/, 0/*ibytes*/)))
-    disorder_fatal(0, "ov_fopen %s: %d", path, err);
-  if(!(vi = ov_info(vf, 0/*link*/)))
-    disorder_fatal(0, "ov_info %s: failed", path);
-  while((n = ov_read(vf, input_buffer, sizeof input_buffer, 1/*bigendianp*/,
-                     2/*bytes/word*/, 1/*signed*/, &bitstream))) {
-    if(n < 0)
-      disorder_fatal(0, "ov_read %s: %ld", path, n);
-    if(bitstream > 0)
-      disorder_fatal(0, "only single-bitstream ogg files are supported");
-    output_header(vi->rate, vi->channels, 16/*bits*/, n, ENDIAN_BIG);
-    if(fwrite(input_buffer, 1, n, outputfp) < (size_t)n)
-      disorder_fatal(errno, "decoding %s: writing sample data", path);
-  }
-}
-
-/** @brief Sample data callback used by decode_wav() */
-static int wav_write(struct wavfile attribute((unused)) *f,
-                     const char *data,
-                     size_t nbytes,
-                     void attribute((unused)) *u) {
-  if(fwrite(data, 1, nbytes, outputfp) < nbytes)
-    disorder_fatal(errno, "decoding %s: writing sample data", path);
-  return 0;
-}
-
-/** @brief WAV file decoder */
-static void decode_wav(void) {
-  struct wavfile f[1];
-  int err;
-
-  if((err = wav_init(f, path)))
-    disorder_fatal(err, "opening %s", path);
-  output_header(f->rate, f->channels, f->bits, f->datasize, ENDIAN_LITTLE);
-  if((err = wav_data(f, wav_write, 0)))
-    disorder_fatal(err, "error decoding %s", path);
-}
-
-/** @brief Metadata callback for FLAC decoder
- *
- * This is a no-op here.
- */
-static void flac_metadata(const FLAC__FileDecoder attribute((unused)) *decoder,
-                         const FLAC__StreamMetadata attribute((unused)) *metadata,
-                         void attribute((unused)) *client_data) {
-}
-
-/** @brief Error callback for FLAC decoder */
-static void flac_error(const FLAC__FileDecoder attribute((unused)) *decoder,
-                      FLAC__StreamDecoderErrorStatus status,
-                      void attribute((unused)) *client_data) {
-  disorder_fatal(0, "error decoding %s: %s", path,
-                 FLAC__StreamDecoderErrorStatusString[status]);
-}
-
-/** @brief Write callback for FLAC decoder */
-static FLAC__StreamDecoderWriteStatus flac_write
-    (const FLAC__FileDecoder attribute((unused)) *decoder,
-     const FLAC__Frame *frame,
-     const FLAC__int32 *const buffer[],
-     void attribute((unused)) *client_data) {
-  size_t n, c;
-
-  output_header(frame->header.sample_rate,
-                frame->header.channels,
-                frame->header.bits_per_sample,
-                (frame->header.channels * frame->header.blocksize
-                 * frame->header.bits_per_sample) / 8,
-                ENDIAN_BIG);
-  for(n = 0; n < frame->header.blocksize; ++n) {
-    for(c = 0; c < frame->header.channels; ++c) {
-      switch(frame->header.bits_per_sample) {
-      case 8: output_8(buffer[c][n]); break;
-      case 16: output_16(buffer[c][n]); break;
-      case 24: output_24(buffer[c][n]); break;
-      case 32: output_32(buffer[c][n]); break;
-      }
-    }
-  }
-  return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
-}
-
-
-/** @brief FLAC file decoder */
-static void decode_flac(void) {
-#if HAVE_FLAC_FILE_DECODER_H
-  FLAC__FileDecoder *fd = 0;
-  FLAC__FileDecoderState fs;
-
-  if(!(fd = FLAC__file_decoder_new()))
-    disorder_fatal(0, "FLAC__file_decoder_new failed");
-  if(!(FLAC__file_decoder_set_filename(fd, path)))
-    disorder_fatal(0, "FLAC__file_set_filename failed");
-  FLAC__file_decoder_set_metadata_callback(fd, flac_metadata);
-  FLAC__file_decoder_set_error_callback(fd, flac_error);
-  FLAC__file_decoder_set_write_callback(fd, flac_write);
-  if((fs = FLAC__file_decoder_init(fd)))
-    disorder_fatal(0, "FLAC__file_decoder_init: %s", FLAC__FileDecoderStateString[fs]);
-  FLAC__file_decoder_process_until_end_of_file(fd);
-#else
-  FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
-  FLAC__StreamDecoderInitStatus is;
-
-  if (!sd)
-    disorder_fatal(0, "FLAC__stream_decoder_new failed");
-
-  if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
-                                          flac_error, 0)))
-    disorder_fatal(0, "FLAC__stream_decoder_init_file %s: %s",
-                   path, FLAC__StreamDecoderInitStatusString[is]);
-
-  FLAC__stream_decoder_process_until_end_of_stream(sd);
-  FLAC__stream_decoder_finish(sd);
-  FLAC__stream_decoder_delete(sd);
-#endif
-}
-
 /** @brief Lookup table of decoders */
 static const struct decoder decoders[] = {
   { "*.mp3", decode_mp3 },
diff --git a/server/decode.h b/server/decode.h
new file mode 100644 (file)
index 0000000..97bf687
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/decode.h
+ * @brief General-purpose decoder for use by speaker process
+ */
+#ifndef DECODE_H
+#define DECODE_H
+
+#include "disorder-server.h"
+#include "hreader.h"
+#include "speaker-protocol.h"
+
+#define INPUT_BUFFER_SIZE 1048576
+  
+/** @brief Output file */
+extern FILE *outputfp;
+
+/** @brief Input filename */
+extern const char *path;
+
+/** @brief Input buffer */
+extern char input_buffer[INPUT_BUFFER_SIZE];
+
+/** @brief Number of bytes read into buffer */
+extern int input_count;
+
+/** @brief Write an 8-bit word */
+static inline void output_8(int n) {
+  if(putc(n, outputfp) < 0)
+    disorder_fatal(errno, "decoding %s: output error", path);
+}
+
+/** @brief Write a 16-bit word in bigendian format */
+static inline void output_16(uint16_t n) {
+  if(putc(n >> 8, outputfp) < 0
+     || putc(n, outputfp) < 0)
+    disorder_fatal(errno, "decoding %s: output error", path);
+}
+
+/** @brief Write a 24-bit word in bigendian format */
+static inline void output_24(uint32_t n) {
+  if(putc(n >> 16, outputfp) < 0
+     || putc(n >> 8, outputfp) < 0
+     || putc(n, outputfp) < 0)
+    disorder_fatal(errno, "decoding %s: output error", path);
+}
+
+/** @brief Write a 32-bit word in bigendian format */
+static inline void output_32(uint32_t n) {
+  if(putc(n >> 24, outputfp) < 0
+     || putc(n >> 16, outputfp) < 0
+     || putc(n >> 8, outputfp) < 0
+     || putc(n, outputfp) < 0)
+    disorder_fatal(errno, "decoding %s: output error", path);
+}
+
+void output_header(int rate,
+                   int channels,
+                   int bits,
+                   int nbytes,
+                   int endian);
+
+void decode_mp3(void);
+void decode_ogg(void);
+void decode_wav(void);
+void decode_flac(void);
+
+#endif /* DECODE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index f980b6f3ea9a1ab1aabbe171dc4d466c996ee58e..7cd1cf6d85e294ccfda9b5778782b05afd046e86 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2008, 2009 Richard Kettlewell
+ * Copyright (C) 2008-2010 Richard Kettlewell
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -140,6 +140,7 @@ struct queue_entry *queue_add(const char *track, const char *submitter,
 #define WHERE_END 1                    /* Add to end of queue */
 #define WHERE_BEFORE_RANDOM 2          /* End, or before random track */
 #define WHERE_AFTER 3                   /* After the target */
+#define WHERE_NOWHERE 4                 /* Don't add to queue at all */
 /* add an entry to the queue.  Return a pointer to the new entry. */
 
 void queue_remove(struct queue_entry *q, const char *who);
@@ -371,6 +372,18 @@ int play_background(ev_source *ev,
 #define START_HARDFAIL 1   /**< @brief Track is broken. */
 #define START_SOFTFAIL 2   /**< @brief Track OK, system (temporarily?) broken */
 
+void periodic_mount_check(ev_source *ev_);
+
+#ifndef MOUNT_CHECK_INTERVAL
+# ifdef PATH_MTAB
+// statting a file is really cheap so check once a second
+#  define MOUNT_CHECK_INTERVAL 1
+# else
+// hashing getfsstat() output could be more expensive so be less aggressive
+#  define MOUNT_CHECK_INTERVAL 5
+# endif
+#endif
+
 #endif /* DISORDER_SERVER_H */
 
 /*
index 14b459f861524a7d66d9ff9b8ded1fc2c59f6403..2000a2f2d55aefafb732f30346605d7df2259da9 100644 (file)
@@ -302,6 +302,8 @@ int main(int argc, char **argv) {
   create_periodic(ev, periodic_play_check, 1, 0);
   /* Try adding a random track immediately and once every two seconds */
   create_periodic(ev, periodic_add_random, 2, 1);
+  /* Issue a rescan when devices are mounted or unmouted */
+  create_periodic(ev, periodic_mount_check, MOUNT_CHECK_INTERVAL, 1);
   /* enter the event loop */
   n = ev_run(ev);
   /* if we exit the event loop, something must have gone wrong */
index cdc3a44fc72a57e70eb1f60d823afda31a9e209e..fb0741503808e261ad532481ba32c08a23564017 100644 (file)
@@ -378,8 +378,8 @@ int main(int argc, char **argv) {
     case 'd': dump = 1; break;
     case 'u': undump = 1; break;
     case 'D': debugging = 1; break;
-    case 'r': recover = TRACKDB_NORMAL_RECOVER;
-    case 'R': recover = TRACKDB_FATAL_RECOVER;
+    case 'r': recover = TRACKDB_NORMAL_RECOVER; break;
+    case 'R': recover = TRACKDB_FATAL_RECOVER; break;
     case 'a': recompute = 1; break;
     case 'P': remove_pathless = 1; break;
     default: disorder_fatal(0, "invalid option");
diff --git a/server/mount.c b/server/mount.c
new file mode 100644 (file)
index 0000000..8b1c602
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/mount.c
+ * @brief Periodically check for devices being mounted and unmounted
+ */
+#include "disorder-server.h"
+#if HAVE_GETFSSTAT
+# include <sys/param.h>
+# include <sys/ucred.h>
+# include <sys/mount.h>
+#endif
+
+#if HAVE_GETFSSTAT
+static int compare_fsstat(const void *av, const void *bv) {
+  const struct statfs *a = av, *b = bv;
+  int c;
+  c = memcmp(&a->f_fsid, &b->f_fsid, sizeof a->f_fsid);
+  if(c)
+    return c;
+  c = strcmp(a->f_mntonname, b->f_mntonname);
+  if(c)
+    return c;
+  return 0;
+}
+#endif
+
+void periodic_mount_check(ev_source *ev_) {
+  if(!config->mount_rescan)
+    return;
+#if HAVE_GETFSSTAT
+  /* On OS X, we keep track of the hash of the kernel's mounted
+   * filesystem list */
+  static int first = 1;
+  static unsigned char last[20];
+  unsigned char *current;
+  int nfilesystems, space;
+  struct statfs *buf;
+  gcrypt_hash_handle h;
+  gcry_error_t e;
+
+  space = getfsstat(NULL, 0, MNT_NOWAIT);
+  buf = xcalloc(space, sizeof *buf);
+  nfilesystems = getfsstat(buf, space * sizeof *buf, MNT_NOWAIT);
+  if(nfilesystems > space)
+    // The array grew between check and use!  We just give up and try later.
+    return;
+  // Put into order so we get a bit of consistency
+  qsort(buf, nfilesystems, sizeof *buf, compare_fsstat);
+  if((e = gcry_md_open(&h, GCRY_MD_SHA1, 0))) {
+    disorder_error(0, "gcry_md_open: %s", gcry_strerror(e));
+    return;
+  }
+  for(int n = 0; n < nfilesystems; ++n) {
+    gcry_md_write(h, &buf[n].f_fsid, sizeof buf[n].f_fsid);
+    gcry_md_write(h, buf[n].f_mntonname, 1 + strlen(buf[n].f_mntonname));
+  }
+  current = gcry_md_read(h, GCRY_MD_SHA1);
+  if(!first && memcmp(current, last, sizeof last))
+    trackdb_rescan(ev_, 1/*check*/, 0, 0);
+  memcpy(last, current, sizeof last);
+  first = 0;
+  gcry_md_close(h);
+#elif defined PATH_MTAB
+  /* On Linux we keep track of the modification time of /etc/mtab */
+  static time_t last_mount;
+  struct stat sb;
+  
+  if(stat(PATH_MTAB, &sb) >= 0) {
+    if(last_mount != 0 && last_mount != sb.st_mtime)
+      trackdb_rescan(ev_, 1/*check*/, 0, 0);
+    last_mount = sb.st_mtime;
+  }
+#endif
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index bcfc3a8847b311c2788f19b03eb480b80b81d368..0f97a2ff024a9d3360526af306828b3ad7767630 100644 (file)
@@ -188,6 +188,8 @@ int main(int argc, char attribute((unused)) **argv) {
     }
     if(!n)
       break;
+    D(("NEW HEADER: %"PRIu32" bytes %"PRIu32"Hz %"PRIu8" channels %"PRIu8" bits %"PRIu8" endian",
+       header.nbytes, header.rate, header.channels, header.bits, header.endian));
     /* Sanity check the header */
     if(header.rate < 100 || header.rate > 1000000)
       disorder_fatal(0, "implausible rate %"PRId32"Hz (%#"PRIx32")",
@@ -210,7 +212,8 @@ int main(int argc, char attribute((unused)) **argv) {
     else {
       /* If we have a resampler active already check it is suitable and destroy
        * it if not */
-      if(!formats_equal(&header, &latest_format) && rs_in_use) {
+      if(rs_in_use) {
+        D(("call resample_close"));
         resample_close(rs);
         rs_in_use = 0;
       }
@@ -227,6 +230,7 @@ int main(int argc, char attribute((unused)) **argv) {
              config->sample_format.endian);*/
       if(!rs_in_use) {
         /* Create a suitable resampler. */
+        D(("call resample_init"));
         resample_init(rs,
                       header.bits,
                       header.channels, 
@@ -260,17 +264,20 @@ int main(int argc, char attribute((unused)) **argv) {
           left -= r;
           used += r;
           //syslog(LOG_INFO, "read %zd bytes", r);
+          D(("read %zd bytes", r));
         }
         /*syslog(LOG_INFO, " in: %02x %02x %02x %02x",
                (uint8_t)buffer[0],
                (uint8_t)buffer[1], 
                (uint8_t)buffer[2],
                (uint8_t)buffer[3]);*/
+        D(("calling resample_convert used=%zu !left=%d", used, !left));
         const size_t consumed = resample_convert(rs,
                                                  (uint8_t *)buffer, used,
                                                  !left,
                                                  converted, 0);
         //syslog(LOG_INFO, "used=%zu consumed=%zu", used, consumed);
+        D(("consumed=%zu", consumed));
         memmove(buffer, buffer + consumed, used - consumed);
         used -= consumed;
       }
index f93fd5a8707d96de6af9558fbd08ed7e088b5a0f..8406238d52f8865fe62047725ce05d87ec886d8d 100644 (file)
@@ -39,6 +39,7 @@ static int start_child(struct queue_entry *q,
 static int prepare_child(struct queue_entry *q, 
                          const struct pbgc_params *params,
                          void attribute((unused)) *bgdata);
+static void ensure_next_scratch(ev_source *ev);
 
 /** @brief File descriptor of our end of the socket to the speaker */
 static int speaker_fd = -1;
@@ -82,14 +83,32 @@ static int speaker_readable(ev_source *ev, int fd,
   case SM_FINISHED:                    /* scratched the playing track */
   case SM_STILLBORN:                   /* scratched too early */
   case SM_UNKNOWN:                     /* scratched WAY too early */
-    if(playing && !strcmp(sm.id, playing->id))
+    if(playing && !strcmp(sm.id, playing->id)) {
+      if((playing->state == playing_unplayed
+          || playing->state == playing_started)
+         && sm.type == SM_FINISHED)
+        playing->state = playing_ok;
       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;
+  case SM_ARRIVED: {
+    /* track ID is now prepared */
+    struct queue_entry *q;
+    for(q = qhead.next; q != &qhead && strcmp(q->id, sm.id); q = q->next)
+      ;
+    if(q && q->preparing) {
+      q->preparing = 0;
+      q->prepared = 1;
+      /* We might be waiting to play the now-prepared track */
+      play(ev);
+    }
+    break;
+  }
   default:
     disorder_error(0, "unknown speaker message type %d", sm.type);
   }
@@ -195,7 +214,9 @@ static void finished(ev_source *ev) {
  * some time before the speaker reports it as finished) or when a non-raw
  * (i.e. non-speaker) player terminates.  In the latter case it's imaginable
  * that the OS has buffered the last few samples.
- * 
+ *
+ * NB.  The finished track might NOT be in the queue (yet) - it might be a
+ * pre-chosen scratch.
  */
 static int player_finished(ev_source *ev,
                           pid_t pid,
@@ -279,7 +300,7 @@ static int start(ev_source *ev,
 
   D(("start %s", q->id));
   /* Find the player plugin. */
-  if(!(player = find_player(q)) < 0)
+  if(!(player = find_player(q)))
     return START_HARDFAIL;              /* No player */
   if(!(q->pl = open_plugin(player->s[1], 0)))
     return START_HARDFAIL;
@@ -371,19 +392,21 @@ int prepare(ev_source *ev,
   if(q->pid >= 0)
     return START_OK;
   /* If the track is already prepared, do nothing */
-  if(q->prepared)
+  if(q->prepared || q->preparing)
     return START_OK;
   /* Find the player plugin */
-  if(!(player = find_player(q)) < 0
+  if(!(player = find_player(q))) 
     return START_HARDFAIL;              /* No player */
   q->pl = open_plugin(player->s[1], 0);
   q->type = play_get_type(q->pl);
   if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
     return START_OK;                    /* Not a raw player */
-  const int rc = play_background(ev, player, q, prepare_child, NULL);
+  int rc = play_background(ev, player, q, prepare_child, NULL);
   if(rc == START_OK) {
     ev_child(ev, q->pid, 0, player_finished, q);
-    q->prepared = 1;
+    q->preparing = 1;
+    /* Actually the track is still "in flight" */
+    rc = START_SOFTFAIL;
   }
   return rc;
 }
@@ -609,6 +632,8 @@ void play(ev_source *ev) {
      * potentially be a just-added random track. */
     if(qhead.next != &qhead)
       prepare(ev, qhead.next);
+    /* Make sure there is a prepared scratch */
+    ensure_next_scratch(ev);
     break;
   }
 }
@@ -656,12 +681,27 @@ void disable_random(const char *who) {
 
 /* Scratching --------------------------------------------------------------- */
 
+/** @brief Track to play next time something is scratched */
+static struct queue_entry *next_scratch;
+
+/** @brief Ensure there isa prepared scratch */
+static void ensure_next_scratch(ev_source *ev) {
+  if(next_scratch)                      /* There's one already */
+    return;
+  if(!config->scratch.n)                /* There are no scratches */
+    return;
+  int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+  next_scratch = queue_add(config->scratch.s[r], NULL,
+                           WHERE_NOWHERE, NULL, origin_scratch);
+  if(ev)
+    prepare(ev, next_scratch);
+}
+
 /** @brief Scratch a track
  * @param who User responsible (or NULL)
  * @param id Track ID (or NULL for current)
  */
 void scratch(const char *who, const char *id) {
-  struct queue_entry *q;
   struct speaker_message sm;
 
   D(("scratch playing=%p state=%d id=%s playing->id=%s",
@@ -692,12 +732,20 @@ void scratch(const char *who, const char *id) {
       speaker_send(speaker_fd, &sm);
       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, NULL, 
-                    origin_scratch);
+    /* If playing is enabled then add a scratch to the queue.  Having a scratch
+     * appear in the queue when further play is disabled is weird and
+     * contradicts implicit assumptions made elsewhere, so we try to avoid
+     * it. */
+    if(playing_is_enabled()) {
+      /* Try to make sure there is a scratch */
+      ensure_next_scratch(NULL);
+      /* Insert it at the head of the queue */
+      if(next_scratch){
+        next_scratch->submitter = who;
+        queue_insert_entry(&qhead, next_scratch);
+        eventlog_raw("queue", queue_marshall(next_scratch), (const char *)0);
+        next_scratch = NULL;
+      }
     }
     notify_scratch(playing->track, playing->submitter, who,
                   xtime(0) - playing->played);
index e513d164b61410e30a7efe3ed332ec6b645b0bda..7dcaa845bef920505f6b29979b1659bbac5f3367 100644 (file)
@@ -104,6 +104,8 @@ struct queue_entry *queue_add(const char *track, const char *submitter,
     }
     queue_insert_entry(afterme, q);
     break;
+  case WHERE_NOWHERE:
+    return q;
   }
   /* submitter will be a null pointer for a scratch */
   if(submitter)
index bc99681db8297a40d42c69a7116f7276e0a844f9..7cb24b6a3c1ef75538916a8519cf37698e3a562f 100644 (file)
@@ -160,7 +160,7 @@ done:
   if(fp)
     xfclose(fp);
   if(pid)
-    while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+    while((waitpid(pid, &w, 0)) == -1 && errno == EINTR)
       ;
 }
 
index f1b20a123d96433188d70bef0286f60fcf8406c5..c7b4eede3be0691d5b444cc57453bbbf06873b57 100644 (file)
@@ -471,14 +471,12 @@ static int schedule_lookup(const char *id,
 static int schedule_trigger(ev_source *ev,
                            const struct timeval attribute((unused)) *now,
                            void *u) {
-  const char *action, *id = u;
+  const char *id = u;
   struct kvp *actiondata = schedule_get(id);
   int n;
 
   if(!actiondata)
     return 0;
-  /* schedule_get() enforces these being present */
-  action = kvp_get(actiondata, "action");
   /* Look up the action */
   n = schedule_lookup(id, actiondata);
   if(n < 0)
index 858edbc9d2da531516903aa3a77bd2ac08170cd5..4dafabbbcf380ac5756ba5c23ddf687a398c96f9 100644 (file)
@@ -1177,7 +1177,7 @@ static int c_nop(struct conn *c,
 static int c_new(struct conn *c,
                 char **vec,
                 int nvec) {
-  int max, n;
+  int max;
   char **tracks;
 
   if(nvec > 0)
@@ -1188,7 +1188,6 @@ static int c_new(struct conn *c,
     max = config->new_max;
   tracks = trackdb_new(0, max);
   sink_printf(ev_writer_sink(c->w), "253 New track list follows\n");
-  n = 0;
   while(*tracks) {
     sink_printf(ev_writer_sink(c->w), "%s%s\n",
                **tracks == '.' ? "." : "", *tracks);
index de5692b9fe7b91e5b24453e867dddc244a2e8234..3af36aaac22289ab6616f0a63282f2c6ffe091a5 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2005-2009 Richard Kettlewell
+ * Copyright (C) 2005-2010 Richard Kettlewell
  * Portions (C) 2007 Mark Wooding
  *
  * This program is free software: you can redistribute it and/or modify
@@ -316,12 +316,16 @@ static int speaker_fill(struct track *t) {
       n = read(t->fd, t->buffer + where, left);
     } while(n < 0 && errno == EINTR);
     pthread_mutex_lock(&lock);
-    if(n < 0) {
-      if(errno != EAGAIN)
-        disorder_fatal(errno, "error reading sample stream");
+    if(n < 0 && errno == EAGAIN) {
+      /* EAGAIN means more later */
       rc = 0;
-    } else if(n == 0) {
-      D(("fill %s: eof detected", t->id));
+    } else if(n <= 0) {
+      /* n=0 means EOF.  n<0 means some error occurred.  We log the error but
+       * otherwise treat it as identical to EOF. */
+      if(n < 0)
+        disorder_error(errno, "error reading sample stream for %s", t->id);
+      else
+        D(("fill %s: eof detected", t->id));
       t->eof = 1;
       /* A track always becomes playable at EOF; we're not going to see any
        * more data. */
@@ -336,7 +340,8 @@ static int speaker_fill(struct track *t) {
         t->playable = 1;
       rc = 0;
     }
-  }
+  } else
+    rc = 0;
   return rc;
 }
 
@@ -521,7 +526,8 @@ static void mainloop(void) {
           D(("id %s fd %d", id, fd));
           t = findtrack(id, 1/*create*/);
           if (write(fd, "", 1) < 0)             /* write an ack */
-            disorder_error(errno, "writing ack to inbound connection");
+            disorder_error(errno, "writing ack to inbound connection for %s",
+                           id);
           if(t->fd != -1) {
             disorder_error(0, "%s: already got a connection", id);
             xclose(fd);
@@ -529,6 +535,10 @@ static void mainloop(void) {
             nonblock(fd);
             t->fd = fd;               /* yay */
           }
+          /* Notify the server that the connection arrived */
+          sm.type = SM_ARRIVED;
+          strcpy(sm.id, id);
+          speaker_send(1, &sm);
         }
       } else
         disorder_error(errno, "accept");
index a5598b7526977d558050e1ba2644572f68159973..eda07679de1d647e82a378fd7e04af0b9e863a73 100644 (file)
@@ -19,4 +19,5 @@
 pkgdata_DATA=slap.ogg scratch.ogg
 
 EXTRA_DIST=${pkgdata_DATA} \
-       scratch.wav scratch.flac scratch.mp3 scratch.raw long.ogg
+       scratch.wav scratch.flac scratch.mp3 scratch.raw long.ogg \
+       scratch-mp3.raw
index 235d7467a73703fba565609ce18247f02f66148d..a5a8be9c7c483ad2de06fad4831e8857ec1526de 100644 (file)
@@ -85,7 +85,7 @@ Make track with relative path S exist"""
     trackdir = os.path.dirname(trackpath)
     if not os.path.exists(trackdir):
         os.makedirs(trackdir)
-    copyfile("%s/sounds/long.ogg" % top_builddir, trackpath)
+    copyfile("%s/sounds/long.ogg" % top_srcdir, trackpath)
     # We record the tracks we created so they can be tested against
     # server responses.  We put them into NFC since that's what the server
     # uses internally.
@@ -286,6 +286,7 @@ def stop_daemon():
 Stop the daemon if it has not stopped already"""
     global daemon
     if daemon == None:
+        print " (daemon not running)"
         return
     rc = daemon.poll()
     if rc == None:
@@ -337,7 +338,9 @@ def run(module=None, report=True):
     except Exception, e:
         traceback.print_exc(None, sys.stderr)
         failures += 1
-    stop_daemon()
+    finally:
+        stop_daemon()
+        os.system("ps -ef | grep disorderd")
     if report:
         if failures:
             print " FAILED"