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
libtests/t-wstat
libtests/t-macros
libtests/t-cgi
+libtests/t-configuration
doc/*.tmpl
doc/disorder_templates.5
oc/disorder_templates.5.html
clients/rtpmon
libtests/t-resample
clients/resample
+disobedience/manual/Makefile
--- /dev/null
+See the end of README for authorship details.
}
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 {
<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>
<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>
<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>
“Recent”, “Added” and “Choose” 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>
<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>
<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>
<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>“found track in no collection” 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>
--- /dev/null
+
+ 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>.
+
--- /dev/null
+See version control history for detailed change information.
+
# 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@
--- /dev/null
+See CHANGES.html for high-level change information.
+
+See README.upgrades for upgrade information.
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)
--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:
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).
* 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
** 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
+++ /dev/null
--*-outline-*-
-
-* plugins
-
-** configuration
-
-Allow plugins to be configured via the main config file somehow.
-
-* web interface
-
-** language choice
-
-Parse HTTP_ACCEPT_LANGUAGE and use it to choose template subdirectory.
-I might leave this until I hear that someone actually wants a
-multilingual jukebox.
-
-** rearrange queue
-
-Needs thought on how to design the interface.
-
-** improve volume control
-
-** templates
-
-Build defaults into program to save file IO.
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
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
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");
/* 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}
* - @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}
# 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
# 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])
# 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
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"
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"
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 \
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}"
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
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
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"
cgi/Makefile
clients/Makefile
disobedience/Makefile
+ disobedience/manual/Makefile
doc/Makefile
templates/Makefile
plugins/Makefile
- driver/Makefile
debian/Makefile
sounds/Makefile
python/Makefile
postrm.disorder-server overrides.disorder-server \
templates.disorder-server conffiles.disorder-server \
rules changelog usr.share.menu.disobedience \
- postinst.disobedience
+ postinst.disobedience disobedience-manual
+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
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.
--- /dev/null
+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
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};\
$(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
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
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 \
$(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 \
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/*
#
# 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)
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
/** @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 = {
.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) {
/* 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);
#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;
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
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,
},
{
"Track properties",
+ GTK_STOCK_PROPERTIES,
choose_properties_activate,
choose_properties_sensitive,
0,
},
{
"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,
* @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 */
#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 */
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);
/*
* 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
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);
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:
* (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;
* 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;
/** @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);
}
/** @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,
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,
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,
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,
/** @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
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);
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);
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
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;
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);
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 */
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
/** @brief Main tab group */
GtkWidget *tabs;
+/** @brief Mini-mode widget for playing track */
+GtkWidget *playing_mini;
+
/** @brief Main client */
disorder_eclient *client;
/** @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);
* 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"));
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);
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,
/* 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*/,
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();
void (*clicked)(GtkButton *button, gpointer userdata);
const char *tip;
GtkWidget *widget;
+ void (*pack)(GtkBox *box,
+ GtkWidget *child,
+ gboolean expand,
+ gboolean fill,
+ guint padding);
};
/* Variables --------------------------------------------------------------- */
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);
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,
GtkWidget *menubar(GtkWidget *w);
/* Create the menu bar */
+int full_mode;
void users_set_sensitive(int sensitive);
/* 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
void play_completed(void *v,
const char *err);
+extern const GtkTargetEntry choose_targets[];
+
/* Login details */
void login_box(void);
/* Help */
-void popup_help(void);
+void popup_help(const char *what);
/* RTP */
/* 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 */
#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()) {
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 */
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,
/* 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,
},
};
--- /dev/null
+#
+# 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
--- /dev/null
+<?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>
--- /dev/null
+/*
+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
+}
--- /dev/null
+<!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>
--- /dev/null
+<!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>&</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
+ (“Service”). 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>
--- /dev/null
+<!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 © 2003-2009 <a
+ href="http://www.greenend.org.uk/rjk/">Richard Kettlewell</a><br>
+
+ Portions copyright © 2007 <a
+ href="http://www.chiark.greenend.org.uk/~ryounger/">Ross
+ Younger</a><br>
+
+ Portions copyright © 2007, 2008 Mark Wooding<br>
+
+ Portions extracted from <a
+ href="http://mpg321.sourceforge.net/">MPG321</a>, Copyright © 2001 Joe
+ Drew, Copyright © 2000-2001 Robert Leslie<br>
+
+ Portions copyright © 1997-2006 <a
+ href="http://www.fsf.org/">Free Software Foundation, Inc</a><br>
+
+ Portions Copyright © 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>
--- /dev/null
+<!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>
--- /dev/null
+<!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>
--- /dev/null
+<!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
+ “adopt” 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
+ “happy” 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>
--- /dev/null
+<!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 (“3:04”).
+ 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>
/*
* 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);
GtkWidget attribute((unused)) *menu_item) {
D(("manual_popup"));
- popup_help();
+ popup_help(NULL);
}
/** @brief Called when version arrives, displays about... popup */
},
{
(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 */
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
{
(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 */
(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 */
(char *)"<Branch>", /* item_type */
0 /* extra_data */
},
-#endif
-
+
{
(char *)"/Help", /* path */
0, /* accelerator */
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 */
"<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");
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
/*
* 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
const guint8 *data;
};
-#include "images.h"
+#include "../images/images.h"
/* Miscellaneous GTK+ stuff ------------------------------------------------ */
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;
/* 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;
/*
* 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;
/** @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 */
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:
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);
/** @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);
};
/** @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);
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,
/* 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,
},
};
}
}
-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;
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 */
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.
#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,
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) {
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
++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)
/* 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);
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;
}
/* 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);
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;
* 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);
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
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)
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,
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;
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) {
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;
/* 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;
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"));
(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);
/* 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 */
/* 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",
/* 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);
/* 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);
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
/*
* 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
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,
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;
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);
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 */
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 */
/* 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;
}
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);
/** @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 = {
.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 */
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
/** @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 = {
.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) {
/*
* 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
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()
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)
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 \
.\"
-.\" 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
.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
.\" .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)
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:
.RS
.TP 8
.B pcm
-
Output level for the audio device.
This is probably what you want and is the default.
.TP
.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.
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.
.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
.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.
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.
.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
.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.
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.
+++ /dev/null
-#
-# 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
+++ /dev/null
-/*
- * 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:
-*/
#
# 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
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
--- /dev/null
+<?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>
--- /dev/null
+<?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>
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 \
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 \
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
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
*/
char *cgi_sgmlquote(const char *src) {
uint32_t *ucs, c;
- int n;
struct dynstr d[1];
struct sink *s;
exit(1);
dynstr_init(d);
s = sink_dynstr(d);
- n = 1;
/* format the string */
while((c = *ucs++)) {
switch(c) {
c->ident = 0;
xfree(c->user);
c->user = 0;
- return 0;
+ return ret;
}
/** @brief Play a track
/*
* 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
/** @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;
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;
}
{ 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 },
{ 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 },
{ 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 },
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}");
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);
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
/*
* 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
/** @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
/** @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 */
char *config_private(void);
/* get the private config file */
+int config_verify(void);
+
void config_free(struct config *c);
extern char *configfile;
/** @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;
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[];
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
int c, depth;
for(;;) {
- switch(c = *s) {
+ switch(*s) {
case ' ':
case '\t':
case '\r':
* @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>
/** @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
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));
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) {
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;
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
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;
}
* - @ref SM_FINISHED
* - @ref SM_PLAYING
* - @ref SM_UNKNOWN
+ * - @ref SM_ARRIVED
*/
int type;
/** @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. */
char **parsetags(const char *s);
int tag_intersection(char **a, char **b);
-int valid_username(const char *user);
#endif /* TRACKDB_INT_H */
#include "configuration.h"
#include "vector.h"
#include "eventlog.h"
+#include "validity.h"
static int trackdb_playlist_get_tid(const char *name,
const char *who,
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
#include "unidata.h"
#include "base64.h"
#include "sendmail.h"
+#include "validity.h"
#define RESCAN "disorder-rescan"
#define DEADLOCK "disorder-deadlock"
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)
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)))) {
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:
}
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) {
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);
}
*/
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);
}
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)
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
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,
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;
}
/* 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));
#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) {
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 */
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) {
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
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
* 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;
* 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
* 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);
/** @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
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;
#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;
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)
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:
--- /dev/null
+/*
+ * 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:
+*/
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);
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);
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(); \
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
const char **vec;
vec = disorder_malloc((nparameters + 2) * sizeof (char *));
- i = 0;
j = 0;
for(i = 0; i < nparameters; ++i)
vec[j++] = parameters[i];
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
*
* 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;
--- /dev/null
+/*
+ * 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:
+*/
#! /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/&/\&/g;
- s/</\</g;
- s/>/\>/g;
- s/@/\@/g;
- s!\(.\)\b\1!<b>\1</b>!g;
- s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
- s!_\b\(.\)!<i>\1</i>!g;
- s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
- s!</\([bi]\)><\1>!!g'
-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/&/\&/g;
+ s/</\</g;
+ s/>/\>/g;
+ s/@/\@/g;
+ s!\(.\)\b\1!<b>\1</b>!g;
+ s!\(&[#0-9a-z][0-9a-z]*;\)\b\1!<b>\1</b>!g;
+ s!_\b\(.\)!<i>\1</i>!g;
+ s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
+ s!</\([bi]\)><\1>!!g'
+ echo "</pre>"
+ if $stdhead; then
+ echo "@credits"
+ fi
+ echo " </body>"
+ echo "</html>"
+ mv $output.new $output
+done
-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
#
# 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
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) \
$(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
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
# 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
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
--- /dev/null
+/*
+ * 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:
+*/
/*
* 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 */
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
* 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)
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 },
--- /dev/null
+/*
+ * 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:
+*/
/*
* 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
#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);
#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 */
/*
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 */
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");
--- /dev/null
+/*
+ * 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:
+*/
}
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")",
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;
}
config->sample_format.endian);*/
if(!rs_in_use) {
/* Create a suitable resampler. */
+ D(("call resample_init"));
resample_init(rs,
header.bits,
header.channels,
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;
}
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;
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);
}
* 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,
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;
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;
}
* 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;
}
}
/* 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",
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);
}
queue_insert_entry(afterme, q);
break;
+ case WHERE_NOWHERE:
+ return q;
}
/* submitter will be a null pointer for a scratch */
if(submitter)
if(fp)
xfclose(fp);
if(pid)
- while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+ while((waitpid(pid, &w, 0)) == -1 && errno == EINTR)
;
}
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)
static int c_new(struct conn *c,
char **vec,
int nvec) {
- int max, n;
+ int max;
char **tracks;
if(nvec > 0)
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);
/*
* 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
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. */
t->playable = 1;
rc = 0;
}
- }
+ } else
+ rc = 0;
return rc;
}
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);
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");
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
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.
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:
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"