This file documents recent user-visible changes to DisOrder.
-
Changes up to version 4.4
+
Changes up to version 5.1
+
+
+
+
Removable Device Support
+
+
+
+
The server will now automatically initiate a rescan when a filesystem is
+ mounted or unmounted. (Use the mount_rescan option if you want to
+ suppress this behavior.)
+
+
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.
+
+
+
+
+
+
Changes up to version 5.0
@@ -75,7 +105,8 @@ span.command {
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.
+ that bugs may have been (re-)introduced. Decoding of scratches is also
+ initiated ahead of time, giving more reliable playback.
The command backend now (optionally) sends silence instead
of suspending writes when a pause occurs or no track is playing.
@@ -85,6 +116,12 @@ span.command {
SoX. SoX support will be
removed in a future version.
+
The libao plugin has been removed, because the plugin API is not
+ usable in libao 1.0.0.
+
+
Playlists are now supported. These allow a collection of tracks to be
+ prepared offline and played as a unit.
+
Disobedience
@@ -96,6 +133,12 @@ span.command {
“Recent”, “Added” and “Choose” tabs
to the queue.
+
Disobedience now supports playlist editing and has a compact mode,
+ available from the Control menu.
“found track in no collection” messages for scratches
+ are now suppressed
+
+
+
+
(none)
+
Disobedience would sometimes fail to notice when a track
+ started, leading to its display getting out of date.
+
+
-
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..4432540
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,676 @@
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+
+ Copyright (C)
+
+ This program is free software: you can 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 .
+
+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:
+
+ Copyright (C)
+ 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
+.
+
+ 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
+.
+
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..c263762
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,2 @@
+See version control history for detailed change information.
+
diff --git a/Makefile.am b/Makefile.am
index 501f344..b41014f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -16,7 +16,7 @@
# along with this program. If not, see .
#
-EXTRA_DIST=TODO CHANGES.html README.streams BUGS \
+EXTRA_DIST=CHANGES.html README.streams BUGS \
README.upgrades README.client README.raw README.vhost README.developers
SUBDIRS=@subdirs@
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..28889c4
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,3 @@
+See CHANGES.html for high-level change information.
+
+See README.upgrades for upgrade information.
diff --git a/README b/README
index 57435a8..b1ddc92 100644
--- a/README
+++ b/README
@@ -25,17 +25,17 @@ effort.
Build dependencies:
Name Tested Notes
- libdb 4.3.29 not 4.2/4.6; 4.[457] seem to be ok
+ libdb 4.5.20 not 4.6; 4.[78] seem to be ok
libgc 6.8
- libvorbisfile 1.1.2
- libpcre 6.7 need UTF-8 support
+ libvorbisfile 1.2.0
+ libpcre 7.6 need UTF-8 support
libmad 0.15.1b
- libgcrypt 1.2.3
- libao 0.8.6
- libasound 1.0.13
- libFLAC 1.1.2
+ libgcrypt 1.4.1
+ libao 0.8.8 1.0.0 is broken
+ libasound 1.0.16
+ libFLAC 1.2.1
libsamplerate 0.1.4 currently optional
- GNU C 4.1.2 }
+ GNU C 4.2.1 }
GNU Make 3.81 } Non-GNU versions will NOT work
GNU Sed 4.1.5 }
Python 2.5.2 (optional; 2.4 won't work)
@@ -89,9 +89,6 @@ platform, please get in touch.
--without-gtk Don't build GTK+ client (Disobedience)
--without-python Don't build Python support
- On a Mac you can use --with-bits=64 to request a 64-bit build. The default
- is 32 bits. You will need suitable versions of all the libraries used.
-
If configure cannot guess where your web server keeps its HTML documents and
CGI programs, you may have to tell it, for instance:
diff --git a/README.developers b/README.developers
index 9e84520..5c71b07 100644
--- a/README.developers
+++ b/README.developers
@@ -18,22 +18,13 @@ Dependencies:
refuse to use it).
* On FreeBSD you'll need at least these packages:
- autotools
- bash
- flac
- mad
- boehm-gc
- db43
- gmake
- gsed
- libao
- libgcrypt
- wget
- vorbis-tools
+ autotools bash flac mad boehm-gc db43 gmake gsed libao libgcrypt wget
+ vorbis-tools
* On OS X with Fink:
- fink install gtk+2-dev gc libgrypt pcre flac vorbis-tools libmad wget sed
+ fink install gtk+2-dev gc libgrypt pcre flac vorbis-tools libmad wget \
+ sed libsamplerate0-dev
* Please report unstated dependencies (here, README or debian/control).
@@ -42,7 +33,7 @@ Building:
* Compiled versions of configure and the makefiles are not included in bzr,
so if you didn't use a source tarball, you must start as follows:
- bash ./prepare
+ bash ./autogen.sh
./configure -C
make
diff --git a/README.raw b/README.raw
index a2da623..a107964 100644
--- a/README.raw
+++ b/README.raw
@@ -13,22 +13,8 @@ The purpose of raw format players is:
** Usage
-To use raw format, use the execraw module and make the command choose the
-"disorder" libao driver. You may need to link the driver from wherever
-DisOrder installs it (e.g. /usr/local/lib/ao/plugins-2) to where libao will
-look for it (e.g. /usr/lib/ao/plugins-2 or /sw/lib/ao/plugins-2).
-
-You should pass the "fragile" option to ogg123. This is because ogg123 ignores
-write errors!
-
-mpg321 does not appear to have this bug.
-
-For _non_ raw players it is advisable to use the new --wait-for-device option.
-This repeatedly tries to open the audio device before starting the player
-proper. It times out after a couple of seconds.
-
-See disorder_config(5) and the example configuration file for further
-information and examples.
+By default, built-in raw-format players are used for several encodings, so you
+do not need to do anything.
** Low-Level Details
diff --git a/TODO b/TODO
deleted file mode 100644
index e15c225..0000000
--- a/TODO
+++ /dev/null
@@ -1,25 +0,0 @@
--*-outline-*-
-
-* plugins
-
-** configuration
-
-Allow plugins to be configured via the main config file somehow.
-
-* web interface
-
-** language choice
-
-Parse HTTP_ACCEPT_LANGUAGE and use it to choose template subdirectory.
-I might leave this until I hear that someone actually wants a
-multilingual jukebox.
-
-** rearrange queue
-
-Needs thought on how to design the interface.
-
-** improve volume control
-
-** templates
-
-Build defaults into program to save file IO.
diff --git a/prepare b/autogen.sh
similarity index 85%
rename from prepare
rename to autogen.sh
index dd3de9b..a63049a 100755
--- a/prepare
+++ b/autogen.sh
@@ -21,13 +21,6 @@ set -e
srcdir=$(dirname $0)
here=$(pwd)
cd $srcdir
-rm -f COPYING
-for f in /usr/share/common-licenses/GPL-3 $HOME/doc/GPL-3 $HOME/Documents/GPL-3; do
- if test -e "$f"; then
- ln -s "$f" COPYING
- break
- fi
-done
if test -d $HOME/share/aclocal; then
aclocal --acdir=$HOME/share/aclocal
else
diff --git a/cgi/actions.c b/cgi/actions.c
index 30ddba1..f6755b0 100644
--- a/cgi/actions.c
+++ b/cgi/actions.c
@@ -95,6 +95,10 @@ static void act_playing(void) {
if(refresh > config->gap)
refresh = config->gap;
}
+ /* Bound the refresh interval below as a back-stop against the above
+ * calculations coming up with a stupid answer */
+ if(refresh < config->refresh_min)
+ refresh = config->refresh_min;
if((action = cgi_get("action")))
url = cgi_makeurl(config->url, "action", action, (char *)0);
else
@@ -608,7 +612,7 @@ static int process_prefs(int numfile) {
byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
disorder_set(dcgi_client, file, name, value);
}
- if((value = numbered_arg("random", numfile)))
+ if(numbered_arg("random", numfile))
disorder_unset(dcgi_client, file, "pick_at_random");
else
disorder_set(dcgi_client, file, "pick_at_random", "0");
diff --git a/cgi/macros-disorder.c b/cgi/macros-disorder.c
index 30614c1..29835bb 100644
--- a/cgi/macros-disorder.c
+++ b/cgi/macros-disorder.c
@@ -859,24 +859,36 @@ static int exp__files_dirs(int nargs,
/* Get the list */
if(fn(dcgi_client, dir, re, &tracks, &ntracks))
return 0;
- /* Sort it. NB trackname_transform() does not go to the server. */
- tsd = tracksort_init(ntracks, tracks, type);
- /* Expand the subsiduary templates. We chuck in @sort and @display because
- * it is particularly easy to do so. */
- for(n = 0; n < ntracks; ++n)
- if((rc = mx_expand(mx_rewritel(m,
- "index", make_index(n),
- "parity", n % 2 ? "odd" : "even",
- "track", tsd[n].track,
- "first", n == 0 ? "true" : "false",
- "last", n + 1 == ntracks ? "false" : "true",
- "sort", tsd[n].sort,
- "display", tsd[n].display,
- (char *)0),
- output, u)))
- return rc;
+ if(type) {
+ /* Sort it. NB trackname_transform() does not go to the server. */
+ tsd = tracksort_init(ntracks, tracks, type);
+ /* Expand the subsiduary templates. We chuck in @sort and @display because
+ * it is particularly easy to do so. */
+ for(n = 0; n < ntracks; ++n)
+ if((rc = mx_expand(mx_rewritel(m,
+ "index", make_index(n),
+ "parity", n % 2 ? "odd" : "even",
+ "track", tsd[n].track,
+ "first", n == 0 ? "true" : "false",
+ "last", n + 1 == ntracks ? "false" : "true",
+ "sort", tsd[n].sort,
+ "display", tsd[n].display,
+ (char *)0),
+ output, u)))
+ return rc;
+ } else {
+ for(n = 0; n < ntracks; ++n)
+ if((rc = mx_expand(mx_rewritel(m,
+ "index", make_index(n),
+ "parity", n % 2 ? "odd" : "even",
+ "track", tracks[n],
+ "first", n == 0 ? "true" : "false",
+ "last", n + 1 == ntracks ? "false" : "true",
+ (char *)0),
+ output, u)))
+ return rc;
+ }
return 0;
-
}
/*$ @tracks{DIR}{RE}{TEMPLATE}
@@ -936,14 +948,12 @@ static int exp__search_shim(disorder_client *c, const char *terms,
* - @parity: "even" or "odd" alternately
* - @first: "true" on the first directory and "false" otherwise
* - @last: "true" on the last directory and "false" otherwise
- * - @sort: the sort key for this track
- * - @display: the UNQUOTED display string for this track
*/
static int exp_search(int nargs,
const struct mx_node **args,
struct sink *output,
void *u) {
- return exp__files_dirs(nargs, args, output, u, "track", exp__search_shim);
+ return exp__files_dirs(nargs, args, output, u, NULL, exp__search_shim);
}
/*$ @label{NAME}
diff --git a/configure.ac b/configure.ac
index e48cce2..1d81d83 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,7 +1,7 @@
# Process this file with autoconf to produce a configure script.
#
# This file is part of DisOrder.
-# Copyright (C) 2004-2009 Richard Kettlewell
+# Copyright (C) 2004-2010 Richard Kettlewell
# Portions copyright (C) 2007 Ross Younger
#
# This program is free software: you can redistribute it and/or modify
@@ -18,9 +18,9 @@
# along with this program. If not, see .
#
-AC_INIT([disorder], [4.3+], [richard+disorder@sfere.greenend.org.uk])
+AC_INIT([disorder], [5.0], [richard+disorder@sfere.greenend.org.uk])
AC_CONFIG_AUX_DIR([config.aux])
-AM_INIT_AUTOMAKE(disorder, [4.3+])
+AM_INIT_AUTOMAKE(disorder, [5.0])
AC_CONFIG_SRCDIR([server/disorderd.c])
AM_CONFIG_HEADER([config.h])
@@ -156,7 +156,7 @@ case "$host" in
# Look for a suitable version of libdb among the versions found in FreeBSD 7.0
AC_CACHE_CHECK([looking for a libdb install],[rjk_cv_libdb],[
rjk_cv_libdb="none"
- for db in db43 db44 db45 db46; do
+ for db in db43 db44 db45 db47; do
if test -e /usr/local/lib/$db; then
rjk_cv_libdb=$db
break
@@ -326,6 +326,11 @@ if test -z "$pkghttpdir"; then
fi
AC_SUBST([pkghttpdir])
+if test -z "$dochtmldir"; then
+ dochtmldir='$(docdir)/html'
+fi
+AC_SUBST([dochtmldir])
+
subdirs="scripts lib"
if test $want_tests = yes; then
subdirs="${subdirs} libtests"
@@ -333,7 +338,7 @@ fi
subdirs="${subdirs} clients doc examples debian"
if test $want_server = yes; then
- subdirs="${subdirs} server plugins driver sounds"
+ subdirs="${subdirs} server plugins sounds"
fi
if test $want_cgi = yes; then
subdirs="${subdirs} cgi templates images"
@@ -517,7 +522,6 @@ if test $want_server = yes; then
AC_CHECK_HEADERS([db.h],[:],[
missing_headers="$missing_headers $ac_header"
])
- AC_CHECK_HEADERS([FLAC/file_decoder.h])
fi
AC_CHECK_HEADERS([dlfcn.h gcrypt.h \
getopt.h iconv.h langinfo.h \
@@ -542,6 +546,18 @@ AC_C_BIGENDIAN
AC_CHECK_TYPES([struct sockaddr_in6],,,[AC_INCLUDES_DEFAULT
#include ])
+# Figure out how we'll check for devices being mounted and unmounted
+AC_CACHE_CHECK([for list of mounted filesystems],[rjk_cv_mtab],[
+ if test -e /etc/mtab; then
+ rjk_cv_mtab=/etc/mtab
+ else
+ rjk_cv_mtab=none
+ fi
+])
+if test $rjk_cv_mtab != none; then
+ AC_DEFINE_UNQUOTED([PATH_MTAB],["$rjk_cv_mtab"],[path to file containing mount list])
+fi
+
# enable -Werror when we check for certain characteristics:
old_CFLAGS="${CFLAGS}"
@@ -634,7 +650,7 @@ if test ! -z "$missing_functions"; then
fi
# Functions we can take or leave
-AC_CHECK_FUNCS([fls])
+AC_CHECK_FUNCS([fls getfsstat])
if test $want_server = yes; then
# had better be version 3 or later
@@ -680,6 +696,7 @@ AM_CONDITIONAL([SERVER], [test x$want_server = xyes])
if test $want_gtk = yes; then
AC_DEFINE([WITH_GTK], [1], [define if using GTK+])
fi
+AM_CONDITIONAL([GTK], [test x$want_gtk = xyes])
if test "x$GCC" = xyes; then
# We need LLONG_MAX and annoyingly GCC doesn't always give it to us
@@ -746,7 +763,7 @@ if test "x$GCC" = xyes; then
fi
# a reasonable default set of warnings
- CC="${CC} -Wall -W -Wpointer-arith -Wbad-function-cast \
+ CC="${CC} -Wall -W -Wpointer-arith \
-Wwrite-strings -Wmissing-prototypes \
-Wmissing-declarations -Wnested-externs"
@@ -823,10 +840,10 @@ AC_CONFIG_FILES([Makefile
cgi/Makefile
clients/Makefile
disobedience/Makefile
+ disobedience/manual/Makefile
doc/Makefile
templates/Makefile
plugins/Makefile
- driver/Makefile
debian/Makefile
sounds/Makefile
python/Makefile
diff --git a/debian/Makefile.am b/debian/Makefile.am
index b799a1c..293df22 100644
--- a/debian/Makefile.am
+++ b/debian/Makefile.am
@@ -25,4 +25,4 @@ EXTRA_DIST=README.Debian config.disorder-server control \
postrm.disorder-server overrides.disorder-server \
templates.disorder-server conffiles.disorder-server \
rules changelog usr.share.menu.disobedience \
- postinst.disobedience
+ postinst.disobedience disobedience-manual
diff --git a/debian/changelog b/debian/changelog
index 61e1fe7..e76e763 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+disorder (5.0) unstable; urgency=low
+
+ * DisOrder 5.0
+
+ -- Richard Kettlewell Sun, 06 Jun 2010 12:43:21 +0100
+
disorder (4.3) unstable; urgency=low
* DisOrder 4.3
diff --git a/debian/copyright b/debian/copyright
index bc90bff..918dbb4 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -29,5 +29,5 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
-On Debian systems, look in /usr/share/common-licenses/GPL for a copy
+On Debian systems, look in /usr/share/common-licenses/GPL-3 for a copy
of the GPL.
diff --git a/debian/disobedience-manual b/debian/disobedience-manual
new file mode 100644
index 0000000..93a81b3
--- /dev/null
+++ b/debian/disobedience-manual
@@ -0,0 +1,12 @@
+Document: disobedience-manual
+Title: Disobedience Manual
+Author: Richard Kettlewell
+Abstract: This manual describes how to use Disobedience, the GUI
+ client for DisOrder.
+Section: Sound
+
+Format: HTML
+Index: /usr/share/doc/disorder/html/index.html
+Files: /usr/share/doc/disorder/html/*.html
+ /usr/share/doc/disorder/html/*.png
+ /usr/share/doc/disorder/html/*.css
diff --git a/debian/rules b/debian/rules
index 620c775..1861c13 100755
--- a/debian/rules
+++ b/debian/rules
@@ -62,12 +62,12 @@ FAKEROOT=fakeroot
SHELL=bash
-# ./prepare is the script that generates configure etc. It only needs to be
+# ./autogen.sh is the script that generates configure etc. It only needs to be
# run if building from a checkout rather than a tarball.
build:
@set -e;if test ! -f configure; then \
- echo ./prepare;\
- ./prepare;\
+ echo ./autogen.sh;\
+ ./autogen.sh;\
fi
@set -e;if test ! -f config.status; then \
echo ./configure ${CONFIGURE} ${CONFIGURE_EXTRA};\
@@ -166,7 +166,6 @@ pkg-disorder-server: build
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C images
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C server
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C templates
- $(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C driver
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C plugins
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C sounds
$(MAKE) DESTDIR=`pwd`/debian/disorder-server installdirs install -C doc
@@ -181,10 +180,6 @@ pkg-disorder-server: build
echo mv $$f $${f/.0.0.0};\
mv $$f $${f/.0.0.0};\
done
- @for f in debian/disorder-server/usr/lib/ao/plugins*/*.so.0.0.0; do \
- echo mv $$f $${f/.0.0.0};\
- mv $$f $${f/.0.0.0};\
- done
find debian/disorder-server -name '*.so' -print0 | xargs -r0 strip --strip-unneeded
find debian/disorder-server -name '*.so' -print0 | xargs -r0 chmod -x
$(MKDIR) debian/disorder-server/etc/disorder
@@ -211,7 +206,6 @@ pkg-disorder-server: build
strip --remove-section=.comment \
debian/disorder-server/usr/sbin/* \
debian/disorder-server${cgiexecdir}/* \
- debian/disorder-server/usr/lib/ao/plugins*/*.so \
debian/disorder-server/usr/lib/disorder/*.so
cd debian/disorder-server && \
find -name DEBIAN -prune -o -type f -print \
@@ -280,6 +274,7 @@ pkg-disobedience: build
$(MKDIR) debian/disobedience/usr/share/man/man1
$(MKDIR) debian/disobedience/usr/share/pixmaps
$(MKDIR) debian/disobedience/usr/share/menu
+ $(MKDIR) debian/disobedience/usr/share/doc-base
$(MAKE) -C disobedience install DESTDIR=`pwd`/debian/disobedience
strip --remove-section=.comment debian/disobedience/usr/bin/disobedience
$(INSTALL_DATA) doc/disobedience.1 \
@@ -289,6 +284,8 @@ pkg-disobedience: build
debian/disobedience/usr/share/pixmaps
$(INSTALL_DATA) debian/usr.share.menu.disobedience \
debian/disobedience/usr/share/menu/disobedience
+ $(INSTALL_DATA) debian/disobedience-manual \
+ debian/disobedience/usr/share/doc-base/disobedience-manual
gzip -9f debian/disobedience/usr/share/man/man*/*
dpkg-shlibdeps -Tdebian/substvars.disobedience \
debian/disobedience/usr/bin/*
diff --git a/disobedience/Makefile.am b/disobedience/Makefile.am
index 0171633..7b823c2 100644
--- a/disobedience/Makefile.am
+++ b/disobedience/Makefile.am
@@ -1,6 +1,6 @@
#
# This file is part of DisOrder.
-# Copyright (C) 2006-2009 Richard Kettlewell
+# Copyright (C) 2006-2010 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,18 +17,17 @@
#
bin_PROGRAMS=disobedience
-pkgdata_DATA=disobedience.html
+SUBDIRS=manual
AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
-PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png)
disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c \
recent.c added.c queue-generic.c queue-generic.h queue-menu.c \
choose.c choose-menu.c choose-search.c popup.c misc.c \
control.c properties.c menu.c log.c progress.c login.c rtp.c \
help.c ../lib/memgc.c settings.c users.c lookup.c choose.h \
- popup.h playlists.c multidrag.c multidrag.h autoscroll.c
+ popup.h playlists.c multidrag.c multidrag.h autoscroll.c \
autoscroll.h
disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
$(LIBASOUND) $(COREAUDIO) $(LIBDB) $(LIBICONV)
@@ -39,35 +38,13 @@ install-exec-hook:
check: check-help
-disobedience.html: ../doc/disobedience.1 $(top_srcdir)/scripts/htmlman
- rm -f $@.new
- $(top_srcdir)/scripts/htmlman $< >$@.new
- chmod 444 $@.new
- mv -f $@.new $@
-
-misc.o: images.h
-
-images.h: $(PNGS)
- set -e; \
- exec > @$.new; \
- for png in $(PNGS); do \
- name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`; \
- gdk-pixbuf-csource --raw --name=image_$$name $$png; \
- done; \
- echo "static const struct image images[] = {"; \
- for png in $(PNGS); do \
- name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`; \
- echo " { \"$$name.png\", image_$$name },"; \
- done; \
- echo "};"
- mv @$.new $@
+misc.o: ../images/images.h
# check everything has working --help
check-help: all
unset DISPLAY;./disobedience --version > /dev/null
unset DISPLAY;./disobedience --help > /dev/null
-CLEANFILES=disobedience.html images.h \
- *.gcda *.gcov *.gcno *.c.html index.html
+CLEANFILES=*.gcda *.gcov *.gcno *.c.html index.html
export GNUSED
diff --git a/disobedience/added.c b/disobedience/added.c
index 7f654ee..04e1d77 100644
--- a/disobedience/added.c
+++ b/disobedience/added.c
@@ -80,10 +80,10 @@ static const struct queue_column added_columns[] = {
/** @brief Pop-up menu for new tracks list */
static struct menuitem added_menuitems[] = {
- { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
- { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
- { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
- { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+ { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+ { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+ { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+ { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
};
struct queuelike ql_added = {
@@ -93,6 +93,8 @@ struct queuelike ql_added = {
.ncolumns = sizeof added_columns / sizeof *added_columns,
.menuitems = added_menuitems,
.nmenuitems = sizeof added_menuitems / sizeof *added_menuitems,
+ .drag_source_targets = choose_targets,
+ .drag_source_actions = GDK_ACTION_COPY,
};
GtkWidget *added_widget(void) {
diff --git a/disobedience/autoscroll.c b/disobedience/autoscroll.c
index bfe71c1..2b24699 100644
--- a/disobedience/autoscroll.c
+++ b/disobedience/autoscroll.c
@@ -68,12 +68,11 @@ static gboolean autoscroll_timeout(gpointer data) {
/* see if we are near the edge. */
offset = ty - (visible_rect.y + 2 * SCROLL_EDGE_SIZE);
- if (offset > 0)
- {
- offset = ty - (visible_rect.y + visible_rect.height - 2 * SCROLL_EDGE_SIZE);
- if (offset < 0)
- return TRUE;
- }
+ if (offset > 0) {
+ offset = ty - (visible_rect.y + visible_rect.height - 2 * SCROLL_EDGE_SIZE);
+ if (offset < 0)
+ return TRUE;
+ }
GtkAdjustment *vadjustment = gtk_tree_view_get_vadjustment(tree_view);
diff --git a/disobedience/choose-menu.c b/disobedience/choose-menu.c
index f1aa3b0..b0f59b5 100644
--- a/disobedience/choose-menu.c
+++ b/disobedience/choose-menu.c
@@ -22,6 +22,15 @@
#include "popup.h"
#include "choose.h"
+static void choose_playchildren_callback(GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer data);
+static void choose_playchildren_received(void *v,
+ const char *err,
+ int nvec, char **vec);
+static void choose_playchildren_played(void *v, const char *err);
+
/** @brief Popup menu */
static GtkWidget *choose_menu;
@@ -121,7 +130,7 @@ static void choose_properties_activate(GtkMenuItem attribute((unused)) *item,
gtk_tree_selection_selected_foreach(choose_selection,
choose_gather_selected_files_callback,
v);
- properties(v->nvec, (const char **)v->vec);
+ properties(v->nvec, (const char **)v->vec, toplevel);
}
/** @brief Set sensitivity for select children
@@ -210,10 +219,53 @@ static void choose_selectchildren_activate
0);
}
+/** @brief Play all children */
+static void choose_playchildren_activate
+ (GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* Only one thing is selected */
+ gtk_tree_selection_selected_foreach(choose_selection,
+ choose_playchildren_callback,
+ 0);
+}
+
+static void choose_playchildren_callback(GtkTreeModel attribute((unused)) *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer attribute((unused)) data) {
+ /* Find the children and play them */
+ disorder_eclient_files(client, choose_playchildren_received,
+ choose_get_track(iter),
+ NULL/*re*/,
+ NULL);
+ /* Expand the node */
+ gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view), path, FALSE);
+}
+
+static void choose_playchildren_received(void attribute((unused)) *v,
+ const char *err,
+ int nvec, char **vec) {
+ if(err) {
+ popup_protocol_error(0, err);
+ return;
+ }
+ for(int n = 0; n < nvec; ++n)
+ disorder_eclient_play(client, vec[n], choose_playchildren_played, NULL);
+}
+
+static void choose_playchildren_played(void attribute((unused)) *v,
+ const char *err) {
+ if(err) {
+ popup_protocol_error(0, err);
+ return;
+ }
+}
+
/** @brief Pop-up menu for choose */
static struct menuitem choose_menuitems[] = {
{
"Play track",
+ GTK_STOCK_MEDIA_PLAY,
choose_play_activate,
choose_play_sensitive,
0,
@@ -221,6 +273,7 @@ static struct menuitem choose_menuitems[] = {
},
{
"Track properties",
+ GTK_STOCK_PROPERTIES,
choose_properties_activate,
choose_properties_sensitive,
0,
@@ -228,13 +281,23 @@ static struct menuitem choose_menuitems[] = {
},
{
"Select children",
+ NULL,
choose_selectchildren_activate,
choose_selectchildren_sensitive,
0,
0
},
+ {
+ "Play children",
+ NULL,
+ choose_playchildren_activate,
+ choose_selectchildren_sensitive, /* re-use */
+ 0,
+ 0
+ },
{
"Deselect all tracks",
+ NULL,
choose_selectnone_activate,
choose_selectnone_sensitive,
0,
diff --git a/disobedience/choose-search.c b/disobedience/choose-search.c
index acfddfa..60df2ec 100644
--- a/disobedience/choose-search.c
+++ b/disobedience/choose-search.c
@@ -441,6 +441,7 @@ static gboolean choose_get_visible_range(GtkTreeView *tree_view,
* @param direction -1 for prev, +1 for next
*/
static void choose_move(int direction) {
+ assert(direction); /* placate analyzer */
/* Refocus the main view so typahead find continues to work */
gtk_widget_grab_focus(choose_view);
/* If there's no results we have nothing to do */
diff --git a/disobedience/choose.c b/disobedience/choose.c
index 9829f19..a1d50c1 100644
--- a/disobedience/choose.c
+++ b/disobedience/choose.c
@@ -33,15 +33,19 @@
#include "disobedience.h"
#include "choose.h"
#include "multidrag.h"
+#include "queue-generic.h"
#include
/** @brief Drag types */
-static const GtkTargetEntry choose_targets[] = {
+const GtkTargetEntry choose_targets[] = {
{
- (char *)"text/x-disorder-playable-tracks", /* drag type */
+ PLAYABLE_TRACKS, /* drag type */
GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
- 1 /* ID value */
+ PLAYABLE_TRACKS_ID /* ID value */
},
+ {
+ .target = NULL
+ }
};
/** @brief The current selection tree */
@@ -711,7 +715,7 @@ GtkWidget *choose_widget(void) {
gtk_drag_source_set(choose_view,
GDK_BUTTON1_MASK,
choose_targets,
- sizeof choose_targets / sizeof *choose_targets,
+ 1,
GDK_ACTION_COPY);
g_signal_connect(choose_view, "drag-data-get",
G_CALLBACK(choose_drag_data_get), NULL);
diff --git a/disobedience/control.c b/disobedience/control.c
index 931746a..f45492f 100644
--- a/disobedience/control.c
+++ b/disobedience/control.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder.
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,9 @@
struct icon;
-static void clicked_icon(GtkButton *, gpointer);
+static void clicked_icon(GtkToolButton *, gpointer);
+static void toggled_icon(GtkToggleToolButton *button,
+ gpointer user_data);
static void clicked_menu(GtkMenuItem *, gpointer userdata);
static void toggled_menu(GtkCheckMenuItem *, gpointer userdata);
@@ -51,12 +53,18 @@ static void icon_changed(const char *event,
static void volume_changed(const char *event,
void *eventdata,
void *callbackdata);
+static void control_minimode(const char *event,
+ void *eventdata,
+ void *callbackdata);
/* Control bar ------------------------------------------------------------- */
/** @brief Guard against feedback */
int suppress_actions = 1;
+/** @brief Toolbar widget */
+static GtkWidget *toolbar;
+
/** @brief Definition of an icon
*
* We have two kinds of icon:
@@ -69,21 +77,27 @@ int suppress_actions = 1;
* (All icons can be sensitive or insensitive, separately to the above.)
*/
struct icon {
- /** @brief Filename for 'on' image */
- const char *icon_on;
+ /** @brief TRUE to use GTK+ stock icons instead of filenames */
+ gboolean stock;
+
+ /** @brief TRUE for toggle buttons, FALSE for action buttons */
+ gboolean toggle;
+
+ /** @brief Filename for image or stock string */
+ const char *icon;
/** @brief Text for 'on' tooltip */
const char *tip_on;
- /** @brief Filename for 'off' image or NULL for an action icon */
- const char *icon_off;
-
- /** @brief Text for 'off tooltip */
+ /** @brief Text for 'off' tooltip */
const char *tip_off;
/** @brief Associated menu item or NULL */
const char *menuitem;
+ /** @brief Label text */
+ const char *label;
+
/** @brief Events that change this icon, separated by spaces */
const char *events;
@@ -114,6 +128,9 @@ struct icon {
* Can be NULL for always sensitive.
*/
int (*sensitive)(void);
+
+ /** @brief True if the menu item has inverse sense to the button */
+ gboolean menu_invert;
/** @brief Pointer to button */
GtkWidget *button;
@@ -121,16 +138,16 @@ struct icon {
/** @brief Pointer to menu item */
GtkWidget *item;
- GtkWidget *image_on;
- GtkWidget *image_off;
+ GtkWidget *image;
};
static int pause_resume_on(void) {
- return !(last_state & DISORDER_TRACK_PAUSED);
+ return !!(last_state & DISORDER_TRACK_PAUSED);
}
static int pause_resume_sensitive(void) {
- return !!(last_state & DISORDER_PLAYING)
+ return playing_track
+ && !!(last_state & DISORDER_PLAYING)
&& (last_rights & RIGHT_PAUSE);
}
@@ -166,19 +183,24 @@ static int rtp_sensitive(void) {
/** @brief Table of all icons */
static struct icon icons[] = {
{
- icon_on: "pause32.png",
- tip_on: "Pause playing track",
- icon_off: "play32.png",
- tip_off: "Resume playing track",
+ toggle: TRUE,
+ stock: TRUE,
+ icon: GTK_STOCK_MEDIA_PAUSE,
+ label: "Pause",
+ tip_on: "Resume playing track",
+ tip_off: "Pause playing track",
menuitem: "/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: "/Control/Scratch",
sensitive: scratch_sensitive,
@@ -186,9 +208,11 @@ static struct icon icons[] = {
events: "playing-track-changed rights-changed",
},
{
- icon_on: "randomenabled32.png",
+ toggle: TRUE,
+ stock: FALSE,
+ icon: "cards24.png",
+ label: "Random",
tip_on: "Disable random play",
- icon_off: "randomdisabled32.png",
tip_off: "Enable random play",
menuitem: "/Control/Random play",
on: random_enabled,
@@ -198,9 +222,11 @@ static struct icon icons[] = {
events: "random-changed rights-changed",
},
{
- icon_on: "playenabled32.png",
+ toggle: TRUE,
+ stock: TRUE,
+ icon: GTK_STOCK_MEDIA_PLAY,
+ label: "Play",
tip_on: "Disable play",
- icon_off: "playdisabled32.png",
tip_off: "Enable play",
on: playing_enabled,
sensitive: playing_sensitive,
@@ -209,9 +235,11 @@ static struct icon icons[] = {
events: "enabled-changed rights-changed",
},
{
- icon_on: "rtpenabled32.png",
+ toggle: TRUE,
+ stock: TRUE,
+ icon: GTK_STOCK_CONNECT,
+ label: "RTP",
tip_on: "Stop playing network stream",
- icon_off: "rtpdisabled32.png",
tip_off: "Play network stream",
menuitem: "/Control/Network player",
on: rtp_enabled,
@@ -232,36 +260,52 @@ static GtkWidget *balance_widget;
/** @brief Create the control bar */
GtkWidget *control_widget(void) {
- GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox;
+ GtkWidget *hbox = gtk_hbox_new(FALSE, 1);
int n;
D(("control_widget"));
assert(mainmenufactory); /* ordering must be right */
+ toolbar = gtk_toolbar_new();
+ /* Don't permit overflow arrow as otherwise the toolbar isn't greedy enough
+ * in asking for space. The ideal is probably to make the volume and balance
+ * sliders hang down from the toolbar so it unavoidably gets the whole width
+ * of the window to play with. */
+ gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar), FALSE);
+ gtk_toolbar_set_style(GTK_TOOLBAR(toolbar),
+ full_mode ? GTK_TOOLBAR_BOTH : GTK_TOOLBAR_ICONS);
for(n = 0; n < NICONS; ++n) {
- /* Create the button */
- icons[n].button = gtk_button_new();
+ struct icon *const icon = &icons[n];
+ icon->button = (icon->toggle
+ ? GTK_WIDGET(gtk_toggle_tool_button_new())
+ : GTK_WIDGET(gtk_tool_button_new(NULL, NULL)));
gtk_widget_set_style(icons[n].button, tool_style);
- icons[n].image_on = gtk_image_new_from_pixbuf(find_image(icons[n].icon_on));
- gtk_widget_set_style(icons[n].image_on, tool_style);
- g_object_ref(icons[n].image_on);
- /* If it's a toggle icon, create the 'off' half too */
- if(icons[n].icon_off) {
- icons[n].image_off = gtk_image_new_from_pixbuf(find_image(icons[n].icon_off));
- gtk_widget_set_style(icons[n].image_off, tool_style);
- g_object_ref(icons[n].image_off);
+ if(icons[n].stock) {
+ /* We'll use the stock icons for this one */
+ icon->image = gtk_image_new_from_stock(icons[n].icon,
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ } else {
+ /* Create the 'on' image */
+ icon->image = gtk_image_new_from_pixbuf(find_image(icons[n].icon));
}
- g_signal_connect(G_OBJECT(icons[n].button), "clicked",
- G_CALLBACK(clicked_icon), &icons[n]);
- /* pop the icon in a vbox so it doesn't get vertically stretch if there are
- * taller things in the control bar */
- vbox = gtk_vbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(vbox), icons[n].button, TRUE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0);
+ assert(icon->image);
+ gtk_tool_button_set_icon_widget(GTK_TOOL_BUTTON(icon->button),
+ icon->image);
+ gtk_tool_button_set_label(GTK_TOOL_BUTTON(icon->button),
+ icon->label);
+ if(icon->toggle)
+ g_signal_connect(G_OBJECT(icon->button), "toggled",
+ G_CALLBACK(toggled_icon), icon);
+ else
+ g_signal_connect(G_OBJECT(icon->button), "clicked",
+ G_CALLBACK(clicked_icon), icon);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
+ GTK_TOOL_ITEM(icon->button),
+ -1);
if(icons[n].menuitem) {
/* Find the menu item */
icons[n].item = gtk_item_factory_get_widget(mainmenufactory,
icons[n].menuitem);
- if(icons[n].icon_off)
+ if(icon->toggle)
g_signal_connect(G_OBJECT(icons[n].item), "toggled",
G_CALLBACK(toggled_menu), &icons[n]);
else
@@ -287,12 +331,16 @@ GtkWidget *control_widget(void) {
gtk_widget_set_style(balance_widget, tool_style);
gtk_scale_set_digits(GTK_SCALE(volume_widget), 10);
gtk_scale_set_digits(GTK_SCALE(balance_widget), 10);
- gtk_widget_set_size_request(volume_widget, 192, -1);
- gtk_widget_set_size_request(balance_widget, 192, -1);
+ gtk_widget_set_size_request(volume_widget, 128, -1);
+ gtk_widget_set_size_request(balance_widget, 128, -1);
gtk_widget_set_tooltip_text(volume_widget, "Volume");
gtk_widget_set_tooltip_text(balance_widget, "Balance");
- gtk_box_pack_start(GTK_BOX(hbox), volume_widget, FALSE, TRUE, 0);
- gtk_box_pack_start(GTK_BOX(hbox), balance_widget, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), toolbar,
+ FALSE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), volume_widget,
+ FALSE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), balance_widget,
+ FALSE/*expand*/, TRUE/*fill*/, 0);
/* space updates rather than hammering the server */
gtk_range_set_update_policy(GTK_RANGE(volume_widget), GTK_UPDATE_DELAYED);
gtk_range_set_update_policy(GTK_RANGE(balance_widget), GTK_UPDATE_DELAYED);
@@ -308,29 +356,31 @@ GtkWidget *control_widget(void) {
G_CALLBACK(format_balance), 0);
event_register("volume-changed", volume_changed, 0);
event_register("rtp-changed", volume_changed, 0);
+ event_register("mini-mode-changed", control_minimode, 0);
return hbox;
}
+/** @brief Return TRUE if volume setting is supported */
+static int volume_supported(void) {
+ /* TODO: if the server doesn't know how to set the volume [but isn't using
+ * network play] then we should have volume_supported = FALSE */
+ return (!rtp_supported
+ || (rtp_supported && backend && backend->set_volume));
+}
+
/** @brief Update the volume control when it changes */
static void volume_changed(const char attribute((unused)) *event,
void attribute((unused)) *eventdata,
void attribute((unused)) *callbackdata) {
double l, r;
- gboolean volume_supported;
D(("volume_changed"));
++suppress_actions;
/* Only display volume/balance controls if they will work */
- if(!rtp_supported
- || (rtp_supported && backend && backend->set_volume))
- volume_supported = TRUE;
- else
- volume_supported = FALSE;
- /* TODO: if the server doesn't know how to set the volume [but isn't using
- * network play] then we should have volume_supported = FALSE */
- if(volume_supported) {
+ if(volume_supported()) {
gtk_widget_show(volume_widget);
- gtk_widget_show(balance_widget);
+ if(full_mode)
+ gtk_widget_show(balance_widget);
l = volume_l / 100.0;
r = volume_r / 100.0;
gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto);
@@ -352,22 +402,14 @@ static void icon_changed(const char attribute((unused)) *event,
int on = icon->on ? icon->on() : 1;
int sensitive = icon->sensitive ? icon->sensitive() : 1;
//fprintf(stderr, "sensitive->%d\n", sensitive);
- GtkWidget *child, *newchild;
++suppress_actions;
/* If the connection is down nothing is ever usable */
if(!(last_state & DISORDER_CONNECTED))
sensitive = 0;
- //fprintf(stderr, "(checked connected) sensitive->%d\n", sensitive);
- /* Replace the child */
- newchild = on ? icon->image_on : icon->image_off;
- child = gtk_bin_get_child(GTK_BIN(icon->button));
- if(child != newchild) {
- if(child)
- gtk_container_remove(GTK_CONTAINER(icon->button), child);
- gtk_container_add(GTK_CONTAINER(icon->button), newchild);
- gtk_widget_show(newchild);
- }
+ if(icon->toggle)
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(icon->button),
+ on);
/* If you disable play or random play NOT via the icon (for instance, via the
* edit menu or via a completely separate command line invocation) then the
* icon shows up as insensitive. Hover the mouse over it and the correct
@@ -379,8 +421,9 @@ static void icon_changed(const char attribute((unused)) *event,
gtk_widget_set_sensitive(icon->button, sensitive);
/* Icons with an associated menu item */
if(icon->item) {
- if(icon->icon_off)
- gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item), on);
+ if(icon->toggle)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item),
+ !!icon->menu_invert ^ !!on);
gtk_widget_set_sensitive(icon->item, sensitive);
}
--suppress_actions;
@@ -392,13 +435,22 @@ static void icon_action_completed(void attribute((unused)) *v,
popup_protocol_error(0, err);
}
-static void clicked_icon(GtkButton attribute((unused)) *button,
+static void clicked_icon(GtkToolButton attribute((unused)) *button,
gpointer userdata) {
const struct icon *icon = userdata;
if(suppress_actions)
return;
- if(!icon->on || icon->on())
+ icon->action_go_off(client, icon_action_completed, 0);
+}
+
+static void toggled_icon(GtkToggleToolButton attribute((unused)) *button,
+ gpointer user_data) {
+ const struct icon *icon = user_data;
+
+ if(suppress_actions)
+ return;
+ if(icon->on())
icon->action_go_off(client, icon_action_completed, 0);
else
icon->action_go_on(client, icon_action_completed, 0);
@@ -411,7 +463,7 @@ static void clicked_menu(GtkMenuItem attribute((unused)) *menuitem,
static void toggled_menu(GtkCheckMenuItem attribute((unused)) *menuitem,
gpointer userdata) {
- clicked_icon(NULL, userdata);
+ toggled_icon(NULL, userdata);
}
/** @brief Called when a volume command completes */
@@ -559,6 +611,20 @@ static int disable_rtp(disorder_eclient attribute((unused)) *c,
return 0;
}
+static void control_minimode(const char attribute((unused)) *event,
+ void attribute((unused)) *evendata,
+ void attribute((unused)) *callbackdata) {
+ if(full_mode && volume_supported()) {
+ gtk_widget_show(balance_widget);
+ gtk_scale_set_value_pos(GTK_SCALE(volume_widget), GTK_POS_TOP);
+ } else {
+ gtk_widget_hide(balance_widget);
+ gtk_scale_set_value_pos(GTK_SCALE(volume_widget), GTK_POS_RIGHT);
+ }
+ gtk_toolbar_set_style(GTK_TOOLBAR(toolbar),
+ full_mode ? GTK_TOOLBAR_BOTH : GTK_TOOLBAR_ICONS);
+}
+
/*
Local Variables:
c-basic-offset:2
diff --git a/disobedience/disobedience.c b/disobedience/disobedience.c
index 9b050f4..639eb73 100644
--- a/disobedience/disobedience.c
+++ b/disobedience/disobedience.c
@@ -44,6 +44,9 @@ GtkWidget *report_label;
/** @brief Main tab group */
GtkWidget *tabs;
+/** @brief Mini-mode widget for playing track */
+GtkWidget *playing_mini;
+
/** @brief Main client */
disorder_eclient *client;
@@ -100,6 +103,10 @@ const char *server_version;
/** @brief Parsed server version */
long server_version_bytes;
+static GtkWidget *queue;
+
+static GtkWidget *notebook_box;
+
static void check_rtp_address(const char *event,
void *eventdata,
void *callbackdata);
@@ -154,7 +161,7 @@ static GtkWidget *notebook(void) {
* produces not too dreadful appearance */
gtk_widget_set_style(tabs, tool_style);
g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0);
- gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(),
+ gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue = queue_widget(),
gtk_label_new("Queue"));
gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(),
gtk_label_new("Recent"));
@@ -165,18 +172,78 @@ static GtkWidget *notebook(void) {
return tabs;
}
+/* Tracking of window sizes */
+static int toplevel_width = 640, toplevel_height = 480;
+static int mini_width = 480, mini_height = 140;
+static struct timeval last_mode_switch;
+
+static void main_minimode(const char attribute((unused)) *event,
+ void attribute((unused)) *evendata,
+ void attribute((unused)) *callbackdata) {
+ if(full_mode) {
+ gtk_window_resize(GTK_WINDOW(toplevel), toplevel_width, toplevel_height);
+ gtk_widget_show(tabs);
+ gtk_widget_hide(playing_mini);
+ /* Show the queue (bit confusing otherwise!) */
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(tabs), 0);
+ } else {
+ gtk_window_resize(GTK_WINDOW(toplevel), mini_width, mini_height);
+ gtk_widget_hide(tabs);
+ gtk_widget_show(playing_mini);
+ }
+ xgettimeofday(&last_mode_switch, NULL);
+}
+
+/* Called when the window size is allocate */
+static void toplevel_size_allocate(GtkWidget attribute((unused)) *w,
+ GtkAllocation *a,
+ gpointer attribute((unused)) user_data) {
+ struct timeval now;
+ xgettimeofday(&now, NULL);
+ if(tvdouble(tvsub(now, last_mode_switch)) < 0.5) {
+ /* Suppress size-allocate signals that are within half a second of a mode
+ * switch: they are quite likely to be the result of re-arranging widgets
+ * within the old size, not the application of the new size. Yes, this is
+ * a disgusting hack! */
+ return; /* OMG too soon! */
+ }
+ if(full_mode) {
+ toplevel_width = a->width;
+ toplevel_height = a->height;
+ } else {
+ mini_width = a->width;
+ mini_height = a->height;
+ }
+}
+
+/* Periodically check the toplevel's size
+ * (the hack in toplevel_size_allocate() means we could in principle
+ * miss a user-initiated resize)
+ */
+static void check_toplevel_size(const char attribute((unused)) *event,
+ void attribute((unused)) *evendata,
+ void attribute((unused)) *callbackdata) {
+ GtkAllocation a;
+ gtk_window_get_size(GTK_WINDOW(toplevel), &a.width, &a.height);
+ toplevel_size_allocate(NULL, &a, NULL);
+}
+
/** @brief Create and populate the main window */
static void make_toplevel_window(void) {
- GtkWidget *const vbox = gtk_vbox_new(FALSE, 1);
+ GtkWidget *const vbox = gtk_vbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
GtkWidget *const rb = report_box();
D(("top_window"));
toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL);
/* default size is too small */
- gtk_window_set_default_size(GTK_WINDOW(toplevel), 640, 480);
+ gtk_window_set_default_size(GTK_WINDOW(toplevel),
+ toplevel_width, toplevel_height);
/* terminate on close */
g_signal_connect(G_OBJECT(toplevel), "delete_event",
G_CALLBACK(delete_event), NULL);
+ /* track size */
+ g_signal_connect(G_OBJECT(toplevel), "size-allocate",
+ G_CALLBACK(toplevel_size_allocate), NULL);
/* lay out the window */
gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience");
gtk_container_add(GTK_CONTAINER(toplevel), vbox);
@@ -191,13 +258,23 @@ static void make_toplevel_window(void) {
FALSE, /* expand */
FALSE, /* fill */
0);
- gtk_container_add(GTK_CONTAINER(vbox), notebook());
+ playing_mini = playing_widget();
+ gtk_box_pack_start(GTK_BOX(vbox),
+ playing_mini,
+ FALSE,
+ FALSE,
+ 0);
+ notebook_box = gtk_vbox_new(FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(notebook_box), notebook());
+ gtk_container_add(GTK_CONTAINER(vbox), notebook_box);
gtk_box_pack_end(GTK_BOX(vbox),
rb,
FALSE, /* expand */
FALSE, /* fill */
0);
gtk_widget_set_style(toplevel, tool_style);
+ event_register("mini-mode-changed", main_minimode, 0);
+ event_register("periodic-fast", check_toplevel_size, 0);
}
static void userinfo_rights_completed(void attribute((unused)) *v,
@@ -477,6 +554,7 @@ int main(int argc, char **argv) {
/* reset styles now everything has its name */
gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default()));
gtk_widget_show_all(toplevel);
+ gtk_widget_hide(playing_mini);
/* issue a NOP every so often */
g_timeout_add_full(G_PRIORITY_LOW,
2000/*interval, ms*/,
@@ -490,9 +568,7 @@ int main(int argc, char **argv) {
disorder_eclient_version(client, version_completed, 0);
event_register("log-connected", check_rtp_address, 0);
suppress_actions = 0;
-#if PLAYLISTS
playlists_init();
-#endif
/* If no password is set yet pop up a login box */
if(!config->password)
login_box();
diff --git a/disobedience/disobedience.h b/disobedience/disobedience.h
index cafa48d..719eea4 100644
--- a/disobedience/disobedience.h
+++ b/disobedience/disobedience.h
@@ -85,6 +85,11 @@ struct button {
void (*clicked)(GtkButton *button, gpointer userdata);
const char *tip;
GtkWidget *widget;
+ void (*pack)(GtkBox *box,
+ GtkWidget *child,
+ gboolean expand,
+ gboolean fill,
+ guint padding);
};
/* Variables --------------------------------------------------------------- */
@@ -117,7 +122,8 @@ void popup_protocol_error(int code,
const char *msg);
/* Report an error */
-void properties(int ntracks, const char **tracks);
+void properties(int ntracks, const char **tracks,
+ GtkWidget *parent);
/* Pop up a properties window for a list of tracks */
GtkWidget *scroll_widget(GtkWidget *child);
@@ -134,7 +140,8 @@ void popup_submsg(GtkWidget *parent, GtkMessageType mt, const char *msg);
void fpopup_msg(GtkMessageType mt, const char *fmt, ...);
-struct progress_window *progress_window_new(const char *title);
+struct progress_window *progress_window_new(const char *title,
+ GtkWidget *parent);
/* Pop up a progress window */
void progress_window_progress(struct progress_window *pw,
@@ -159,6 +166,7 @@ void all_update(void);
GtkWidget *menubar(GtkWidget *w);
/* Create the menu bar */
+int full_mode;
void users_set_sensitive(int sensitive);
@@ -172,6 +180,7 @@ extern int suppress_actions;
/* Queue/Recent/Added */
GtkWidget *queue_widget(void);
+GtkWidget *playing_widget(void);
GtkWidget *recent_widget(void);
GtkWidget *added_widget(void);
/* Create widgets for displaying the queue, the recently played list and the
@@ -212,6 +221,8 @@ void choose_update(void);
void play_completed(void *v,
const char *err);
+extern const GtkTargetEntry choose_targets[];
+
/* Login details */
void login_box(void);
@@ -224,7 +235,7 @@ void manage_users(void);
/* Help */
-void popup_help(void);
+void popup_help(const char *what);
/* RTP */
@@ -253,17 +264,15 @@ void popup_settings(void);
/* Playlists */
-#if PLAYLISTS
void playlists_init(void);
-void edit_playlists(gpointer callback_data,
- guint callback_action,
- GtkWidget *menu_item);
+void playlist_window_create(gpointer callback_data,
+ guint callback_action,
+ GtkWidget *menu_item);
extern char **playlists;
extern int nplaylists;
-extern GtkWidget *playlists_widget;
+extern GtkWidget *menu_playlists_widget;
extern GtkWidget *playlists_menu;
-extern GtkWidget *editplaylists_widget;
-#endif
+extern GtkWidget *menu_editplaylists_widget;
#endif /* DISOBEDIENCE_H */
diff --git a/disobedience/help.c b/disobedience/help.c
index 8ec405c..ca5a8d8 100644
--- a/disobedience/help.c
+++ b/disobedience/help.c
@@ -24,12 +24,23 @@
#include
/** @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()) {
diff --git a/disobedience/log.c b/disobedience/log.c
index f1c4f79..71d2af0 100644
--- a/disobedience/log.c
+++ b/disobedience/log.c
@@ -115,6 +115,7 @@ static void log_moved(void attribute((unused)) *v,
static void log_playing(void attribute((unused)) *v,
const char attribute((unused)) *track,
const char attribute((unused)) *user) {
+ event_raise("playing-started", 0);
}
/** @brief Called when a track is added to the queue */
diff --git a/disobedience/login.c b/disobedience/login.c
index 90eaa0d..0c6e7d7 100644
--- a/disobedience/login.c
+++ b/disobedience/login.c
@@ -232,6 +232,12 @@ static void login_cancel(GtkButton attribute((unused)) *button,
gtk_widget_destroy(login_window);
}
+/** @brief User pressed cancel in the login window */
+static void login_help(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ popup_help("intro.html#login");
+}
+
/** @brief Keypress handler */
static gboolean login_keypress(GtkWidget attribute((unused)) *widget,
GdkEventKey *event,
@@ -253,16 +259,25 @@ static gboolean login_keypress(GtkWidget attribute((unused)) *widget,
/* Buttons that appear at the bottom of the window */
static struct button buttons[] = {
{
- "Login",
- login_ok,
- "(Re-)connect using these settings",
- 0
+ GTK_STOCK_HELP,
+ login_help,
+ "Go to manual",
+ 0,
+ gtk_box_pack_start,
},
{
GTK_STOCK_CLOSE,
login_cancel,
"Discard changes and close window",
- 0
+ 0,
+ gtk_box_pack_end,
+ },
+ {
+ "Login",
+ login_ok,
+ "(Re-)connect using these settings",
+ 0,
+ gtk_box_pack_end,
},
};
diff --git a/disobedience/manual/Makefile.am b/disobedience/manual/Makefile.am
new file mode 100644
index 0000000..cd607f0
--- /dev/null
+++ b/disobedience/manual/Makefile.am
@@ -0,0 +1,30 @@
+#
+# This file is part of DisOrder.
+# Copyright (C) 2009 Richard Kettlewell
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+dist_dochtml_DATA=index.html intro.html misc.html playlists.html \
+ properties.html tabs.html window.html disobedience.css \
+ arch-simple.png button-pause.png button-playing.png \
+ button-random.png button-rtp.png button-scratch.png \
+ choose-search.png choose.png disobedience-debian-menu.png \
+ disobedience-terminal.png disorder-email-confirm.png \
+ disorder-web-login.png login.png menu-control.png \
+ menu-edit.png menu-help.png menu-server.png \
+ playlist-create.png playlist-picker-menu.png \
+ playlist-popup-menu.png playlist-window.png queue-menu.png \
+ queue.png queue2.png recent.png track-properties.png \
+ volume-slider.png
diff --git a/disobedience/manual/arch-simple.png b/disobedience/manual/arch-simple.png
new file mode 100644
index 0000000..c7d965a
Binary files /dev/null and b/disobedience/manual/arch-simple.png differ
diff --git a/disobedience/manual/arch-simple.svg b/disobedience/manual/arch-simple.svg
new file mode 100644
index 0000000..a03d78a
--- /dev/null
+++ b/disobedience/manual/arch-simple.svg
@@ -0,0 +1,220 @@
+
+
+
diff --git a/disobedience/manual/button-pause.png b/disobedience/manual/button-pause.png
new file mode 100644
index 0000000..565f227
Binary files /dev/null and b/disobedience/manual/button-pause.png differ
diff --git a/disobedience/manual/button-playing.png b/disobedience/manual/button-playing.png
new file mode 100644
index 0000000..7b6fb2a
Binary files /dev/null and b/disobedience/manual/button-playing.png differ
diff --git a/disobedience/manual/button-random.png b/disobedience/manual/button-random.png
new file mode 100644
index 0000000..9791c2c
Binary files /dev/null and b/disobedience/manual/button-random.png differ
diff --git a/disobedience/manual/button-rtp.png b/disobedience/manual/button-rtp.png
new file mode 100644
index 0000000..ad0d749
Binary files /dev/null and b/disobedience/manual/button-rtp.png differ
diff --git a/disobedience/manual/button-scratch.png b/disobedience/manual/button-scratch.png
new file mode 100644
index 0000000..c90bfc5
Binary files /dev/null and b/disobedience/manual/button-scratch.png differ
diff --git a/disobedience/manual/choose-search.png b/disobedience/manual/choose-search.png
new file mode 100644
index 0000000..bc28194
Binary files /dev/null and b/disobedience/manual/choose-search.png differ
diff --git a/disobedience/manual/choose.png b/disobedience/manual/choose.png
new file mode 100644
index 0000000..2f88ece
Binary files /dev/null and b/disobedience/manual/choose.png differ
diff --git a/disobedience/manual/disobedience-debian-menu.png b/disobedience/manual/disobedience-debian-menu.png
new file mode 100644
index 0000000..49bbdb4
Binary files /dev/null and b/disobedience/manual/disobedience-debian-menu.png differ
diff --git a/disobedience/manual/disobedience-terminal.png b/disobedience/manual/disobedience-terminal.png
new file mode 100644
index 0000000..5cf2e8d
Binary files /dev/null and b/disobedience/manual/disobedience-terminal.png differ
diff --git a/disobedience/manual/disobedience.css b/disobedience/manual/disobedience.css
new file mode 100644
index 0000000..04b6f80
--- /dev/null
+++ b/disobedience/manual/disobedience.css
@@ -0,0 +1,91 @@
+/*
+This file is part of DisOrder.
+Copyright (C) 2009 Richard Kettlewell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+/* default font and colors */
+body {
+ color: black;
+ background-color: white;
+ font-family: times,serif;
+ font-weight: normal;
+ font-size: 12pt;
+ font-variant: normal
+}
+
+/* general link colors */
+a {
+ color: blue;
+ text-decoration: none
+}
+
+h2 a {
+ color: black
+}
+
+p.chapter a {
+ color: black;
+ font-family: helvetica,sans-serif;
+ font-weight: bold;
+ font-size: 18pt
+}
+
+a:active {
+ color: red
+}
+
+a:visited {
+ color: red
+}
+
+/* title bars */
+h1 {
+ font-family: helvetica,sans-serif;
+ font-weight: bold;
+ font-size: 18pt;
+ font-variant: normal;
+ text-align: center;
+ border: 1px solid black;
+ padding: 0.2em;
+ background-color: #e0e0e0;
+ display: block
+}
+
+/* secondary titles */
+h2 {
+ font-family: helvetica,sans-serif;
+ font-weight: bold;
+ font-size: 16pt;
+ font-variant: normal;
+ display: block
+}
+
+td {
+ vertical-align: top;
+ padding: 8px
+}
+
+td:first-child {
+ text-align: right
+}
+
+table {
+ margin-left: 2em
+}
+
+p.image {
+ text-align: center
+}
diff --git a/disobedience/manual/disorder-email-confirm.png b/disobedience/manual/disorder-email-confirm.png
new file mode 100644
index 0000000..c535a67
Binary files /dev/null and b/disobedience/manual/disorder-email-confirm.png differ
diff --git a/disobedience/manual/disorder-web-login.png b/disobedience/manual/disorder-web-login.png
new file mode 100644
index 0000000..3155a4c
Binary files /dev/null and b/disobedience/manual/disorder-web-login.png differ
diff --git a/disobedience/manual/index.html b/disobedience/manual/index.html
new file mode 100644
index 0000000..7245024
--- /dev/null
+++ b/disobedience/manual/index.html
@@ -0,0 +1,75 @@
+
+
+
+
+ Disobedience
+
+
+
+
Disobedience
+
+
This is the manual for Disobedience, the graphical client
+ for DisOrder.
DisOrder
+ 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.
+
+
DisOrder has three main user interfaces.
+
+
+
It has a command-line interface, suitable for ad-hoc use and
+ scripting.
+
+
It has a web interface, usable with graphical web browsers
+ (Firefox, Internet Explorer etc).
+
+
It has a graphical client called Disobedience.
+
+
+
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.
If you have a Debian system you can download the .deb
+ files from
+ DisOrder's home page and install those. There are four
+ packages to choose from:
+
+
+
disorder.deb - the base package. You should always
+ install this. It contains the command-line client.
+
+
disorder-server.deb - 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.
+
+
disobedience.deb - the graphical client. If you are
+ reading this manual you want this package!
+
+
disorder-rtp.deb - 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.
+
+
+
+
(At the time of writing, DisOrder is not included as part of
+ Debian.)
+
+
If you have another kind of Linux system, or a Mac, you must
+ build from source code. See the README file included in
+ the source distribution for more details. Note that to use
+ Disobedience on a Mac, you will need X11.app.
+
+
There is no Windows support (although the web interface can be
+ used from Windows computers).
The easiest way to get a DisOrder login is to access the web
+ interface and set one up using that. To do this,
+ visit http://HOSTNAME/cgi-bin/disorder,
+ where HOSTNAME is the name of the server where DisOrder is
+ installed. You should then be able to select the Login
+ option at the top of the screen.
+
+
+
+
Go to the New Users 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 Register, you will
+ be sent an email requiring you to confirm your registration.
+
+
+
+
Your login won't be active until you click on this URL.
+
+
(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.)
+
+
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.
On Debian systems it should be possible to find Disobedience in
+ the menu system:
+
+
+
+
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 & suffix to stop it tying up your terminal.
+
+
+
+
(Please note that Disobedience shouldn't write any messages to
+ the terminal. If it does that probably indicates a bug, which
+ should be
+ reported.)
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.
+
+
+
+
If Disobedience is running on a different computer to the
+ server, then you should make sure the Remote 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 same computer as the server then you can
+ leave the Remote box clear and it should be able to connect
+ to it without using the network.
+
+
In any case, you will need to enter your username and
+ password, as set up earlier.
+
+
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 Server > Login option to bring the login window
+ back, or (if you prefer), edit the file ~/.disorder/passwd
+ directly.
+
+
+
+ Back to contents
+
+
+
diff --git a/disobedience/manual/login.png b/disobedience/manual/login.png
new file mode 100644
index 0000000..9f3b074
Binary files /dev/null and b/disobedience/manual/login.png differ
diff --git a/disobedience/manual/menu-control.png b/disobedience/manual/menu-control.png
new file mode 100644
index 0000000..c2da17b
Binary files /dev/null and b/disobedience/manual/menu-control.png differ
diff --git a/disobedience/manual/menu-edit.png b/disobedience/manual/menu-edit.png
new file mode 100644
index 0000000..7cdb83b
Binary files /dev/null and b/disobedience/manual/menu-edit.png differ
diff --git a/disobedience/manual/menu-help.png b/disobedience/manual/menu-help.png
new file mode 100644
index 0000000..4ecc391
Binary files /dev/null and b/disobedience/manual/menu-help.png differ
diff --git a/disobedience/manual/menu-server.png b/disobedience/manual/menu-server.png
new file mode 100644
index 0000000..5502bf5
Binary files /dev/null and b/disobedience/manual/menu-server.png differ
diff --git a/disobedience/manual/misc.html b/disobedience/manual/misc.html
new file mode 100644
index 0000000..b65b6fa
--- /dev/null
+++ b/disobedience/manual/misc.html
@@ -0,0 +1,106 @@
+
+
+
+
+ Disobedience: Appendix
+
+
+
+
Network play uses a background copy
+ of disorder-playrtp. If you quit Disobedience the
+ player will continue playing and can be disabled from a later
+ run of Disobedience.
+
+
The player will log to ~/.disorder/HOSTNAME-rtp.log so
+ look there if it does not seem to be working.
+
+
You can stop it without running Disobedience by the command
+ killall disorder-playrtp.
Please report bugs using
+ DisOrder's bug
+ tracker.
+
+
Known problems include:
+
+
+
+
There is no particular provision for multiple users of the
+ same computer sharing a single disorder-playrtp process.
+ This shouldn't be too much of a problem in practice but something
+ could perhaps be done given demand.
+
+
Try to do remote user management when the server is
+ configured to refuse this produces rather horrible error
+ behavior.
+
+
Resizing columns doesn't work very well. This is a GTK+
+ bug.
This program is free software: you can 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/.
+
+
+
+ Back to contents
+
+
+
diff --git a/disobedience/manual/playlist-create.png b/disobedience/manual/playlist-create.png
new file mode 100644
index 0000000..f48997d
Binary files /dev/null and b/disobedience/manual/playlist-create.png differ
diff --git a/disobedience/manual/playlist-picker-menu.png b/disobedience/manual/playlist-picker-menu.png
new file mode 100644
index 0000000..be5a65b
Binary files /dev/null and b/disobedience/manual/playlist-picker-menu.png differ
diff --git a/disobedience/manual/playlist-popup-menu.png b/disobedience/manual/playlist-popup-menu.png
new file mode 100644
index 0000000..0bb673d
Binary files /dev/null and b/disobedience/manual/playlist-popup-menu.png differ
diff --git a/disobedience/manual/playlist-window.png b/disobedience/manual/playlist-window.png
new file mode 100644
index 0000000..9ffd991
Binary files /dev/null and b/disobedience/manual/playlist-window.png differ
diff --git a/disobedience/manual/playlists.html b/disobedience/manual/playlists.html
new file mode 100644
index 0000000..bef0c18
--- /dev/null
+++ b/disobedience/manual/playlists.html
@@ -0,0 +1,118 @@
+
+
+
+
+ Disobedience: Playlists
+
+
+
+
5. Playlists
+
+
The chapter describes playlist and how to use them.
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.
+
+
Playlists fall into three categories:
+
+
+
+
Shared
+
Shared playlists have no owner and can be seen and edited
+ by anybody.
+
+
+
+
Public
+
Public playlist are owned by their creator and can be seen
+ by anybody. Only their creator can edit them, however.
+
+
+
+
Private
+
Private playlists are owned by their creator and can only
+ be seen or edited by their creator.
+
+
+
+
+
To bring up the playlist window, select Edit > Edit
+ Playlists from the menu.
To create a playlist, click the Add 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.
+
+
Only Roman letters (without any accents) and digits are allowed
+ in playlist names.
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).
+
+
+
+
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 queue. You can
+ rearrange tracks within it by drag and drop and you can drag tracks
+ from the Choose tab into it.
+
+
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 Queue tab.
The chapter describes how to edit track properties.
+
+
+
+
This window can be invoked from any of the four tabs by
+ selecting one or more tracks and then either selected Edit >
+ Track Properties or via the right-click pop-up menu.
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.
+
+
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:
+
+
+
+
In the Choose tab, select all the tracks in the album.
+
+
Select Edit > Track Properties to bring up the track
+ properties window.
+
+
Edit the album name in the first track.
+
+
Click the arrow button to the right of the corrected version.
Each track has an associated collection of tags. These can used
+ when searching for tracks in the Choose tab or to control
+ which tracks are picked at random (although this functionality is
+ not readily available in current versions of Disobedience).
+
+
To add tags to a track enter the tags you want to apply to it in
+ the Tags field, separated by commas. Tags cannot contain
+ commas and are compared without regard to whitespace.
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.)
+
+
If no weight has been explicitly set then the track gets a
+ default weight of 90,000.
+
+
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.
+
+
+
+ Back to contents
+
+
+
diff --git a/disobedience/manual/queue-menu.png b/disobedience/manual/queue-menu.png
new file mode 100644
index 0000000..af2dff9
Binary files /dev/null and b/disobedience/manual/queue-menu.png differ
diff --git a/disobedience/manual/queue.png b/disobedience/manual/queue.png
new file mode 100644
index 0000000..224fc71
Binary files /dev/null and b/disobedience/manual/queue.png differ
diff --git a/disobedience/manual/queue2.png b/disobedience/manual/queue2.png
new file mode 100644
index 0000000..ff5cbed
Binary files /dev/null and b/disobedience/manual/queue2.png differ
diff --git a/disobedience/manual/recent.png b/disobedience/manual/recent.png
new file mode 100644
index 0000000..8ee5e68
Binary files /dev/null and b/disobedience/manual/recent.png differ
diff --git a/disobedience/manual/tabs.html b/disobedience/manual/tabs.html
new file mode 100644
index 0000000..ea63a37
--- /dev/null
+++ b/disobedience/manual/tabs.html
@@ -0,0 +1,185 @@
+
+
+
+
+ Disobedience: Tabs
+
+
+
+
3. Tabs
+
+
The chapter contains detailed descriptions of the Queue, Recent,
+ Choose and Added tabs.
The Queue tab has already
+ been briefly described, but there
+ are some more things to say about. To start with, the meaning of
+ the columns:
+
+
+
+
When
+
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.
+
+
+
Who
+
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.
+
+
+
Artist
+
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.
+
+
+
Album
+
The album that the track came from.
+
+
+
Title
+
The title of the track.
+
+
+
Length
+
The length the track will play for. For the playing track
+ this will include the amount of time it's been playing so
+ far.
+
+
+
+
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).
+
+
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.
+
+
Right-clicking in the queue will create a pop-up menu:
+
+
+
+
Track Properties will create a window with editable
+ properties of each selected track. Scratch playing track
+ only works if the playing track is the selected track and will stop
+ it playing. Remove track from queue will remove the
+ selected (non-playing) tracks from the queue.
+
+
Adopt track will apply your name to one without an entry
+ in the Who 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.)
The Recent tab is similar in structure to the queue but
+ it shows tracks that have played recently. The When column
+ indicates when the track played rather than when it will
+ played.
+
+
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 Play track, which allows a recently played track
+ to be added back to the queue.
+
+
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.
The Choose tab contains all the tracks known to the
+ server, organized into a hierarchical structure based on the
+ underlying file and directory structure.
+
+
+
+
The boxes in the Queued 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.
+
+
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).
+
+
Right clicking will create a pop-up menu with what are hopefuly
+ now familiar options. Play track will add the selected
+ track(s) to the queue and Track Properties will create a
+ window with editable properties of each selected track.
+
+
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.
+
+
Selected tracks can also be dragged to the queue, by dragging
+ first to the Queue tab itself and then to the desired
+ location in the queue.
+
+
+
+
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).
+
+
If you enter more than one word at a time then only tracks which
+ match both words will be listed.
+
+
You can search for tags as
+ well as words. For instance to search for the tag
+ “happy” you would enter tag:happy in the
+ search box.
The Added 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 played tracks list.
+
+
+
+ Back to contents
+
+
+
diff --git a/disobedience/manual/track-properties.png b/disobedience/manual/track-properties.png
new file mode 100644
index 0000000..27093b8
Binary files /dev/null and b/disobedience/manual/track-properties.png differ
diff --git a/disobedience/manual/volume-slider.png b/disobedience/manual/volume-slider.png
new file mode 100644
index 0000000..c7e25c5
Binary files /dev/null and b/disobedience/manual/volume-slider.png differ
diff --git a/disobedience/manual/window.html b/disobedience/manual/window.html
new file mode 100644
index 0000000..72fe5f7
--- /dev/null
+++ b/disobedience/manual/window.html
@@ -0,0 +1,164 @@
+
+
+
+
+ Disobedience: Window Layout
+
+
+
+
2. Window Layout
+
+
This chapter contains a tour of the main Disobedience
+ window.
Disobedience should look something like this when you've started
+ it up and logged in:
+
+
+
+
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: Queue, Recent and so on. The Queue tab
+ is selected: this displays the currently playing track and the list
+ of tracks that will play in the near future.
+
+
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 Who column is empty.
+
+
In the screenshot below both of these things have changed.
+ Use rjk has selected some tracks and the first of them is
+ playing.
+
+
+
+
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.
The pause button. This only effective when a track is
+ playing. When it is pressed the playing track is paused.
+
+
+
+
+
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.
+
+
+
+
+
+
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.
+
+
+
+
+
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.
+
+
+
+
+
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.
+
+
+
+
To the right of the buttons are two sliders:
+
+
+
+
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).
The Server menu is loosely analogous to the File
+ 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 Login, which brings back the login window shown the
+ first time it is run, Manage Users which allows
+ adminstrators to do user management, and Quit.
+
+
+
+
+
+
The Edit 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.
+
+
The Track Properties option will create a window with
+ editable properties of each selected
+ track and the Edit Playlists option will create a window
+ allowing editing of playlists.
+
+
+
+
The Control menu options are mostly equivalent to the
+ buttons described above. The exceptions is Activate
+ Playlist which allows you to play
+ a playlist, and Compact Mode
+ which switches Disobedience's window to a smaller format.
+
+
+
+
The Help menu has an option to bring up the Disobedience
+ manual and an About option which will display a bit of
+ version information for the server and for Disobedience (which
+ might not be the same).
+
+
+
+ Back to contents
+
+
+
diff --git a/disobedience/menu.c b/disobedience/menu.c
index 0243139..bae0f00 100644
--- a/disobedience/menu.c
+++ b/disobedience/menu.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder.
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,18 +21,22 @@
#include "disobedience.h"
+static void toggled_minimode(GtkCheckMenuItem *item, gpointer userdata);
+
static GtkWidget *selectall_widget;
static GtkWidget *selectnone_widget;
static GtkWidget *properties_widget;
-#if PLAYLISTS
-GtkWidget *playlists_widget;
+GtkWidget *menu_playlists_widget;
GtkWidget *playlists_menu;
-GtkWidget *editplaylists_widget;
-#endif
+GtkWidget *menu_editplaylists_widget;
+static GtkWidget *menu_minimode_widget;
/** @brief Main menu widgets */
GtkItemFactory *mainmenufactory;
+/** @brief Set for full mode, clear for mini mode */
+int full_mode = 1;
+
static void about_popup_got_version(void *v,
const char *err,
const char *value);
@@ -136,7 +140,7 @@ static void manual_popup(gpointer attribute((unused)) callback_data,
GtkWidget attribute((unused)) *menu_item) {
D(("manual_popup"));
- popup_help();
+ popup_help(NULL);
}
/** @brief Called when version arrives, displays about... popup */
@@ -276,15 +280,15 @@ GtkWidget *menubar(GtkWidget *w) {
},
{
(char *)"/Edit/Select all tracks", /* path */
- 0, /* accelerator */
+ (char *)"A", /* accelerator */
menu_tab_action, /* callback */
offsetof(struct tabtype, selectall_activate), /* callback_action */
- 0, /* item_type */
- 0 /* extra_data */
+ (char *)"", /* item_type */
+ GTK_STOCK_SELECT_ALL, /* extra_data */
},
{
(char *)"/Edit/Deselect all tracks", /* path */
- 0, /* accelerator */
+ (char *)"A", /* accelerator */
menu_tab_action, /* callback */
offsetof(struct tabtype, selectnone_activate), /* callback_action */
0, /* item_type */
@@ -295,19 +299,17 @@ GtkWidget *menubar(GtkWidget *w) {
0, /* accelerator */
menu_tab_action, /* callback */
offsetof(struct tabtype, properties_activate), /* callback_action */
- 0, /* item_type */
- 0 /* extra_data */
+ (char *)"", /* item_type */
+ GTK_STOCK_PROPERTIES, /* extra_data */
},
-#if PLAYLISTS
{
(char *)"/Edit/Edit playlists", /* path */
0, /* accelerator */
- edit_playlists, /* callback */
+ playlist_window_create, /* callback */
0, /* callback_action */
0, /* item_type */
0 /* extra_data */
},
-#endif
{
@@ -323,8 +325,8 @@ GtkWidget *menubar(GtkWidget *w) {
(char *)"S", /* accelerator */
0, /* callback */
0, /* callback_action */
- 0, /* item_type */
- 0 /* extra_data */
+ (char *)"", /* item_type */
+ GTK_STOCK_STOP, /* extra_data */
},
{
(char *)"/Control/Playing", /* path */
@@ -350,7 +352,14 @@ GtkWidget *menubar(GtkWidget *w) {
(char *)"", /* item_type */
0 /* extra_data */
},
-#if PLAYLISTS
+ {
+ (char *)"/Control/Compact mode", /* path */
+ (char *)"M", /* accelerator */
+ 0, /* callback */
+ 0, /* callback_action */
+ (char *)"", /* item_type */
+ 0 /* extra_data */
+ },
{
(char *)"/Control/Activate playlist", /* path */
0, /* accelerator */
@@ -359,8 +368,7 @@ GtkWidget *menubar(GtkWidget *w) {
(char *)"", /* item_type */
0 /* extra_data */
},
-#endif
-
+
{
(char *)"/Help", /* path */
0, /* accelerator */
@@ -370,12 +378,12 @@ GtkWidget *menubar(GtkWidget *w) {
0 /* extra_data */
},
{
- (char *)"/Help/Manual page", /* path */
+ (char *)"/Help/Manual", /* path */
0, /* accelerator */
manual_popup, /* callback */
0, /* callback_action */
- 0, /* item_type */
- 0 /* extra_data */
+ (char *)"", /* item_type */
+ GTK_STOCK_HELP, /* extra_data */
},
{
(char *)"/Help/About DisOrder", /* path */
@@ -404,22 +412,20 @@ GtkWidget *menubar(GtkWidget *w) {
"/Edit/Deselect all tracks");
properties_widget = gtk_item_factory_get_widget(mainmenufactory,
"/Edit/Track properties");
-#if PLAYLISTS
- playlists_widget = gtk_item_factory_get_item(mainmenufactory,
+ menu_playlists_widget = gtk_item_factory_get_item(mainmenufactory,
"/Control/Activate playlist");
playlists_menu = gtk_item_factory_get_widget(mainmenufactory,
"/Control/Activate playlist");
- editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
+ menu_editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
"/Edit/Edit playlists");
-#endif
+ menu_minimode_widget = gtk_item_factory_get_widget(mainmenufactory,
+ "/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,
"/Edit");
@@ -430,9 +436,21 @@ GtkWidget *menubar(GtkWidget *w) {
m = gtk_item_factory_get_widget(mainmenufactory,
"");
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
diff --git a/disobedience/misc.c b/disobedience/misc.c
index 96e1750..a1fdf4d 100644
--- a/disobedience/misc.c
+++ b/disobedience/misc.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2008, 2010 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,7 +27,7 @@ struct image {
const guint8 *data;
};
-#include "images.h"
+#include "../images/images.h"
/* Miscellaneous GTK+ stuff ------------------------------------------------ */
@@ -187,7 +187,14 @@ GtkWidget *create_buttons_box(struct button *buttons,
gtk_widget_set_style(buttons[n].widget, tool_style);
g_signal_connect(G_OBJECT(buttons[n].widget), "clicked",
G_CALLBACK(buttons[n].clicked), 0);
- gtk_box_pack_start(GTK_BOX(box), buttons[n].widget, FALSE, FALSE, 1);
+ void (*pack)(GtkBox *box,
+ GtkWidget *child,
+ gboolean expand,
+ gboolean fill,
+ guint padding);
+ if(!(pack = buttons[n].pack))
+ pack = gtk_box_pack_start;
+ pack(GTK_BOX(box), buttons[n].widget, FALSE, FALSE, 1);
gtk_widget_set_tooltip_text(buttons[n].widget, buttons[n].tip);
}
return box;
diff --git a/disobedience/multidrag.c b/disobedience/multidrag.c
index 8b28c94..f4a5a76 100644
--- a/disobedience/multidrag.c
+++ b/disobedience/multidrag.c
@@ -92,8 +92,9 @@ static gboolean multidrag_button_press_event(GtkWidget *w,
/* We are only interested in left-button behavior */
if(event->button != 1)
return FALSE;
- /* We are only interested in unmodified clicks (not SHIFT etc) */
- if(event->state & GDK_MODIFIER_MASK)
+ /* We are only uninterested in clicks without CTRL or SHIFT. GTK ignores the
+ * other possible modifiers, so we do too. */
+ if(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK))
return FALSE;
/* We are only interested if a well-defined path is clicked */
GtkTreePath *path = NULL;
diff --git a/disobedience/playlists.c b/disobedience/playlists.c
index cd8979d..c95db43 100644
--- a/disobedience/playlists.c
+++ b/disobedience/playlists.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2008 Richard Kettlewell
+ * Copyright (C) 2008, 2009 Richard Kettlewell
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,37 +18,178 @@
* USA
*/
/** @file disobedience/playlists.c
- * @brief Playlist for Disobedience
+ * @brief Playlist support for Disobedience
*
* The playlists management window contains:
- * - a list of all playlists
+ * - the playlist picker (a list of all playlists) TODO should be a tree!
* - an add button
* - a delete button
- * - a drag+drop capable view of the playlist
- * - a close button
+ * - the playlist editor (a d+d-capable view of the currently picked playlist)
+ * - a close button TODO
+ *
+ * This file also maintains the playlist menu, allowing playlists to be
+ * activated from the main window's menu.
+ *
+ * Internally we maintain the playlist list, which is just the current list of
+ * playlists. Changes to this are reflected in the playlist menu and the
+ * playlist picker.
+ *
*/
#include "disobedience.h"
+#include "queue-generic.h"
+#include "popup.h"
+#include "validity.h"
-#if PLAYLISTS
-
-static void playlists_updated(void *v,
- const char *err,
- int nvec, char **vec);
+static void playlist_list_received_playlists(void *v,
+ const char *err,
+ int nvec, char **vec);
+static void playlist_editor_fill(const char *event,
+ void *eventdata,
+ void *callbackdata);
+static int playlist_playall_sensitive(void *extra);
+static void playlist_playall_activate(GtkMenuItem *menuitem,
+ gpointer user_data);
+static int playlist_remove_sensitive(void *extra) ;
+static void playlist_remove_activate(GtkMenuItem *menuitem,
+ gpointer user_data);
+static void playlist_new_locked(void *v, const char *err);
+static void playlist_new_retrieved(void *v, const char *err,
+ int nvec,
+ char **vec);
+static void playlist_new_created(void *v, const char *err);
+static void playlist_new_unlocked(void *v, const char *err);
+static void playlist_new_entry_edited(GtkEditable *editable,
+ gpointer user_data);
+static void playlist_new_button_toggled(GtkToggleButton *tb,
+ gpointer userdata);
+static void playlist_new_changed(const char *event,
+ void *eventdata,
+ void *callbackdata);
+static const char *playlist_new_valid(void);
+static void playlist_new_details(char **namep,
+ char **fullnamep,
+ gboolean *sharedp,
+ gboolean *publicp,
+ gboolean *privatep);
+static void playlist_new_ok(GtkButton *button,
+ gpointer userdata);
+static void playlist_new_cancel(GtkButton *button,
+ gpointer userdata);
+static void playlists_editor_received_tracks(void *v,
+ const char *err,
+ int nvec, char **vec);
+static void playlist_window_destroyed(GtkWidget *widget,
+ GtkWidget **widget_pointer);
+static gboolean playlist_window_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static int playlistcmp(const void *ap, const void *bp);
+static void playlist_modify_locked(void *v, const char *err);
+void playlist_modify_retrieved(void *v, const char *err,
+ int nvec,
+ char **vec);
+static void playlist_modify_updated(void *v, const char *err);
+static void playlist_modify_unlocked(void *v, const char *err);
+static void playlist_drop(struct queuelike *ql,
+ int ntracks,
+ char **tracks, char **ids,
+ struct queue_entry *after_me);
+struct playlist_modify_data;
+static void playlist_drop_modify(struct playlist_modify_data *mod,
+ int nvec, char **vec);
+static void playlist_remove_modify(struct playlist_modify_data *mod,
+ int nvec, char **vec);
+static gboolean playlist_new_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static gboolean playlist_picker_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+ gpointer userdata);
+static void playlist_editor_set_buttons(const char *event,
+ void *eventdata,
+ void *callbackdata);
+static void playlist_editor_got_share(void *v,
+ const char *err,
+ const char *value);
+static void playlist_editor_share_set(void *v, const char *err);
+static void playlist_picker_update_section(const char *title, const char *key,
+ int start, int end);
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+ const char *title, const char *key,
+ GtkTreeIter iter[1],
+ gboolean create);
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+ char **exists,
+ int nexists);
+static gboolean playlist_picker_button(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer user_data);
+static gboolean playlist_editor_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static void playlist_editor_ok(GtkButton *button, gpointer userdata);
+static void playlist_editor_help(GtkButton *button, gpointer userdata);
/** @brief Playlist editing window */
-static GtkWidget *playlists_window;
+static GtkWidget *playlist_window;
-/** @brief Tree model for list of playlists */
-static GtkListStore *playlists_list;
+/** @brief Columns for the playlist editor */
+static const struct queue_column playlist_columns[] = {
+ { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
+ { "Album", column_namepart, "album", COL_EXPAND|COL_ELLIPSIZE },
+ { "Title", column_namepart, "title", COL_EXPAND|COL_ELLIPSIZE },
+};
-/** @brief Selection for list of playlists */
-static GtkTreeSelection *playlists_selection;
+/** @brief Pop-up menu for playlist editor
+ *
+ * Status:
+ * - track properties works but, bizarrely, raises the main window
+ * - play track works
+ * - play playlist works
+ * - select/deselect all work
+ */
+static struct menuitem playlist_menuitems[] = {
+ { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+ { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+ { "Play playlist", NULL, playlist_playall_activate, playlist_playall_sensitive, 0, 0 },
+ { "Remove track from playlist", GTK_STOCK_DELETE, playlist_remove_activate, playlist_remove_sensitive, 0, 0 },
+ { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+ { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+};
-/** @brief Currently selected playlist */
-static const char *playlists_selected;
+static const GtkTargetEntry playlist_targets[] = {
+ {
+ PLAYLIST_TRACKS, /* drag type */
+ GTK_TARGET_SAME_WIDGET, /* rearrangement within a widget */
+ PLAYLIST_TRACKS_ID /* ID value */
+ },
+ {
+ PLAYABLE_TRACKS, /* drag type */
+ GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
+ PLAYABLE_TRACKS_ID, /* ID value */
+ },
+ {
+ .target = NULL
+ }
+};
-/** @brief Delete button */
-static GtkWidget *playlists_delete_button;
+/** @brief Queuelike for editing a playlist */
+static struct queuelike ql_playlist = {
+ .name = "playlist",
+ .columns = playlist_columns,
+ .ncolumns = sizeof playlist_columns / sizeof *playlist_columns,
+ .menuitems = playlist_menuitems,
+ .nmenuitems = sizeof playlist_menuitems / sizeof *playlist_menuitems,
+ .drop = playlist_drop,
+ .drag_source_targets = playlist_targets,
+ .drag_source_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+ .drag_dest_targets = playlist_targets,
+ .drag_dest_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+};
+
+/* Maintaining the list of playlists ---------------------------------------- */
/** @brief Current list of playlists or NULL */
char **playlists;
@@ -56,11 +197,31 @@ char **playlists;
/** @brief Count of playlists */
int nplaylists;
-/** @brief Schedule an update to the list of playlists */
-static void playlists_update(const char attribute((unused)) *event,
- void attribute((unused)) *eventdata,
- void attribute((unused)) *callbackdata) {
- disorder_eclient_playlists(client, playlists_updated, 0);
+/** @brief Schedule an update to the list of playlists
+ *
+ * Called periodically and when a playlist is created or deleted.
+ */
+static void playlist_list_update(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
+ disorder_eclient_playlists(client, playlist_list_received_playlists, 0);
+}
+
+/** @brief Called with a new list of playlists */
+static void playlist_list_received_playlists(void attribute((unused)) *v,
+ const char *err,
+ int nvec, char **vec) {
+ if(err) {
+ playlists = 0;
+ nplaylists = -1;
+ /* Probably means server does not support playlists */
+ } else {
+ playlists = vec;
+ nplaylists = nvec;
+ qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
+ }
+ /* Tell our consumers */
+ event_raise("playlists-updated", 0);
}
/** @brief qsort() callback for playlist name comparison */
@@ -89,244 +250,1380 @@ static int playlistcmp(const void *ap, const void *bp) {
return strcmp(a, b);
}
-/** @brief Called with a new list of playlists */
-static void playlists_updated(void attribute((unused)) *v,
- const char *err,
- int nvec, char **vec) {
+/* Playlists menu ----------------------------------------------------------- */
+
+static void playlist_menu_playing(void attribute((unused)) *v,
+ const char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+
+/** @brief Play received playlist contents
+ *
+ * Passed as a completion callback by menu_activate_playlist().
+ */
+static void playlist_menu_received_content(void attribute((unused)) *v,
+ const char *err,
+ int nvec, char **vec) {
if(err) {
- playlists = 0;
- nplaylists = -1;
- /* Probably means server does not support playlists */
- } else {
- playlists = vec;
- nplaylists = nvec;
- qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ return;
}
- /* Tell our consumers */
- event_raise("playlists-updated", 0);
+ for(int n = 0; n < nvec; ++n)
+ disorder_eclient_play(client, vec[n], playlist_menu_playing, NULL);
}
-/** @brief Called to activate a playlist */
-static void menu_activate_playlist(GtkMenuItem *menuitem,
+/** @brief Called to activate a playlist
+ *
+ * Called when the menu item for a playlist is clicked.
+ */
+static void playlist_menu_activate(GtkMenuItem *menuitem,
gpointer attribute((unused)) user_data) {
GtkLabel *label = GTK_LABEL(GTK_BIN(menuitem)->child);
const char *playlist = gtk_label_get_text(label);
- fprintf(stderr, "activate playlist %s\n", playlist); /* TODO */
+ disorder_eclient_playlist_get(client, playlist_menu_received_content,
+ playlist, NULL);
}
-/** @brief Called when the playlists change */
-static void menu_playlists_changed(const char attribute((unused)) *event,
- void attribute((unused)) *eventdata,
- void attribute((unused)) *callbackdata) {
+/** @brief Called when the playlists change
+ *
+ * Naively refills the menu. The results might be unsettling if the menu is
+ * currently open, but this is hopefuly fairly rare.
+ */
+static void playlist_menu_changed(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
if(!playlists_menu)
return; /* OMG too soon */
GtkMenuShell *menu = GTK_MENU_SHELL(playlists_menu);
- /* TODO: we could be more sophisticated and only insert/remove widgets as
- * needed. For now that's too much effort. */
while(menu->children)
gtk_container_remove(GTK_CONTAINER(menu), GTK_WIDGET(menu->children->data));
/* NB nplaylists can be -1 as well as 0 */
for(int n = 0; n < nplaylists; ++n) {
GtkWidget *w = gtk_menu_item_new_with_label(playlists[n]);
- g_signal_connect(w, "activate", G_CALLBACK(menu_activate_playlist), 0);
+ g_signal_connect(w, "activate", G_CALLBACK(playlist_menu_activate), 0);
gtk_widget_show(w);
gtk_menu_shell_append(menu, w);
}
- gtk_widget_set_sensitive(playlists_widget,
+ gtk_widget_set_sensitive(menu_playlists_widget,
nplaylists > 0);
- gtk_widget_set_sensitive(editplaylists_widget,
+ gtk_widget_set_sensitive(menu_editplaylists_widget,
nplaylists >= 0);
}
-/** @brief (Re-)populate the playlist tree model */
-static void playlists_fill(void) {
- GtkTreeIter iter[1];
+/* Popup to create a new playlist ------------------------------------------- */
+
+/** @brief New-playlist popup */
+static GtkWidget *playlist_new_window;
+
+/** @brief Text entry in new-playlist popup */
+static GtkWidget *playlist_new_entry;
+
+/** @brief Label for displaying feedback on what's wrong */
+static GtkWidget *playlist_new_info;
+
+/** @brief "Shared" radio button */
+static GtkWidget *playlist_new_shared;
+
+/** @brief "Public" radio button */
+static GtkWidget *playlist_new_public;
+
+/** @brief "Private" radio button */
+static GtkWidget *playlist_new_private;
+
+/** @brief Buttons for new-playlist popup */
+static struct button playlist_new_buttons[] = {
+ {
+ .stock = GTK_STOCK_OK,
+ .clicked = playlist_new_ok,
+ .tip = "Create new playlist"
+ },
+ {
+ .stock = GTK_STOCK_CANCEL,
+ .clicked = playlist_new_cancel,
+ .tip = "Do not create new playlist"
+ }
+};
+#define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons)
+
+/** @brief Pop up a new window to enter the playlist name and details */
+static void playlist_new_playlist(void) {
+ assert(playlist_new_window == NULL);
+ playlist_new_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ g_signal_connect(playlist_new_window, "destroy",
+ G_CALLBACK(gtk_widget_destroyed), &playlist_new_window);
+ gtk_window_set_title(GTK_WINDOW(playlist_new_window), "Create new playlist");
+ /* Window will be modal, suppressing access to other windows */
+ gtk_window_set_modal(GTK_WINDOW(playlist_new_window), TRUE);
+ gtk_window_set_transient_for(GTK_WINDOW(playlist_new_window),
+ GTK_WINDOW(playlist_window));
+
+ /* Window contents will use a table (grid) layout */
+ GtkWidget *table = gtk_table_new(3, 3, FALSE/*!homogeneous*/);
+
+ /* First row: playlist name */
+ gtk_table_attach_defaults(GTK_TABLE(table),
+ gtk_label_new("Playlist name"),
+ 0, 1, 0, 1);
+ playlist_new_entry = gtk_entry_new();
+ g_signal_connect(playlist_new_entry, "changed",
+ G_CALLBACK(playlist_new_entry_edited), NULL);
+ gtk_table_attach_defaults(GTK_TABLE(table),
+ playlist_new_entry,
+ 1, 3, 0, 1);
+
+ /* Second row: radio buttons to choose type */
+ playlist_new_shared = gtk_radio_button_new_with_label(NULL, "shared");
+ playlist_new_public
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
+ "public");
+ playlist_new_private
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
+ "private");
+ g_signal_connect(playlist_new_shared, "toggled",
+ G_CALLBACK(playlist_new_button_toggled), NULL);
+ g_signal_connect(playlist_new_public, "toggled",
+ G_CALLBACK(playlist_new_button_toggled), NULL);
+ g_signal_connect(playlist_new_private, "toggled",
+ G_CALLBACK(playlist_new_button_toggled), NULL);
+ gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_shared, 0, 1, 1, 2);
+ gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_public, 1, 2, 1, 2);
+ gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_private, 2, 3, 1, 2);
+
+ /* Third row: info bar saying why not */
+ playlist_new_info = gtk_label_new("");
+ gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_info,
+ 0, 3, 2, 3);
+
+ /* Fourth row: ok/cancel buttons */
+ GtkWidget *hbox = create_buttons_box(playlist_new_buttons,
+ NPLAYLIST_NEW_BUTTONS,
+ gtk_hbox_new(FALSE, 0));
+ gtk_table_attach_defaults(GTK_TABLE(table), hbox, 0, 3, 3, 4);
+
+ gtk_container_add(GTK_CONTAINER(playlist_new_window),
+ frame_widget(table, NULL));
+
+ /* Set initial state of OK button */
+ playlist_new_changed(0,0,0);
+
+ g_signal_connect(playlist_new_window, "key-press-event",
+ G_CALLBACK(playlist_new_keypress), 0);
+
+ /* Display the window */
+ gtk_widget_show_all(playlist_new_window);
+}
- if(!playlists_list)
- playlists_list = gtk_list_store_new(1, G_TYPE_STRING);
- gtk_list_store_clear(playlists_list);
+/** @brief Keypress handler */
+static gboolean playlist_new_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->state)
+ return FALSE;
+ switch(event->keyval) {
+ case GDK_Return:
+ playlist_new_ok(NULL, NULL);
+ return TRUE;
+ case GDK_Escape:
+ gtk_widget_destroy(playlist_new_window);
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+/** @brief Called when 'ok' is clicked in new-playlist popup */
+static void playlist_new_ok(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ if(playlist_new_valid())
+ return;
+ gboolean shared, public, private;
+ char *name, *fullname;
+ playlist_new_details(&name, &fullname, &shared, &public, &private);
+
+ /* We need to:
+ * - lock the playlist
+ * - check it doesn't exist
+ * - set sharing (which will create it empty
+ * - unlock it
+ *
+ * TODO we should freeze the window while this is going on to stop a second
+ * click.
+ */
+ disorder_eclient_playlist_lock(client, playlist_new_locked, fullname,
+ fullname);
+}
+
+/** @brief Called when the proposed new playlist has been locked */
+static void playlist_new_locked(void *v, const char *err) {
+ char *fullname = v;
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ return;
+ }
+ disorder_eclient_playlist_get(client, playlist_new_retrieved,
+ fullname, fullname);
+}
+
+/** @brief Called when the proposed new playlist's contents have been retrieved
+ *
+ * ...or rather, normally, when it's been reported that it does not exist.
+ */
+static void playlist_new_retrieved(void *v, const char *err,
+ int nvec,
+ char attribute((unused)) **vec) {
+ char *fullname = v;
+ if(!err && nvec != -1)
+ /* A rare case but not in principle impossible */
+ err = "A playlist with that name already exists.";
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname);
+ return;
+ }
+ gboolean shared, public, private;
+ playlist_new_details(0, 0, &shared, &public, &private);
+ disorder_eclient_playlist_set_share(client, playlist_new_created, fullname,
+ public ? "public"
+ : private ? "private"
+ : "shared",
+ fullname);
+}
+
+/** @brief Called when the new playlist has been created */
+static void playlist_new_created(void attribute((unused)) *v, const char *err) {
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ return;
+ }
+ disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL);
+ // TODO arrange for the new playlist to be selected
+}
+
+/** @brief Called when the newly created playlist has unlocked */
+static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ /* Pop down the creation window */
+ gtk_widget_destroy(playlist_new_window);
+}
+
+/** @brief Called when 'cancel' is clicked in new-playlist popup */
+static void playlist_new_cancel(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ gtk_widget_destroy(playlist_new_window);
+}
+
+/** @brief Called when some radio button in the new-playlist popup changes */
+static void playlist_new_button_toggled(GtkToggleButton attribute((unused)) *tb,
+ gpointer attribute((unused)) userdata) {
+ playlist_new_changed(0,0,0);
+}
+
+/** @brief Called when the text entry field in the new-playlist popup changes */
+static void playlist_new_entry_edited(GtkEditable attribute((unused)) *editable,
+ gpointer attribute((unused)) user_data) {
+ playlist_new_changed(0,0,0);
+}
+
+/** @brief Called to update new playlist window state
+ *
+ * This is called whenever one the text entry or radio buttons changed, and
+ * also when the set of known playlists changes. It determines whether the new
+ * playlist would be creatable and sets the sensitivity of the OK button
+ * and info display accordingly.
+ */
+static void playlist_new_changed(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
+ if(!playlist_new_window)
+ return;
+ const char *reason = playlist_new_valid();
+ gtk_widget_set_sensitive(playlist_new_buttons[0].widget,
+ !reason);
+ gtk_label_set_text(GTK_LABEL(playlist_new_info), reason);
+}
+
+/** @brief Test whether the new-playlist window settings are valid
+ * @return NULL on success or an error string if not
+ */
+static const char *playlist_new_valid(void) {
+ gboolean shared, public, private;
+ char *name, *fullname;
+ playlist_new_details(&name, &fullname, &shared, &public, &private);
+ if(!(shared || public || private))
+ return "No type set.";
+ if(!*name)
+ return "";
+ /* See if the result is valid */
+ if(!valid_username(name)
+ || playlist_parse_name(fullname, NULL, NULL))
+ return "Not a valid playlist name.";
+ /* See if the result clashes with an existing name. This is not a perfect
+ * check, the playlist might be created after this point but before we get a
+ * chance to disable the "OK" button. However when we try to create the
+ * playlist we will first try to retrieve it, with a lock held, so we
+ * shouldn't end up overwriting anything. */
for(int n = 0; n < nplaylists; ++n)
- gtk_list_store_insert_with_values(playlists_list, iter, n/*position*/,
- 0, playlists[n], /* column 0 */
- -1); /* no more cols */
- // TODO reselect whatever was formerly selected if possible, if not then
- // zap the contents view
+ if(!strcmp(playlists[n], fullname)) {
+ if(shared)
+ return "A shared playlist with that name already exists.";
+ else
+ return "You already have a playlist with that name.";
+ }
+ /* As far as we can tell creation would work */
+ return NULL;
+}
+
+/** @brief Get entered new-playlist details
+ * @param namep Where to store entered name (or NULL)
+ * @param fullnamep Where to store computed full name (or NULL)
+ * @param sharep Where to store 'shared' flag (or NULL)
+ * @param publicp Where to store 'public' flag (or NULL)
+ * @param privatep Where to store 'private' flag (or NULL)
+ */
+static void playlist_new_details(char **namep,
+ char **fullnamep,
+ gboolean *sharedp,
+ gboolean *publicp,
+ gboolean *privatep) {
+ gboolean shared, public, private;
+ g_object_get(playlist_new_shared, "active", &shared, (char *)NULL);
+ g_object_get(playlist_new_public, "active", &public, (char *)NULL);
+ g_object_get(playlist_new_private, "active", &private, (char *)NULL);
+ char *gname = gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry),
+ 0, -1); /* name owned by calle */
+ char *name = xstrdup(gname);
+ g_free(gname);
+ if(sharedp) *sharedp = shared;
+ if(publicp) *publicp = public;
+ if(privatep) *privatep = private;
+ if(namep) *namep = name;
+ if(fullnamep) {
+ if(shared) *fullnamep = name;
+ else byte_xasprintf(fullnamep, "%s.%s", config->username, name);
+ }
+}
+
+/* Playlist picker ---------------------------------------------------------- */
+
+/** @brief Delete button */
+static GtkWidget *playlist_picker_delete_button;
+
+/** @brief Tree model for list of playlists
+ *
+ * This has two columns:
+ * - column 0 will be the display name
+ * - column 1 will be the sort key/playlist name (and will not be displayed)
+ */
+static GtkTreeStore *playlist_picker_list;
+
+/** @brief Selection for list of playlists */
+static GtkTreeSelection *playlist_picker_selection;
+
+/** @brief Currently selected playlist */
+static const char *playlist_picker_selected;
+
+/** @brief (Re-)populate the playlist picker tree model */
+static void playlist_picker_fill(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
+ if(!playlist_window)
+ return;
+ if(!playlist_picker_list)
+ playlist_picker_list = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ /* We will accumulate a list of all the sections that exist */
+ char **sections = xcalloc(nplaylists, sizeof (char *));
+ int nsections = 0;
+ /* Make sure shared playlists are there */
+ int start = 0, end;
+ for(end = start; end < nplaylists && !strchr(playlists[end], '.'); ++end)
+ ;
+ if(start != end) {
+ playlist_picker_update_section("Shared playlists", "",
+ start, end);
+ sections[nsections++] = (char *)"";
+ }
+ /* Make sure owned playlists are there */
+ while((start = end) < nplaylists) {
+ const int nl = strchr(playlists[start], '.') - playlists[start];
+ char *name = xstrndup(playlists[start], nl);
+ for(end = start;
+ end < nplaylists
+ && playlists[end][nl] == '.'
+ && !strncmp(playlists[start], playlists[end], nl);
+ ++end)
+ ;
+ playlist_picker_update_section(name, name, start, end);
+ sections[nsections++] = name;
+ }
+ /* Delete obsolete sections */
+ playlist_picker_delete_obsolete(NULL, sections, nsections);
+}
+
+/** @brief Update a section in the picker tree model
+ * @param section Section name
+ * @param start First entry in @ref playlists
+ * @param end Past last entry in @ref playlists
+ */
+static void playlist_picker_update_section(const char *title, const char *key,
+ int start, int end) {
+ /* Find the section, creating it if necessary */
+ GtkTreeIter section_iter[1];
+ playlist_picker_find(NULL, title, key, section_iter, TRUE);
+ /* Add missing rows */
+ for(int n = start; n < end; ++n) {
+ GtkTreeIter child[1];
+ char *name;
+ if((name = strchr(playlists[n], '.')))
+ ++name;
+ else
+ name = playlists[n];
+ playlist_picker_find(section_iter,
+ name, playlists[n],
+ child,
+ TRUE);
+ }
+ /* Delete anything that shouldn't exist. */
+ playlist_picker_delete_obsolete(section_iter, playlists + start, end - start);
+}
+
+/** @brief Find and maybe create a row in the picker tree model
+ * @param parent Parent iterator (or NULL for top level)
+ * @param title Display name of section
+ * @param key Key to search for
+ * @param iter Iterator to point at key
+ * @param create If TRUE, key will be created if it doesn't exist
+ * @param compare Row comparison function
+ * @return TRUE if key exists else FALSE
+ *
+ * If the @p key exists then @p iter will point to it and TRUE will be
+ * returned.
+ *
+ * If the @p key does not exist and @p create is TRUE then it will be created.
+ * @p iter wil point to it and TRUE will be returned.
+ *
+ * If the @p key does not exist and @p create is FALSE then FALSE will be
+ * returned.
+ */
+static gboolean playlist_picker_find(GtkTreeIter *parent,
+ const char *title,
+ const char *key,
+ GtkTreeIter iter[1],
+ gboolean create) {
+ gchar *candidate;
+ GtkTreeIter next[1];
+ gboolean it;
+ int row = 0;
+
+ it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+ next,
+ parent);
+ while(it) {
+ /* Find the value at row 'next' */
+ gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+ next,
+ 1, &candidate,
+ -1);
+ /* See how it compares with @p key */
+ int c = strcmp(key, candidate);
+ g_free(candidate);
+ if(!c) {
+ *iter = *next;
+ return TRUE; /* we found our key */
+ }
+ if(c < 0) {
+ /* @p key belongs before row 'next' */
+ if(create) {
+ gtk_tree_store_insert_with_values(playlist_picker_list,
+ iter,
+ parent,
+ row, /* insert here */
+ 0, title, 1, key, -1);
+ return TRUE;
+ } else
+ return FALSE;
+ ++row;
+ }
+ it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list), next);
+ }
+ /* We have reached the end and not found a row that should be later than @p
+ * key. */
+ if(create) {
+ gtk_tree_store_insert_with_values(playlist_picker_list,
+ iter,
+ parent,
+ INT_MAX, /* insert at end */
+ 0, title, 1, key, -1);
+ return TRUE;
+ } else
+ return FALSE;
+}
+
+/** @brief Delete obsolete rows
+ * @param parent Parent or NULL
+ * @param exists List of rows that should exist (by key)
+ * @param nexists Length of @p exists
+ */
+static void playlist_picker_delete_obsolete(GtkTreeIter parent[1],
+ char **exists,
+ int nexists) {
+ /* Delete anything that shouldn't exist. */
+ GtkTreeIter iter[1];
+ gboolean it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list),
+ iter,
+ parent);
+ while(it) {
+ /* Find the value at row 'next' */
+ gchar *candidate;
+ gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list),
+ iter,
+ 1, &candidate,
+ -1);
+ gboolean found = FALSE;
+ for(int n = 0; n < nexists; ++n)
+ if((found = !strcmp(candidate, exists[n])))
+ break;
+ if(!found)
+ it = gtk_tree_store_remove(playlist_picker_list, iter);
+ else
+ it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list),
+ iter);
+ g_free(candidate);
+ }
}
/** @brief Called when the selection might have changed */
-static void playlists_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
- gpointer attribute((unused)) user_data) {
+static void playlist_picker_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
+ gpointer attribute((unused)) user_data) {
GtkTreeIter iter;
char *gselected, *selected;
/* Identify the current selection */
- if(gtk_tree_selection_get_selected(playlists_selection, 0, &iter)) {
- gtk_tree_model_get(GTK_TREE_MODEL(playlists_list), &iter,
- 0, &gselected, -1);
+ if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)
+ && gtk_tree_store_iter_depth(playlist_picker_list, &iter) > 0) {
+ gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), &iter,
+ 1, &gselected, -1);
selected = xstrdup(gselected);
g_free(gselected);
} else
selected = 0;
+ /* Set button sensitivity according to the new state */
+ int deletable = FALSE;
+ if(selected) {
+ if(strchr(selected, '.')) {
+ if(!strncmp(selected, config->username, strlen(config->username)))
+ deletable = TRUE;
+ } else
+ deletable = TRUE;
+ }
+ gtk_widget_set_sensitive(playlist_picker_delete_button, deletable);
/* Eliminate no-change cases */
- if(!selected && !playlists_selected)
+ if(!selected && !playlist_picker_selected)
return;
- if(selected && playlists_selected && !strcmp(selected, playlists_selected))
+ if(selected
+ && playlist_picker_selected
+ && !strcmp(selected, playlist_picker_selected))
return;
- /* There's been a change */
- playlists_selected = selected;
- if(playlists_selected) {
- fprintf(stderr, "playlists selection changed\n'"); /* TODO */
- gtk_widget_set_sensitive(playlists_delete_button, 1);
- } else
- gtk_widget_set_sensitive(playlists_delete_button, 0);
+ /* Record the new state */
+ playlist_picker_selected = selected;
+ /* Re-initalize the queue */
+ ql_new_queue(&ql_playlist, NULL);
+ /* Synthesize a playlist-modified to re-initialize the editor etc */
+ event_raise("playlist-modified", (void *)playlist_picker_selected);
}
/** @brief Called when the 'add' button is pressed */
-static void playlists_add(GtkButton attribute((unused)) *button,
- gpointer attribute((unused)) userdata) {
- /* Unselect whatever is selected */
- gtk_tree_selection_unselect_all(playlists_selection);
- fprintf(stderr, "playlists_add\n");/* TODO */
+static void playlist_picker_add(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ /* Unselect whatever is selected TODO why?? */
+ gtk_tree_selection_unselect_all(playlist_picker_selection);
+ playlist_new_playlist();
+}
+
+/** @brief Called when playlist deletion completes */
+static void playlists_picker_delete_completed(void attribute((unused)) *v,
+ const char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
}
/** @brief Called when the 'Delete' button is pressed */
-static void playlists_delete(GtkButton attribute((unused)) *button,
- gpointer attribute((unused)) userdata) {
+static void playlist_picker_delete(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
GtkWidget *yesno;
int res;
- if(!playlists_selected)
- return; /* shouldn't happen */
- yesno = gtk_message_dialog_new(GTK_WINDOW(playlists_window),
+ if(!playlist_picker_selected)
+ return;
+ yesno = gtk_message_dialog_new(GTK_WINDOW(playlist_window),
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_YES_NO,
- "Do you really want to delete user %s?"
+ "Do you really want to delete playlist %s?"
" This action cannot be undone.",
- playlists_selected);
+ playlist_picker_selected);
res = gtk_dialog_run(GTK_DIALOG(yesno));
gtk_widget_destroy(yesno);
if(res == GTK_RESPONSE_YES) {
disorder_eclient_playlist_delete(client,
- NULL/*playlists_delete_completed*/,
- playlists_selected,
+ playlists_picker_delete_completed,
+ playlist_picker_selected,
NULL);
}
}
/** @brief Table of buttons below the playlist list */
-static struct button playlists_buttons[] = {
+static struct button playlist_picker_buttons[] = {
{
GTK_STOCK_ADD,
- playlists_add,
+ playlist_picker_add,
"Create a new playlist",
- 0
+ 0,
+ NULL,
},
{
GTK_STOCK_REMOVE,
- playlists_delete,
+ playlist_picker_delete,
"Delete a playlist",
+ 0,
+ NULL,
+ },
+};
+#define NPLAYLIST_PICKER_BUTTONS (sizeof playlist_picker_buttons / sizeof *playlist_picker_buttons)
+
+/** @brief Create the list of playlists for the edit playlists window */
+static GtkWidget *playlist_picker_create(void) {
+ /* Create the list of playlist and populate it */
+ playlist_picker_fill(NULL, NULL, NULL);
+ /* Create the tree view */
+ GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlist_picker_list));
+ /* ...and the renderers for it */
+ GtkCellRenderer *cr = gtk_cell_renderer_text_new();
+ GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes("Playlist",
+ cr,
+ "text", 0,
+ NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
+ /* Get the selection for the view; set its mode; arrange for a callback when
+ * it changes */
+ playlist_picker_selected = NULL;
+ playlist_picker_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_set_mode(playlist_picker_selection, GTK_SELECTION_BROWSE);
+ g_signal_connect(playlist_picker_selection, "changed",
+ G_CALLBACK(playlist_picker_selection_changed), NULL);
+
+ /* Create the control buttons */
+ GtkWidget *buttons = create_buttons_box(playlist_picker_buttons,
+ NPLAYLIST_PICKER_BUTTONS,
+ gtk_hbox_new(FALSE, 1));
+ playlist_picker_delete_button = playlist_picker_buttons[1].widget;
+
+ playlist_picker_selection_changed(NULL, NULL);
+
+ /* Buttons live below the list */
+ GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+
+ g_signal_connect(tree, "key-press-event",
+ G_CALLBACK(playlist_picker_keypress), 0);
+ g_signal_connect(tree, "button-press-event",
+ G_CALLBACK(playlist_picker_button), 0);
+
+ return vbox;
+}
+
+static gboolean playlist_picker_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->state)
+ return FALSE;
+ switch(event->keyval) {
+ case GDK_BackSpace:
+ case GDK_Delete:
+ playlist_picker_delete(NULL, NULL);
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void playlist_picker_select_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* nothing */
+}
+
+static int playlist_picker_select_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_play_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* Re-use the menu-based activation callback */
+ disorder_eclient_playlist_get(client, playlist_menu_received_content,
+ playlist_picker_selected, NULL);
+}
+
+static int playlist_picker_play_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0;
+}
+
+static void playlist_picker_remove_activate(GtkMenuItem attribute((unused)) *item,
+ gpointer attribute((unused)) userdata) {
+ /* Re-use the 'Remove' button' */
+ playlist_picker_delete(NULL, NULL);
+}
+
+static int playlist_picker_remove_sensitive(void *extra) {
+ GtkTreeIter *iter = extra;
+ if(gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0) {
+ if(strchr(playlist_picker_selected, '.')) {
+ if(!strncmp(playlist_picker_selected, config->username,
+ strlen(config->username)))
+ return TRUE;
+ } else
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/** @brief Pop-up menu for picker */
+static struct menuitem playlist_picker_menuitems[] = {
+ {
+ "Select playlist",
+ NULL,
+ playlist_picker_select_activate,
+ playlist_picker_select_sensitive,
+ 0,
+ 0
+ },
+ {
+ "Play playlist",
+ GTK_STOCK_MEDIA_PLAY,
+ playlist_picker_play_activate,
+ playlist_picker_play_sensitive,
+ 0,
+ 0
+ },
+ {
+ "Remove playlist",
+ GTK_STOCK_DELETE,
+ playlist_picker_remove_activate,
+ playlist_picker_remove_sensitive,
+ 0,
0
},
};
-#define NPLAYLISTS_BUTTONS (sizeof playlists_buttons / sizeof *playlists_buttons)
-/** @brief Keypress handler */
-static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget,
- GdkEventKey *event,
- gpointer attribute((unused)) user_data) {
+static gboolean playlist_picker_button(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->type == GDK_BUTTON_PRESS && event->button == 3) {
+ static GtkWidget *playlist_picker_menu;
+
+ /* Right click press pops up a menu */
+ ensure_selected(GTK_TREE_VIEW(widget), event);
+ /* Find the selected row */
+ GtkTreeIter iter[1];
+ if(!gtk_tree_selection_get_selected(playlist_picker_selection, 0, iter))
+ return TRUE;
+ popup(&playlist_picker_menu, event,
+ playlist_picker_menuitems,
+ sizeof playlist_picker_menuitems / sizeof *playlist_picker_menuitems,
+ iter);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void playlist_picker_destroy(void) {
+ playlist_picker_delete_button = NULL;
+ g_object_unref(playlist_picker_list);
+ playlist_picker_list = NULL;
+ playlist_picker_selection = NULL;
+ playlist_picker_selected = NULL;
+}
+
+/* Playlist editor ---------------------------------------------------------- */
+
+static GtkWidget *playlist_editor_shared;
+static GtkWidget *playlist_editor_public;
+static GtkWidget *playlist_editor_private;
+static int playlist_editor_setting_buttons;
+
+/** @brief Buttons for the playlist window */
+static struct button playlist_editor_buttons[] = {
+ {
+ GTK_STOCK_OK,
+ playlist_editor_ok,
+ "Close window",
+ 0,
+ gtk_box_pack_end,
+ },
+ {
+ GTK_STOCK_HELP,
+ playlist_editor_help,
+ "Go to manual",
+ 0,
+ gtk_box_pack_end,
+ },
+};
+
+#define NPLAYLIST_EDITOR_BUTTONS (int)(sizeof playlist_editor_buttons / sizeof *playlist_editor_buttons)
+
+static GtkWidget *playlists_editor_create(void) {
+ assert(ql_playlist.view == NULL); /* better not be set up already */
+
+ GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
+ playlist_editor_shared = gtk_radio_button_new_with_label(NULL, "shared");
+ playlist_editor_public
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+ "public");
+ playlist_editor_private
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+ "private");
+ g_signal_connect(playlist_editor_public, "toggled",
+ G_CALLBACK(playlist_editor_button_toggled),
+ (void *)"public");
+ g_signal_connect(playlist_editor_private, "toggled",
+ G_CALLBACK(playlist_editor_button_toggled),
+ (void *)"private");
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_shared,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_public,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_private,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ playlist_editor_set_buttons(0,0,0);
+ create_buttons_box(playlist_editor_buttons,
+ NPLAYLIST_EDITOR_BUTTONS,
+ hbox);
+
+ GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
+ GtkWidget *view = init_queuelike(&ql_playlist);
+ gtk_box_pack_start(GTK_BOX(vbox), view,
+ TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ g_signal_connect(view, "key-press-event",
+ G_CALLBACK(playlist_editor_keypress), 0);
+ return vbox;
+}
+
+static gboolean playlist_editor_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
if(event->state)
return FALSE;
switch(event->keyval) {
- case GDK_Escape:
- gtk_widget_destroy(playlists_window);
+ case GDK_BackSpace:
+ case GDK_Delete:
+ playlist_remove_activate(NULL, NULL);
return TRUE;
default:
return FALSE;
}
}
-void edit_playlists(gpointer attribute((unused)) callback_data,
- guint attribute((unused)) callback_action,
- GtkWidget attribute((unused)) *menu_item) {
- GtkWidget *tree, *hbox, *vbox, *buttons;
- GtkCellRenderer *cr;
- GtkTreeViewColumn *col;
+/** @brief Called when the public/private buttons are set */
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+ gpointer userdata) {
+ const char *state = userdata;
+ if(!gtk_toggle_button_get_active(tb)
+ || !playlist_picker_selected
+ || playlist_editor_setting_buttons)
+ return;
+ disorder_eclient_playlist_set_share(client, playlist_editor_share_set,
+ playlist_picker_selected, state, NULL);
+}
+
+static void playlist_editor_share_set(void attribute((unused)) *v,
+ const attribute((unused)) char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+
+/** @brief Set the editor button state and sensitivity */
+static void playlist_editor_set_buttons(const char attribute((unused)) *event,
+ void *eventdata,
+ void attribute((unused)) *callbackdata) {
+ /* If this event is for a non-selected playlist do nothing */
+ if(eventdata
+ && playlist_picker_selected
+ && strcmp(eventdata, playlist_picker_selected))
+ return;
+ if(playlist_picker_selected) {
+ if(strchr(playlist_picker_selected, '.'))
+ disorder_eclient_playlist_get_share(client,
+ playlist_editor_got_share,
+ playlist_picker_selected,
+ (void *)playlist_picker_selected);
+ else
+ playlist_editor_got_share((void *)playlist_picker_selected, NULL,
+ "shared");
+ } else
+ playlist_editor_got_share(NULL, NULL, NULL);
+}
+
+/** @brief Called with playlist sharing details */
+static void playlist_editor_got_share(void *v,
+ const char *err,
+ const char *value) {
+ const char *playlist = v;
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ value = NULL;
+ }
+ /* Set the currently active button */
+ ++playlist_editor_setting_buttons;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_shared),
+ value && !strcmp(value, "shared"));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_public),
+ value && !strcmp(value, "public"));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_private),
+ value && !strcmp(value, "private"));
+ /* Set button sensitivity */
+ gtk_widget_set_sensitive(playlist_editor_shared, FALSE);
+ int sensitive = (playlist
+ && strchr(playlist, '.')
+ && !strncmp(playlist, config->username,
+ strlen(config->username)));
+ gtk_widget_set_sensitive(playlist_editor_public, sensitive);
+ gtk_widget_set_sensitive(playlist_editor_private, sensitive);
+ --playlist_editor_setting_buttons;
+}
+
+/** @brief (Re-)populate the playlist tree model */
+static void playlist_editor_fill(const char attribute((unused)) *event,
+ void *eventdata,
+ void attribute((unused)) *callbackdata) {
+ const char *modified_playlist = eventdata;
+ if(!playlist_window)
+ return;
+ if(!playlist_picker_selected)
+ return;
+ if(!strcmp(playlist_picker_selected, modified_playlist))
+ disorder_eclient_playlist_get(client, playlists_editor_received_tracks,
+ playlist_picker_selected,
+ (void *)playlist_picker_selected);
+}
+
+/** @brief Called with new tracks for the playlist */
+static void playlists_editor_received_tracks(void *v,
+ const char *err,
+ int nvec, char **vec) {
+ const char *playlist = v;
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ return;
+ }
+ if(!playlist_picker_selected
+ || strcmp(playlist, playlist_picker_selected)) {
+ /* The tracks are for the wrong playlist - something must have changed
+ * while the fetch command was in flight. We just ignore this callback,
+ * the right answer will be requested and arrive in due course. */
+ return;
+ }
+ if(nvec == -1)
+ /* No such playlist, presumably we'll get a deleted event shortly */
+ return;
+ /* Translate the list of tracks into queue entries */
+ struct queue_entry *newq, **qq = &newq, *qprev = NULL;
+ hash *h = hash_new(sizeof(int));
+ for(int n = 0; n < nvec; ++n) {
+ struct queue_entry *q = xmalloc(sizeof *q);
+ q->prev = qprev;
+ q->track = vec[n];
+ /* Synthesize a unique ID so that the selection survives updates. Tracks
+ * can appear more than once in the queue so we can't use raw track names,
+ * so we add a serial number to the start. */
+ int *serialp = hash_find(h, vec[n]), serial = serialp ? *serialp : 0;
+ byte_xasprintf((char **)&q->id, "%d-%s", serial++, vec[n]);
+ hash_add(h, vec[n], &serial, HASH_INSERT_OR_REPLACE);
+ *qq = q;
+ qq = &q->next;
+ qprev = q;
+ }
+ *qq = NULL;
+ ql_new_queue(&ql_playlist, newq);
+}
+
+static void playlist_editor_ok(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ gtk_widget_destroy(playlist_window);
+}
+
+static void playlist_editor_help(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ popup_help("playlists.html");
+}
+
+/* Playlist mutation -------------------------------------------------------- */
+
+/** @brief State structure for guarded playlist modification
+ *
+ * To safely move, insert or delete rows we must:
+ * - take a lock
+ * - fetch the playlist
+ * - verify it's not changed
+ * - update the playlist contents
+ * - store the playlist
+ * - release the lock
+ *
+ * The playlist_modify_ functions do just that.
+ *
+ * To kick things off create one of these and disorder_eclient_playlist_lock()
+ * with playlist_modify_locked() as its callback. @c modify will be called; it
+ * should disorder_eclient_playlist_set() to set the new state with
+ * playlist_modify_updated() as its callback.
+ */
+struct playlist_modify_data {
+ /** @brief Affected playlist */
+ const char *playlist;
+ /** @brief Modification function
+ * @param mod Pointer back to state structure
+ * @param ntracks Length of playlist
+ * @param tracks Tracks in playlist
+ */
+ void (*modify)(struct playlist_modify_data *mod,
+ int ntracks, char **tracks);
+
+ /** @brief Number of tracks dropped */
+ int ntracks;
+ /** @brief Track names dropped */
+ char **tracks;
+ /** @brief Track IDs dropped */
+ char **ids;
+ /** @brief Drop after this point */
+ struct queue_entry *after_me;
+};
+
+/** @brief Called with playlist locked
+ *
+ * This is the entry point for guarded modification ising @ref
+ * playlist_modify_data.
+ */
+static void playlist_modify_locked(void *v, const char *err) {
+ struct playlist_modify_data *mod = v;
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ return;
+ }
+ disorder_eclient_playlist_get(client, playlist_modify_retrieved,
+ mod->playlist, mod);
+}
+
+/** @brief Called with current playlist contents
+ * Checks that the playlist is still current and has not changed.
+ */
+void playlist_modify_retrieved(void *v, const char *err,
+ int nvec,
+ char **vec) {
+ struct playlist_modify_data *mod = v;
+ if(err) {
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+ return;
+ }
+ if(nvec < 0
+ || !playlist_picker_selected
+ || strcmp(mod->playlist, playlist_picker_selected)) {
+ disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+ return;
+ }
+ /* We check that the contents haven't changed. If they have we just abandon
+ * the operation. The user will have to try again. */
+ struct queue_entry *q;
+ int n;
+ for(n = 0, q = ql_playlist.q; q && n < nvec; ++n, q = q->next)
+ if(strcmp(q->track, vec[n]))
+ break;
+ if(n != nvec || q != NULL) {
+ disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+ return;
+ }
+ mod->modify(mod, nvec, vec);
+}
+
+/** @brief Called when the playlist has been updated */
+static void playlist_modify_updated(void attribute((unused)) *v,
+ const char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+ disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL);
+}
+
+/** @brief Called when the playlist has been unlocked */
+static void playlist_modify_unlocked(void attribute((unused)) *v,
+ const char *err) {
+ if(err)
+ popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err);
+}
+
+/* Drop tracks into a playlist ---------------------------------------------- */
+
+static void playlist_drop(struct queuelike attribute((unused)) *ql,
+ int ntracks,
+ char **tracks, char **ids,
+ struct queue_entry *after_me) {
+ struct playlist_modify_data *mod = xmalloc(sizeof *mod);
+
+ mod->playlist = playlist_picker_selected;
+ mod->modify = playlist_drop_modify;
+ mod->ntracks = ntracks;
+ mod->tracks = tracks;
+ mod->ids = ids;
+ mod->after_me = after_me;
+ disorder_eclient_playlist_lock(client, playlist_modify_locked,
+ mod->playlist, mod);
+}
+
+/** @brief Return true if track @p i is in the moved set */
+static int playlist_drop_is_moved(struct playlist_modify_data *mod,
+ int i) {
+ struct queue_entry *q;
+
+ /* Find the q corresponding to i, so we can get the ID */
+ for(q = ql_playlist.q; i; q = q->next, --i)
+ ;
+ /* See if track i matches any of the moved set by ID */
+ for(int n = 0; n < mod->ntracks; ++n)
+ if(!strcmp(q->id, mod->ids[n]))
+ return 1;
+ return 0;
+}
+
+static void playlist_drop_modify(struct playlist_modify_data *mod,
+ int nvec, char **vec) {
+ char **newvec;
+ int nnewvec;
+
+ //fprintf(stderr, "\nplaylist_drop_modify\n");
+ /* after_me is the queue_entry to insert after, or NULL to insert at the
+ * beginning (including the case when the playlist is empty) */
+ //fprintf(stderr, "after_me = %s\n",
+ // mod->after_me ? mod->after_me->track : "NULL");
+ struct queue_entry *q = ql_playlist.q;
+ int ins = 0;
+ if(mod->after_me) {
+ ++ins;
+ while(q && q != mod->after_me) {
+ q = q->next;
+ ++ins;
+ }
+ }
+ /* Now ins is the index to insert at; equivalently, the row to insert before,
+ * and so equal to nvec to append. */
+#if 0
+ fprintf(stderr, "ins = %d = %s\n",
+ ins, ins < nvec ? vec[ins] : "NULL");
+ for(int n = 0; n < nvec; ++n)
+ fprintf(stderr, "%d: %s %s\n", n, n == ins ? "->" : " ", vec[n]);
+ fprintf(stderr, "nvec = %d\n", nvec);
+#endif
+ if(mod->ids) {
+ /* This is a rearrangement */
+ /* We have:
+ * - vec[], the current layout
+ * - ins, pointing into vec
+ * - mod->tracks[], a subset of vec[] which is to be moved
+ *
+ * ins is the insertion point BUT it is in terms of the whole
+ * array, i.e. before mod->tracks[] have been removed. The first
+ * step then is to remove everything in mod->tracks[] and adjust
+ * ins downwards as necessary.
+ */
+ /* First zero out anything that's moved */
+ int before_ins = 0;
+ for(int n = 0; n < nvec; ++n) {
+ if(playlist_drop_is_moved(mod, n)) {
+ vec[n] = NULL;
+ if(n < ins)
+ ++before_ins;
+ }
+ }
+ /* Now collapse down the array */
+ int i = 0;
+ for(int n = 0; n < nvec; ++n) {
+ if(vec[n])
+ vec[i++] = vec[n];
+ }
+ assert(i + mod->ntracks == nvec);
+ nvec = i;
+ /* Adjust the insertion point to take account of things moved from before
+ * it */
+ ins -= before_ins;
+ /* The effect is now the same as an insertion */
+ }
+ /* This is (now) an insertion */
+ nnewvec = nvec + mod->ntracks;
+ newvec = xcalloc(nnewvec, sizeof (char *));
+ memcpy(newvec, vec,
+ ins * sizeof (char *));
+ memcpy(newvec + ins, mod->tracks,
+ mod->ntracks * sizeof (char *));
+ memcpy(newvec + ins + mod->ntracks, vec + ins,
+ (nvec - ins) * sizeof (char *));
+ disorder_eclient_playlist_set(client, playlist_modify_updated, mod->playlist,
+ newvec, nnewvec, mod);
+}
+
+/* Playlist editor right-click menu ---------------------------------------- */
+
+/** @brief Called to determine whether the playlist is playable */
+static int playlist_playall_sensitive(void attribute((unused)) *extra) {
+ /* If there's no playlist obviously we can't play it */
+ if(!playlist_picker_selected)
+ return FALSE;
+ /* If it's empty we can't play it */
+ if(!ql_playlist.q)
+ return FALSE;
+ /* Otherwise we can */
+ return TRUE;
+}
+
+/** @brief Called to play the selected playlist */
+static void playlist_playall_activate(GtkMenuItem attribute((unused)) *menuitem,
+ gpointer attribute((unused)) user_data) {
+ if(!playlist_picker_selected)
+ return;
+ /* Re-use the menu-based activation callback */
+ disorder_eclient_playlist_get(client, playlist_menu_received_content,
+ playlist_picker_selected, NULL);
+}
+
+/** @brief Called to determine whether the playlist is playable */
+static int playlist_remove_sensitive(void attribute((unused)) *extra) {
+ /* If there's no playlist obviously we can't remove from it */
+ if(!playlist_picker_selected)
+ return FALSE;
+ /* If no tracks are selected we cannot remove them */
+ if(!gtk_tree_selection_count_selected_rows(ql_playlist.selection))
+ return FALSE;
+ /* We're good to go */
+ return TRUE;
+}
+
+/** @brief Called to remove the selected playlist */
+static void playlist_remove_activate(GtkMenuItem attribute((unused)) *menuitem,
+ gpointer attribute((unused)) user_data) {
+ if(!playlist_picker_selected)
+ return;
+ struct playlist_modify_data *mod = xmalloc(sizeof *mod);
+
+ mod->playlist = playlist_picker_selected;
+ mod->modify = playlist_remove_modify;
+ disorder_eclient_playlist_lock(client, playlist_modify_locked,
+ mod->playlist, mod);
+}
+
+static void playlist_remove_modify(struct playlist_modify_data *mod,
+ int attribute((unused)) nvec, char **vec) {
+ GtkTreeIter iter[1];
+ gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql_playlist.store),
+ iter);
+ int n = 0, m = 0;
+ while(it) {
+ if(!gtk_tree_selection_iter_is_selected(ql_playlist.selection, iter))
+ vec[m++] = vec[n++];
+ else
+ n++;
+ it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_playlist.store), iter);
+ }
+ disorder_eclient_playlist_set(client, playlist_modify_updated, mod->playlist,
+ vec, m, mod);
+}
+
+/* Playlists window --------------------------------------------------------- */
+/** @brief Pop up the playlists window
+ *
+ * Called when the playlists menu item is selected
+ */
+void playlist_window_create(gpointer attribute((unused)) callback_data,
+ guint attribute((unused)) callback_action,
+ GtkWidget attribute((unused)) *menu_item) {
/* If the window already exists, raise it */
- if(playlists_window) {
- gtk_window_present(GTK_WINDOW(playlists_window));
+ if(playlist_window) {
+ gtk_window_present(GTK_WINDOW(playlist_window));
return;
}
/* Create the window */
- playlists_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- gtk_widget_set_style(playlists_window, tool_style);
- g_signal_connect(playlists_window, "destroy",
- G_CALLBACK(gtk_widget_destroyed), &playlists_window);
- gtk_window_set_title(GTK_WINDOW(playlists_window), "Playlists Management");
+ playlist_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_widget_set_style(playlist_window, tool_style);
+ g_signal_connect(playlist_window, "destroy",
+ G_CALLBACK(playlist_window_destroyed), &playlist_window);
+ gtk_window_set_title(GTK_WINDOW(playlist_window), "Playlists Management");
/* TODO loads of this is very similar to (copied from!) users.c - can we
* de-dupe? */
/* Keyboard shortcuts */
- g_signal_connect(playlists_window, "key-press-event",
- G_CALLBACK(playlists_keypress), 0);
+ g_signal_connect(playlist_window, "key-press-event",
+ G_CALLBACK(playlist_window_keypress), 0);
/* default size is too small */
- gtk_window_set_default_size(GTK_WINDOW(playlists_window), 240, 240);
- /* Create the list of playlist and populate it */
- playlists_fill();
- /* Create the tree view */
- tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlists_list));
- /* ...and the renderers for it */
- cr = gtk_cell_renderer_text_new();
- col = gtk_tree_view_column_new_with_attributes("Playlist",
- cr,
- "text", 0,
- NULL);
- gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
- /* Get the selection for the view; set its mode; arrange for a callback when
- * it changes */
- playlists_selected = NULL;
- playlists_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
- gtk_tree_selection_set_mode(playlists_selection, GTK_SELECTION_BROWSE);
- g_signal_connect(playlists_selection, "changed",
- G_CALLBACK(playlists_selection_changed), NULL);
+ gtk_window_set_default_size(GTK_WINDOW(playlist_window), 640, 320);
- /* Create the control buttons */
- buttons = create_buttons_box(playlists_buttons,
- NPLAYLISTS_BUTTONS,
- gtk_hbox_new(FALSE, 1));
- playlists_delete_button = playlists_buttons[1].widget;
+ GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_picker_create(),
+ FALSE/*expand*/, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(),
+ FALSE/*expand*/, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(hbox), playlists_editor_create(),
+ TRUE/*expand*/, TRUE/*fill*/, 0);
- /* Buttons live below the list */
- vbox = gtk_vbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
- gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(playlist_window), frame_widget(hbox, NULL));
+ gtk_widget_show_all(playlist_window);
+}
+
+/** @brief Keypress handler */
+static gboolean playlist_window_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->state)
+ return FALSE;
+ switch(event->keyval) {
+ case GDK_Escape:
+ gtk_widget_destroy(playlist_window);
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
- hbox = gtk_hbox_new(FALSE, 0);
- gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE/*expand*/, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(), FALSE/*expand*/, FALSE, 2);
- // TODO something to edit the playlist in
- //gtk_box_pack_start(GTK_BOX(hbox), vbox2, TRUE/*expand*/, TRUE/*fill*/, 0);
- gtk_container_add(GTK_CONTAINER(playlists_window), frame_widget(hbox, NULL));
- gtk_widget_show_all(playlists_window);
+/** @brief Called when the playlist window is destroyed */
+static void playlist_window_destroyed(GtkWidget attribute((unused)) *widget,
+ GtkWidget **widget_pointer) {
+ destroy_queuelike(&ql_playlist);
+ playlist_picker_destroy();
+ *widget_pointer = NULL;
}
/** @brief Initialize playlist support */
void playlists_init(void) {
/* We re-get all playlists upon any change... */
- event_register("playlist-created", playlists_update, 0);
- event_register("playlist-modified", playlists_update, 0);
- event_register("playlist-deleted", playlists_update, 0);
+ event_register("playlist-created", playlist_list_update, 0);
+ event_register("playlist-deleted", playlist_list_update, 0);
/* ...and on reconnection */
- event_register("log-connected", playlists_update, 0);
+ event_register("log-connected", playlist_list_update, 0);
/* ...and from time to time */
- event_register("periodic-slow", playlists_update, 0);
+ event_register("periodic-slow", playlist_list_update, 0);
/* ...and at startup */
- event_register("playlists-updated", menu_playlists_changed, 0);
- playlists_update(0, 0, 0);
-}
+ playlist_list_update(0, 0, 0);
-#endif
+ /* Update the playlists menu when the set of playlists changes */
+ event_register("playlists-updated", playlist_menu_changed, 0);
+ /* Update the new-playlist OK button when the set of playlists changes */
+ event_register("playlists-updated", playlist_new_changed, 0);
+ /* Update the list of playlists in the edit window when the set changes */
+ event_register("playlists-updated", playlist_picker_fill, 0);
+ /* Update the displayed playlist when it is modified */
+ event_register("playlist-modified", playlist_editor_fill, 0);
+ /* Update the shared/public/etc buttons when a playlist is modified */
+ event_register("playlist-modified", playlist_editor_set_buttons, 0);
+}
/*
Local Variables:
diff --git a/disobedience/popup.c b/disobedience/popup.c
index 6613cfc..d7e91f3 100644
--- a/disobedience/popup.c
+++ b/disobedience/popup.c
@@ -34,7 +34,17 @@ void popup(GtkWidget **menup,
g_signal_connect(menu, "destroy",
G_CALLBACK(gtk_widget_destroyed), menup);
for(int n = 0; n < nitems; ++n) {
- items[n].w = gtk_menu_item_new_with_label(items[n].name);
+ if(items[n].stock) {
+ GtkWidget *image = gtk_image_new_from_stock(items[n].stock,
+ GTK_ICON_SIZE_MENU);
+ items[n].w = gtk_image_menu_item_new_with_label(items[n].name);
+ gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(items[n].w),
+ image);
+ } else
+ items[n].w = gtk_menu_item_new_with_label(items[n].name);
+ /* TODO accelerators would be useful here. There might be some
+ * interaction with the main menu accelerators, _except_ for playlist
+ * case! */
gtk_menu_attach(GTK_MENU(menu), items[n].w, 0, 1, n, n + 1);
}
set_tool_colors(menu);
diff --git a/disobedience/popup.h b/disobedience/popup.h
index 9706e03..25726b2 100644
--- a/disobedience/popup.h
+++ b/disobedience/popup.h
@@ -26,6 +26,9 @@ struct menuitem {
/** @brief Menu item name */
const char *name;
+ /** @brief Stock icon name */
+ const char *stock;
+
/** @brief Called to activate the menu item */
void (*activate)(GtkMenuItem *menuitem,
gpointer user_data);
diff --git a/disobedience/progress.c b/disobedience/progress.c
index 2e31603..33dbfc9 100644
--- a/disobedience/progress.c
+++ b/disobedience/progress.c
@@ -30,12 +30,14 @@ struct progress_window {
};
/** @brief Create a progress window */
-struct progress_window *progress_window_new(const char *title) {
+struct progress_window *progress_window_new(const char *title,
+ GtkWidget *parent) {
struct progress_window *pw = xmalloc(sizeof *pw);
pw->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- gtk_window_set_transient_for(GTK_WINDOW(pw->window),
- GTK_WINDOW(toplevel));
+ if(parent)
+ gtk_window_set_transient_for(GTK_WINDOW(pw->window),
+ GTK_WINDOW(parent));
g_signal_connect(pw->window, "destroy",
G_CALLBACK(gtk_widget_destroyed), &pw->window);
gtk_window_set_default_size(GTK_WINDOW(pw->window), 360, -1);
diff --git a/disobedience/properties.c b/disobedience/properties.c
index 62ea22f..6600cf9 100644
--- a/disobedience/properties.c
+++ b/disobedience/properties.c
@@ -46,6 +46,7 @@ static void prefdata_completed(void *v, const char *err, const char *value);
static void properties_ok(GtkButton *button, gpointer userdata);
static void properties_apply(GtkButton *button, gpointer userdata);
static void properties_cancel(GtkButton *button, gpointer userdata);
+static void properties_help(GtkButton *button, gpointer userdata);
static void properties_logged_in(const char *event,
void *eventdata,
@@ -125,23 +126,33 @@ static const struct pref {
/* Buttons that appear at the bottom of the window */
static struct button buttons[] = {
+ {
+ GTK_STOCK_HELP,
+ properties_help,
+ "Go to manual",
+ 0,
+ gtk_box_pack_start,
+ },
{
GTK_STOCK_OK,
properties_ok,
"Apply all changes and close window",
- 0
- },
- {
- GTK_STOCK_APPLY,
- properties_apply,
- "Apply all changes and keep window open",
- 0
+ 0,
+ gtk_box_pack_end,
},
{
GTK_STOCK_CANCEL,
properties_cancel,
"Discard all changes and close window",
- 0
+ 0,
+ gtk_box_pack_end
+ },
+ {
+ GTK_STOCK_APPLY,
+ properties_apply,
+ "Apply all changes and keep window open",
+ 0,
+ gtk_box_pack_end,
},
};
@@ -186,7 +197,8 @@ static gboolean properties_keypress(GtkWidget attribute((unused)) *widget,
}
}
-void properties(int ntracks, const char **tracks) {
+void properties(int ntracks, const char **tracks,
+ GtkWidget *parent) {
int n, m;
struct prefdata *f;
GtkWidget *buttonbox, *vbox, *label, *entry, *propagate;
@@ -299,7 +311,9 @@ void properties(int ntracks, const char **tracks) {
if(pw)
progress_window_progress(pw, 0, 0);
/* Pop up a progress bar while we're waiting */
- pw = progress_window_new("Fetching Track Properties");
+ while(parent->parent)
+ parent = parent->parent;
+ pw = progress_window_new("Fetching Track Properties", parent);
}
/* Everything is filled in now */
@@ -487,6 +501,11 @@ static void properties_cancel(GtkButton attribute((unused)) *button,
properties_event = 0;
}
+static void properties_help(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ popup_help("properties.html");
+}
+
/** @brief Called when we've just logged in
*
* Destroys the current properties window.
diff --git a/disobedience/queue-generic.c b/disobedience/queue-generic.c
index 82bd939..333187a 100644
--- a/disobedience/queue-generic.c
+++ b/disobedience/queue-generic.c
@@ -43,19 +43,6 @@
#include "multidrag.h"
#include "autoscroll.h"
-static const GtkTargetEntry queuelike_targets[] = {
- {
- (char *)"text/x-disorder-queued-tracks", /* drag type */
- GTK_TARGET_SAME_WIDGET, /* rearrangement within a widget */
- 0 /* ID value */
- },
- {
- (char *)"text/x-disorder-playable-tracks", /* drag type */
- GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
- 1 /* ID value */
- },
-};
-
/* Track detail lookup ----------------------------------------------------- */
static void queue_lookups_completed(const char attribute((unused)) *event,
@@ -255,22 +242,27 @@ static void record_queue_map(hash *h,
hash_add(h, id, empty, HASH_INSERT);
nqd = hash_find(h, id);
}
- if(old)
+ if(old) {
+#if DEBUG_QUEUE
+ fprintf(stderr, " old: %s\n", id);
+#endif
nqd->old = old;
- if(new)
+ }
+ if(new) {
+#if DEBUG_QUEUE
+ fprintf(stderr, " new: %s\n", id);
+#endif
nqd->new = new;
+ }
}
-#if 0
+#if DEBUG_QUEUE
static void dump_queue(struct queue_entry *head, struct queue_entry *mark) {
for(struct queue_entry *q = head; q; q = q->next) {
if(q == mark)
- fprintf(stderr, "!");
- fprintf(stderr, "%s", q->id);
- if(q->next)
- fprintf(stderr, " ");
+ fprintf(stderr, " !");
+ fprintf(stderr, " %s\n", q->id);
}
- fprintf(stderr, "\n");
}
static void dump_rows(struct queuelike *ql) {
@@ -280,11 +272,8 @@ static void dump_rows(struct queuelike *ql) {
while(it) {
struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
- fprintf(stderr, "%s", q->id);
- if(it)
- fprintf(stderr, " ");
+ fprintf(stderr, " %s\n", q->id);
}
- fprintf(stderr, "\n");
}
#endif
@@ -300,11 +289,15 @@ void ql_new_queue(struct queuelike *ql,
++suppress_actions;
/* Tell every queue entry which queue owns it */
- //fprintf(stderr, "%s: filling in q->ql\n", ql->name);
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: filling in q->ql\n", ql->name);
+#endif
for(struct queue_entry *q = newq; q; q = q->next)
q->ql = ql;
- //fprintf(stderr, "%s: constructing h\n", ql->name);
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: constructing h\n", ql->name);
+#endif
/* Construct map from id to new and old structures */
hash *h = hash_new(sizeof(struct newqueue_data));
for(struct queue_entry *q = ql->q; q; q = q->next)
@@ -314,7 +307,9 @@ void ql_new_queue(struct queuelike *ql,
/* The easy bit: delete rows not present any more. In the same pass we
* update the secret column containing the queue_entry pointer. */
- //fprintf(stderr, "%s: deleting rows...\n", ql->name);
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: deleting rows...\n", ql->name);
+#endif
GtkTreeIter iter[1];
gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
iter);
@@ -328,10 +323,14 @@ void ql_new_queue(struct queuelike *ql,
ql->ncolumns + QUEUEPOINTER_COLUMN, nqd->new,
-1);
it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+ /* We'll need the new start time */
+ nqd->new->when = q->when;
++kept;
} else {
/* Delete this row (and move iter to the next one) */
- //fprintf(stderr, " delete %s", q->id);
+#if DEBUG_QUEUE
+ fprintf(stderr, " delete %s\n", q->id);
+#endif
it = gtk_list_store_remove(ql->store, iter);
++deleted;
}
@@ -342,7 +341,9 @@ void ql_new_queue(struct queuelike *ql,
/* We're going to have to support arbitrary rearrangements, so we might as
* well add new elements at the end. */
- //fprintf(stderr, "%s: adding rows...\n", ql->name);
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: adding rows...\n", ql->name);
+#endif
struct queue_entry *after = 0;
for(struct queue_entry *q = newq; q; q = q->next) {
const struct newqueue_data *nqd = hash_find(h, q->id);
@@ -363,7 +364,9 @@ void ql_new_queue(struct queuelike *ql,
gtk_list_store_set(ql->store, iter,
ql->ncolumns + QUEUEPOINTER_COLUMN, q,
-1);
- //fprintf(stderr, " add %s", q->id);
+#if DEBUG_QUEUE
+ fprintf(stderr, " add %s\n", q->id);
+#endif
++inserted;
}
after = newq;
@@ -376,49 +379,63 @@ void ql_new_queue(struct queuelike *ql,
* The current code is simple but amounts to a bubble-sort - we might easily
* called gtk_tree_model_iter_next a couple of thousand times.
*/
- //fprintf(stderr, "%s: rearranging rows\n", ql->name);
- //fprintf(stderr, "%s: queue state: ", ql->name);
- //dump_queue(newq, 0);
- //fprintf(stderr, "%s: row state: ", ql->name);
- //dump_rows(ql);
- it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
- iter);
- struct queue_entry *rq = newq; /* r for 'right, correct' */
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: rearranging rows\n", ql->name);
+ fprintf(stderr, "%s: target state:\n", ql->name);
+ dump_queue(newq, 0);
+ fprintf(stderr, "%s: current state:\n", ql->name);
+ dump_rows(ql);
+#endif
+ it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
+ struct queue_entry *tq = newq; /* t-for-target */
int swaps = 0, searches = 0;
+ int row = 0;
while(it) {
- struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
- //fprintf(stderr, " rq = %p, q = %p\n", rq, q);
- //fprintf(stderr, " rq->id = %s, q->id = %s\n", rq->id, q->id);
-
- if(q != rq) {
- //fprintf(stderr, " mismatch\n");
+ struct queue_entry *cq = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
+ /* c-for-current */
+
+ /* Everything has the right queue pointer (see above) so it's sufficient to
+ * compare pointers to detect mismatches */
+ if(cq != tq) {
+#if DEBUG_QUEUE
+ fprintf(stderr, " pointer mismatch at row %d\n", row);
+ fprintf(stderr, " target id %s\n", tq->id);
+ fprintf(stderr, " actual id %s\n", cq->id);
+#endif
+ /* Start looking for the target row fromn the next row */
GtkTreeIter next[1] = { *iter };
gboolean nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
while(nit) {
struct queue_entry *nq = ql_iter_to_q(GTK_TREE_MODEL(ql->store), next);
- //fprintf(stderr, " candidate: %s\n", nq->id);
- if(nq == rq)
+#if DEBUG_QUEUE
+ fprintf(stderr, " candidate: %s\n", nq->id);
+#endif
+ if(nq == tq)
break;
nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
++searches;
}
+ /* Note that this assertion will fail in the face of duplicate IDs.
+ * q->id really does need to be unique. */
assert(nit);
- //fprintf(stderr, " found it\n");
gtk_list_store_swap(ql->store, iter, next);
*iter = *next;
- //fprintf(stderr, "%s: new row state: ", ql->name);
- //dump_rows(ql);
+#if DEBUG_QUEUE
+ fprintf(stderr, "%s: found it. new row state:\n", ql->name);
+ dump_rows(ql);
+#endif
++swaps;
}
/* ...and onto the next one */
it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
- rq = rq->next;
+ tq = tq->next;
+ ++row;
}
-#if 0
+#if DEBUG_QUEUE
fprintf(stderr, "%6s: %3d kept %3d inserted %3d deleted %3d swaps %4d searches\n", ql->name,
kept, inserted, deleted, swaps, searches);
+ fprintf(stderr, "done\n");
#endif
- //fprintf(stderr, "done\n");
ql->q = newq;
/* Set the rest of the columns in new rows */
ql_update_list_store(ql);
@@ -490,6 +507,28 @@ static GtkTreePath *ql_drop_path(GtkWidget *w,
return path;
}
+#if 0
+static const char *act(GdkDragAction action) {
+ struct dynstr d[1];
+
+ dynstr_init(d);
+ if(action & GDK_ACTION_DEFAULT)
+ dynstr_append_string(d, "|DEFAULT");
+ if(action & GDK_ACTION_COPY)
+ dynstr_append_string(d, "|COPY");
+ if(action & GDK_ACTION_MOVE)
+ dynstr_append_string(d, "|MOVE");
+ if(action & GDK_ACTION_LINK)
+ dynstr_append_string(d, "|LINK");
+ if(action & GDK_ACTION_PRIVATE)
+ dynstr_append_string(d, "|PRIVATE");
+ if(action & GDK_ACTION_ASK)
+ dynstr_append_string(d, "|ASK");
+ dynstr_terminate(d);
+ return d->nvec ? d->vec + 1 : "";
+}
+#endif
+
/** @brief Called when a drag moves within a candidate destination
* @param w Destination widget
* @param dc Drag context
@@ -524,8 +563,13 @@ static gboolean ql_drag_motion(GtkWidget *w,
action = GDK_ACTION_MOVE;
else if(dc->actions & GDK_ACTION_COPY)
action = GDK_ACTION_COPY;
- /*fprintf(stderr, "suggested %#x actions %#x result %#x\n",
- dc->suggested_action, dc->actions, action);*/
+ /* TODO this comes up with the wrong answer sometimes. If we are in the
+ * middle of a rearrange then the suggested action will be COPY, which we'll
+ * take, even though MOVE would actually be appropriate. The drag still
+ * seems to work, but it _is_ wrong. The answer is to take the target into
+ * account. */
+ /*fprintf(stderr, "suggested %s actions %s result %s\n",
+ act(dc->suggested_action), act(dc->actions), act(action));*/
if(action) {
// If the action is acceptable then we see if this widget is acceptable
if(gtk_drag_dest_find_target(w, dc, NULL) == GDK_NONE)
@@ -608,12 +652,13 @@ static void ql_drag_data_get_collect(GtkTreeModel *model,
static void ql_drag_data_get(GtkWidget attribute((unused)) *w,
GdkDragContext attribute((unused)) *dc,
GtkSelectionData *data,
- guint attribute((unused)) info_,
+ guint attribute((unused)) info,
guint attribute((unused)) time_,
gpointer user_data) {
struct queuelike *const ql = user_data;
struct dynstr result[1];
+ //fprintf(stderr, "ql_drag_data_get %s info=%d\n", ql->name, info);
dynstr_init(result);
gtk_tree_selection_selected_foreach(ql->selection,
ql_drag_data_get_collect,
@@ -646,7 +691,7 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
gint x,
gint y,
GtkSelectionData *data,
- guint attribute((unused)) info_,
+ guint info_,
guint attribute((unused)) time_,
gpointer user_data) {
struct queuelike *const ql = user_data;
@@ -654,7 +699,7 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
struct vector ids[1], tracks[1];
int parity = 0;
- //fprintf(stderr, "drag-data-received: %d,%d info_=%u\n", x, y, info_);
+ //fprintf(stderr, "drag-data-received: %d,%d info=%u\n", x, y, info_);
/* Get the selection string */
p = result = (char *)gtk_selection_data_get_text(data);
if(!result) {
@@ -687,18 +732,21 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
GtkTreePath *path = ql_drop_path(w, GTK_TREE_MODEL(ql->store), x, y, &pos);
if(path) {
q = ql_path_to_q(GTK_TREE_MODEL(ql->store), path);
+ //fprintf(stderr, " drop path: %s q=%p pos=%d\n",
+ // gtk_tree_path_to_string(path), q, pos);
} else {
/* This generally means a drop past the end of the queue. We find the last
* element in the queue and ask to move after that. */
for(q = ql->q; q && q->next; q = q->next)
;
+ //fprintf(stderr, " after end. q=%p. pos=%d\n", q, pos);
}
switch(pos) {
case GTK_TREE_VIEW_DROP_BEFORE:
case GTK_TREE_VIEW_DROP_INTO_OR_BEFORE:
if(q) {
q = q->prev;
- //fprintf(stderr, " ...but we like to drop near %s\n",
+ //fprintf(stderr, " but we like to drop near %s\n",
// q ? q->id : "NULL");
}
break;
@@ -711,11 +759,12 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
/* Note that q->id can match one of ids[]. This doesn't matter for
* moveafter but TODO may matter for playlist support. */
switch(info_) {
- case 0:
- /* Rearrangement. Send ID and track data. */
+ case QUEUED_TRACKS_ID:
+ case PLAYLIST_TRACKS_ID:
+ /* Rearrangement within some widget. Send ID and track data. */
ql->drop(ql, tracks->nvec, tracks->vec, ids->vec, q);
break;
- case 1:
+ case PLAYABLE_TRACKS_ID:
/* Copying between widgets. IDs mean nothing so don't send them. */
ql->drop(ql, tracks->nvec, tracks->vec, NULL, q);
break;
@@ -724,6 +773,14 @@ static void ql_drag_data_received(GtkWidget attribute((unused)) *w,
gtk_tree_path_free(path);
}
+static int count_drag_targets(const GtkTargetEntry *targets) {
+ const GtkTargetEntry *t = targets;
+
+ while(t->target)
+ ++t;
+ return t - targets;
+}
+
/** @brief Initialize a @ref queuelike */
GtkWidget *init_queuelike(struct queuelike *ql) {
D(("init_queuelike"));
@@ -751,7 +808,7 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
(ql->columns[n].name,
r,
"text", n,
- "background", ql->ncolumns + BACKGROUND_COLUMN,
+ "cell-background", ql->ncolumns + BACKGROUND_COLUMN,
"foreground", ql->ncolumns + FOREGROUND_COLUMN,
(char *)0);
gtk_tree_view_column_set_resizable(c, TRUE);
@@ -763,6 +820,7 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
/* The selection should support multiple things being selected */
ql->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ql->view));
+ g_object_ref(ql->selection);
gtk_tree_selection_set_mode(ql->selection, GTK_SELECTION_MULTIPLE);
/* Catch button presses */
@@ -791,15 +849,15 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
/* This view will act as a drag source */
gtk_drag_source_set(ql->view,
GDK_BUTTON1_MASK,
- queuelike_targets,
- sizeof queuelike_targets / sizeof *queuelike_targets,
- GDK_ACTION_MOVE);
+ ql->drag_source_targets,
+ count_drag_targets(ql->drag_source_targets),
+ ql->drag_dest_actions);
/* This view will act as a drag destination */
gtk_drag_dest_set(ql->view,
GTK_DEST_DEFAULT_HIGHLIGHT|GTK_DEST_DEFAULT_DROP,
- queuelike_targets,
- sizeof queuelike_targets / sizeof *queuelike_targets,
- GDK_ACTION_MOVE|GDK_ACTION_COPY);
+ ql->drag_dest_targets,
+ count_drag_targets(ql->drag_dest_targets),
+ ql->drag_dest_actions);
g_signal_connect(ql->view, "drag-motion",
G_CALLBACK(ql_drag_motion), ql);
g_signal_connect(ql->view, "drag-leave",
@@ -814,9 +872,9 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
/* For queues that cannot accept a drop we still accept a copy out */
gtk_drag_source_set(ql->view,
GDK_BUTTON1_MASK,
- queuelike_targets,
- sizeof queuelike_targets / sizeof *queuelike_targets,
- GDK_ACTION_COPY);
+ ql->drag_source_targets,
+ count_drag_targets(ql->drag_source_targets),
+ ql->drag_source_actions);
g_signal_connect(ql->view, "drag-data-get",
G_CALLBACK(ql_drag_data_get), ql);
make_treeview_multidrag(ql->view, NULL);
@@ -824,7 +882,8 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
/* TODO style? */
- ql->init(ql);
+ if(ql->init)
+ ql->init(ql);
/* Update display text when lookups complete */
event_register("lookups-completed", queue_lookups_completed, ql);
@@ -834,6 +893,31 @@ GtkWidget *init_queuelike(struct queuelike *ql) {
return scrolled;
}
+/** @brief Destroy a queuelike
+ * @param ql Queuelike to destroy
+ *
+ * Returns @p ql to its initial state.
+ */
+void destroy_queuelike(struct queuelike *ql) {
+ if(ql->store) {
+ g_object_unref(ql->store);
+ ql->store = NULL;
+ }
+ if(ql->view) {
+ gtk_object_destroy(GTK_OBJECT(ql->view));
+ ql->view = NULL;
+ }
+ if(ql->menu) {
+ gtk_object_destroy(GTK_OBJECT(ql->menu));
+ ql->menu = NULL;
+ }
+ if(ql->selection) {
+ g_object_unref(ql->selection);
+ ql->selection = NULL;
+ }
+ ql->q = NULL;
+}
+
/*
Local Variables:
c-basic-offset:2
diff --git a/disobedience/queue-generic.h b/disobedience/queue-generic.h
index c15b383..b774f22 100644
--- a/disobedience/queue-generic.h
+++ b/disobedience/queue-generic.h
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2006-2009 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -104,10 +104,30 @@ struct queuelike {
void (*drop)(struct queuelike *ql, int ntracks, char **tracks, char **ids,
struct queue_entry *after_me);
- /** @brief Stashed drag target row */
- GtkTreePath *drag_target;
+ /** @brief Source target list */
+ const GtkTargetEntry *drag_source_targets;
+
+ /** @brief Drag source actions */
+ GdkDragAction drag_source_actions;
+
+ /** @brief Destination target list */
+ const GtkTargetEntry *drag_dest_targets;
+
+ /** @brief Drag destination actions */
+ GdkDragAction drag_dest_actions;
+
};
+enum {
+ PLAYABLE_TRACKS_ID,
+ QUEUED_TRACKS_ID,
+ PLAYLIST_TRACKS_ID
+};
+
+#define PLAYABLE_TRACKS (char *)"text/x-disorder-playable-tracks"
+#define QUEUED_TRACKS (char *)"text/x-disorder-queued-tracks"
+#define PLAYLIST_TRACKS (char *)"text/x-disorder-playlist-tracks"
+
enum {
QUEUEPOINTER_COLUMN,
FOREGROUND_COLUMN,
@@ -116,15 +136,8 @@ enum {
EXTRA_COLUMNS
};
-/* TODO probably need to set "horizontal-separator" to 0, but can't find any
- * coherent description of how to set style properties in isolation. */
-#define BG_PLAYING 0
-#define FG_PLAYING 0
-
-#ifndef BG_PLAYING
-# define BG_PLAYING "#e0ffe0"
-# define FG_PLAYING "black"
-#endif
+#define BG_PLAYING "#e0ffe0"
+#define FG_PLAYING "black"
extern struct queuelike ql_queue;
extern struct queuelike ql_recent;
@@ -157,6 +170,7 @@ gboolean ql_button_release(GtkWidget *widget,
GdkEventButton *event,
gpointer user_data);
GtkWidget *init_queuelike(struct queuelike *ql);
+void destroy_queuelike(struct queuelike *ql);
void ql_update_list_store(struct queuelike *ql) ;
void ql_update_row(struct queue_entry *q,
GtkTreeIter *iter);
diff --git a/disobedience/queue-menu.c b/disobedience/queue-menu.c
index beb40f5..ebf8555 100644
--- a/disobedience/queue-menu.c
+++ b/disobedience/queue-menu.c
@@ -71,7 +71,7 @@ void ql_properties_activate(GtkMenuItem attribute((unused)) *menuitem,
gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
}
if(v->nvec)
- properties(v->nvec, (const char **)v->vec);
+ properties(v->nvec, (const char **)v->vec, ql->view);
}
/* Scratch */
diff --git a/disobedience/queue.c b/disobedience/queue.c
index 80b163a..c495bd8 100644
--- a/disobedience/queue.c
+++ b/disobedience/queue.c
@@ -72,7 +72,7 @@ static void queue_playing_changed(void) {
ql_new_queue(&ql_queue, q);
/* Tell anyone who cares */
event_raise("queue-list-changed", q);
- event_raise("playing-track-changed", q);
+ event_raise("playing-track-changed", playing_track);
}
/** @brief Update the queue itself */
@@ -135,6 +135,21 @@ static gboolean playing_periodic(gpointer attribute((unused)) data) {
/* If there's a track playing, update its row */
if(playing_track)
ql_update_row(playing_track, 0);
+ /* If the first (nonplaying) track starts in the past, update the queue to
+ * get new expected start times; but rate limit this checking. (If we only
+ * do it once a minute then the rest of the queue can get out of date too
+ * easily.) */
+ struct queue_entry *q = ql_queue.q;
+ if(q) {
+ if(q == playing_track)
+ q = q->next;
+ if(q) {
+ time_t now;
+ time(&now);
+ if(q->expected / 15 < now / 15)
+ queue_changed(0,0,0);
+ }
+ }
return TRUE;
}
@@ -142,6 +157,7 @@ static gboolean playing_periodic(gpointer attribute((unused)) data) {
static void queue_init(struct queuelike attribute((unused)) *ql) {
/* Arrange a callback whenever the playing state changes */
event_register("playing-changed", playing_changed, 0);
+ event_register("playing-started", playing_changed, 0);
/* We reget both playing track and queue at pause/resume so that start times
* can be computed correctly */
event_register("pause-changed", playing_changed, 0);
@@ -217,12 +233,28 @@ static const struct queue_column queue_columns[] = {
/** @brief Pop-up menu for queue */
static struct menuitem queue_menuitems[] = {
- { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
- { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
- { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
- { "Scratch playing track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
- { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 },
- { "Adopt track", ql_adopt_activate, ql_adopt_sensitive, 0, 0 },
+ { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive, 0, 0 },
+ { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+ { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+ { "Scratch playing track", GTK_STOCK_STOP, ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
+ { "Remove track from queue", GTK_STOCK_DELETE, ql_remove_activate, ql_remove_sensitive, 0, 0 },
+ { "Adopt track", NULL, ql_adopt_activate, ql_adopt_sensitive, 0, 0 },
+};
+
+static const GtkTargetEntry queue_targets[] = {
+ {
+ QUEUED_TRACKS, /* drag type */
+ GTK_TARGET_SAME_WIDGET, /* rearrangement within a widget */
+ QUEUED_TRACKS_ID /* ID value */
+ },
+ {
+ PLAYABLE_TRACKS, /* drag type */
+ GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
+ PLAYABLE_TRACKS_ID, /* ID value */
+ },
+ {
+ .target = NULL
+ }
};
struct queuelike ql_queue = {
@@ -232,7 +264,11 @@ struct queuelike ql_queue = {
.ncolumns = sizeof queue_columns / sizeof *queue_columns,
.menuitems = queue_menuitems,
.nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems,
- .drop = queue_drop
+ .drop = queue_drop,
+ .drag_source_targets = queue_targets,
+ .drag_source_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+ .drag_dest_targets = queue_targets,
+ .drag_dest_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
};
/** @brief Called when a key is pressed in the queue tree view */
@@ -274,6 +310,45 @@ int queued(const char *track) {
return 0;
}
+/* Playing widget for mini-mode */
+
+static void queue_set_playing_widget(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void *callbackdata) {
+ GtkLabel *w = callbackdata;
+
+ if(playing_track) {
+ const char *artist = namepart(playing_track->track, "display", "artist");
+ const char *album = namepart(playing_track->track, "display", "album");
+ const char *title = namepart(playing_track->track, "display", "title");
+ const char *ldata = column_length(playing_track, NULL);
+ if(!ldata)
+ ldata = "";
+ char *text;
+ byte_xasprintf(&text, "%s/%s/%s %s", artist, album, title, ldata);
+ gtk_label_set_text(w, text);
+ } else
+ gtk_label_set_text(w, "");
+}
+
+GtkWidget *playing_widget(void) {
+ GtkWidget *w = gtk_label_new("");
+ gtk_misc_set_alignment(GTK_MISC(w), 1.0, 0);
+ /* Spot changes to the playing track */
+ event_register("playing-track-changed",
+ queue_set_playing_widget,
+ w);
+ /* Use the best-known name for it */
+ event_register("lookups-complete",
+ queue_set_playing_widget,
+ w);
+ /* Keep the amount played so far up to date */
+ event_register("periodic-fast",
+ queue_set_playing_widget,
+ w);
+ return frame_widget(w, NULL);
+}
+
/*
Local Variables:
c-basic-offset:2
diff --git a/disobedience/recent.c b/disobedience/recent.c
index f53e631..510aac9 100644
--- a/disobedience/recent.c
+++ b/disobedience/recent.c
@@ -78,10 +78,10 @@ static const struct queue_column recent_columns[] = {
/** @brief Pop-up menu for recently played list */
static struct menuitem recent_menuitems[] = {
- { "Track properties", ql_properties_activate, ql_properties_sensitive,0, 0 },
- { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
- { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
- { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+ { "Track properties", GTK_STOCK_PROPERTIES, ql_properties_activate, ql_properties_sensitive,0, 0 },
+ { "Play track", GTK_STOCK_MEDIA_PLAY, ql_play_activate, ql_play_sensitive, 0, 0 },
+ { "Select all tracks", GTK_STOCK_SELECT_ALL, ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+ { "Deselect all tracks", NULL, ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
};
struct queuelike ql_recent = {
@@ -91,6 +91,8 @@ struct queuelike ql_recent = {
.ncolumns = sizeof recent_columns / sizeof *recent_columns,
.menuitems = recent_menuitems,
.nmenuitems = sizeof recent_menuitems / sizeof *recent_menuitems,
+ .drag_source_targets = choose_targets,
+ .drag_source_actions = GDK_ACTION_COPY,
};
GtkWidget *recent_widget(void) {
diff --git a/disobedience/rtp.c b/disobedience/rtp.c
index cbedb18..4693173 100644
--- a/disobedience/rtp.c
+++ b/disobedience/rtp.c
@@ -1,6 +1,6 @@
/*
* This file is part of Disobedience
- * Copyright (C) 2007 Richard Kettlewell
+ * Copyright (C) 2007-2010 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -108,7 +108,7 @@ void start_rtp(void) {
if(!(pid = xfork())) {
if(setsid() < 0)
disorder_fatal(errno, "error calling setsid");
- if(!(pid = xfork())) {
+ if(!xfork()) {
/* grandchild */
exitfn = _exit;
/* log errors and output somewhere reasonably sane. rtp_running()
diff --git a/disobedience/users.c b/disobedience/users.c
index 1d2a169..b078698 100644
--- a/disobedience/users.c
+++ b/disobedience/users.c
@@ -660,13 +660,15 @@ static struct button users_buttons[] = {
GTK_STOCK_ADD,
users_add,
"Create a new user",
- 0
+ 0,
+ NULL,
},
{
GTK_STOCK_REMOVE,
users_delete,
"Delete a user",
- 0
+ 0,
+ NULL,
},
};
#define NUSERS_BUTTONS (sizeof users_buttons / sizeof *users_buttons)
diff --git a/doc/Makefile.am b/doc/Makefile.am
index eaad306..b3d8926 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -35,19 +35,31 @@ SEDFILES=disorder.1 disorderd.8 disorder_config.5 \
include ${top_srcdir}/scripts/sedfiles.make
-HTMLMAN=$(foreach man,$(man_MANS),$(man).html)
-$(HTMLMAN) : %.html : % $(top_srcdir)/scripts/htmlman
- rm -f $@.new
- $(top_srcdir)/scripts/htmlman $< >$@.new
- chmod 444 $@.new
- mv -f $@.new $@
+HTMLMAN=disorderd.8.html disorder.1.html disorder.3.html \
+disorder_config.5.html disorder-dump.8.html disorder_protocol.5.html \
+disorder-deadlock.8.html disorder-rescan.8.html disobedience.1.html \
+disorderfm.1.html disorder-speaker.8.html disorder-playrtp.1.html \
+disorder-normalize.8.html disorder-decode.8.html disorder-stats.8.html \
+disorder-dbupgrade.8.html disorder_templates.5.html \
+disorder_actions.5.html disorder_options.5.html disorder.cgi.8.html \
+disorder_preferences.5.html disorder-choose.8.html
-TMPLMAN=$(foreach man,$(man_MANS),$(man).tmpl)
-$(TMPLMAN) : %.tmpl : % $(top_srcdir)/scripts/htmlman
- rm -f $@.new
- $(top_srcdir)/scripts/htmlman -stdhead $< >$@.new
- chmod 444 $@.new
- mv -f $@.new $@
+$(wordlist 2,9999,$(HTMLMAN)): disorderd.8.html
+disorderd.8.html: $(man_MANS)
+ $(top_srcdir)/scripts/htmlman -- $^
+
+TMPLMAN=disorderd.8.tmpl disorder.1.tmpl disorder.3.tmpl \
+disorder_config.5.tmpl disorder-dump.8.tmpl disorder_protocol.5.tmpl \
+disorder-deadlock.8.tmpl disorder-rescan.8.tmpl disobedience.1.tmpl \
+disorderfm.1.tmpl disorder-speaker.8.tmpl disorder-playrtp.1.tmpl \
+disorder-normalize.8.tmpl disorder-decode.8.tmpl disorder-stats.8.tmpl \
+disorder-dbupgrade.8.tmpl disorder_templates.5.tmpl \
+disorder_actions.5.tmpl disorder_options.5.tmpl disorder.cgi.8.tmpl \
+disorder_preferences.5.tmpl disorder-choose.8.tmpl
+
+$(wordlist 2,9999,$(TMPLMAN)): disorderd.8.tmpl
+disorderd.8.tmpl: $(man_MANS)
+ $(top_srcdir)/scripts/htmlman -stdhead -extension tmpl -- $^
disorder_templates.5.in: disorder_templates.5.head disorder_templates.5.tail \
$(top_srcdir)/lib/macros-builtin.c \
diff --git a/doc/disobedience.1.in b/doc/disobedience.1.in
index f7fb5c2..4462237 100644
--- a/doc/disobedience.1.in
+++ b/doc/disobedience.1.in
@@ -1,5 +1,5 @@
.\"
-.\" Copyright (C) 2004-2008 Richard Kettlewell
+.\" Copyright (C) 2004-2009 Richard Kettlewell
.\"
.\" This program is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
@@ -23,337 +23,9 @@ disobedience \- GUI client for DisOrder jukebox
.SH DESCRIPTION
.B disobedience
is a graphical client for DisOrder.
-.SH "WINDOWS AND ICONS"
-.SS "Server Menu"
-This has the following options:
-.TP
-.B Login
-Brings up the \fBLogin Details Window\fR; see below.
-.TP
-.B "Manage Users"
-Brings up the \fBUser Management Window\fR; see below.
-.TP
-.B Quit
-Terminates the program.
-.SS "Edit Menu"
-This has the following options:
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.SS "Control Menu"
-This has the following options:
-.TP
-.B Scratch
-Interrupts the currently playing track.
-.TP
-.B Playing
-Pause and resume the current track.
-.TP
-.B "Random play"
-Enable and disable random play.
-Does not take effect until the currently playing track finishes.
-.TP
-.B "Network player"
-Enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-.SS "Help Menu"
-This has only one option, "About DisOrder", which pops up a box giving the
-name, author and version number of the software.
-.SS "Controls"
-.TP
-.B "Pause button"
-The pause button can be used to pause and resume tracks.
-This button shows either a pause symbol (two vertical bars) or a resume symbol
-(a right-pointing arrow).
-.TP
-.B "Scratch button"
-The scratch button, a red cross, can be used to interrupt the currently playing
-track.
-.TP
-.B "Random play button"
-The random play button can be used to enable and disable random play.
-It does not take effect until the currently playing track finishes.
-When the button is green, random play is enabled.
-When it is grey, random play is disabled.
-.TP
-.B "Play button"
-The play button controls whether tracks will be played at all.
-As above it does not take effect until the currently playing track finishes.
-When the button is green, play is enabled.
-When it is grey, play is disabled.
-.TP
-.B "Network play button"
-The network play buttons enables or disables network play.
-See
-.B "NETWORK PLAY"
-below.
-When the button is green, network play is enabled.
-When it is grey, network play is disabled.
-.TP
-.B "Volume slider"
-The volume slider indicates the current volume level and can be used to adjust
-it.
-0 is silent and 10 is maximum volume.
-.TP
-.B "Balance slider"
-The balance slider indicates the current balance and can be used to adjust it.
-\-1 means only the left speaker, 0 means both speakers at equal volume and +1
-means the only the right speaker.
-.SS "Queue Tab"
-This displays the currently playing track and the queue.
-The currently playing track is at the top, and can be distinguished by
-the constantly updating timer.
-Queued tracks appear below it.
-.PP
-The left button can be use to select and deselect tracks.
-On its own it just selects the pointed track and deselects everything else.
-With CTRL it flips the state of the pointed track without affecting anything
-else.
-With SHIFT it selects every track from the last click to the current position
-and deselects everything else.
-With both CTRL and SHIFT it selects everything from the last click to the
-current position without deselecting anything.
-.PP
-Tracks can be moved within the queue by dragging them to a new position with
-the left button.
-.PP
-The right button pops up a menu.
-This has the following options:
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.TP
-.B Scratch
-Interrupt the currently playing track.
-(Note that this appears even if you right click over a queued track rather
-than the currently playing track.)
-.TP
-.B "Remove track from queue"
-Remove the selected tracks from the queue.
-.TP
-.B "Adopt track"
-Sets the submitter of a randomly picked track to you.
-.SS "Recent Tab"
-This displays recently played tracks, the most recent at the top.
-.PP
-The left button functions as above, except that drag-and-drop rearrangement
-is not possible.
-The right button pops up a menu with the following options:
-.TP
-.B Properties
-Edit the details of the selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Play track"
-Play the select track(s);
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.SS "Choose Tab"
-This displays all the tracks known to the server in a tree structure.
-.PP
-Directories are represented with an arrow to their left.
-This can be clicked to reveal or hide the contents of the directory.
-The top level "directories" break up tracks by their first letter.
-.PP
-Playable files are represented by their name.
-If they are playing or in the queue then a notes icon appears next to them.
-.PP
-Left clicking on a file will select it.
-As with the queue tab you can use SHIFT and CTRL to select multiple files.
-.PP
-Files may be played by dragging them to the queue tab and thence to a
-destination position in the queue.
-.PP
-The text box at the bottom is a search form.
-If you enter search terms here then tracks containing all those words will be
-highlighted.
-You can also limit the results to tracks with particular tags, by including
-\fBtag:\fITAG\fR for each tag.
-.PP
-To start a new search just edit the contents of the search box.
-The cancel button to its right clears the current search.
-The up and down arrows will scroll the window to make the previous or next
-search result visible.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B Play
-Play selected tracks.
-.TP
-.B Properties
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.PP
-A middle click on a track will add it to the queue.
.PP
-Right clicking over a directory will pop up a menu with the following options:
-.TP
-.B "Play all tracks"
-Play all the tracks in the directory, in the order they appear on screen.
-.TP
-.B "Track properties"
-Edit properties of all tracks in the directory.
-.TP
-.B "Select children"
-Select all the tracks in the directory (and deselect everything else).
-.TP
-.B "Deselect all tracks"
-Deselect everything.
-.SS "Added Tab"
-This displays a list of tracks recently added to the server's database.
-The most recently added track is at the top.
-.PP
-Left clicking a track will select it.
-CTRL and SHIFT work as above to select muliple files.
-.PP
-Right clicking over a track will pop up a menu with the following options:
-.TP
-.B "Track properties"
-Edit properties of selected tracks.
-See
-.B "Properties Window"
-below.
-.TP
-.B "Play track"
-Play selected tracks.
-.TP
-.B "Select All Tracks"
-Select all tracks.
-.TP
-.B "Deselect All Tracks"
-Deselect all tracks.
-.SS "Login Details Window"
-The login details window allows you to edit the connection details and
-authorization information used by Disobedience.
-.PP
-At the top is a 'remote' switch.
-If this is enabled then you can use the \fBHostname\fR and \fBService\fR
-fields to connect to a remote server.
-If it is disabled then then Disobedience will connect to a local server
-instead.
-.PP
-Below this are four text entry fields:
-.TP
-.B Hostname
-The host to connect to.
-.TP
-.B Service
-The service name or port number to connect to.
-.TP
-.B "User name"
-The user name to log in as.
-.TP
-.B Password
-The password to use when logging in.
-Note that this is NOT your login password but is your password to the
-DisOrder server.
-.PP
-It has two buttons:
-.TP
-.B Login
-This button attempts to (re-)connect to the server with the currently displayed
-settings.
-The settings are saved in
-.IR $HOME/.disorder/passwd .
-on success.
-.TP
-.B Close
-This button closes the window, discarding any unsaved changes.
-.SS "Properties Window"
-This window contains details of one or more tracks and allows them to be
-edited.
-.PP
-The Artist, Album and Title fields determine how the tracks appear in
-the queue and recently played tabs.
-.PP
-The Tags field determine which tags apply to the track.
-Tags are separated by commas and can contain any printing characters except
-comma.
-.PP
-The Weight field determines the track weight. Tracks with higher weights are
-proportionately more likely to be picked at random. The default weight is
-90000, and the maximum weight is 2147483647.
-.PP
-The Random checkbox determines whether the track will be picked at random.
-Random play is enabled for every track by default, but it can be turned off
-here.
-.PP
-The double-headed arrow to the right of each preference will propagate its
-value to all the other tracks in the window.
-For instance, this can be used to efficiently correct the artist or album
-fields, or bulk-disable random play for many tracks.
-.PP
-Press "OK" to confirm all changes and close the window, "Apply" to confirm
-changes but keep the window open and "Cancel" to close the window and discard
-all changes.
-.SS "User Management Window"
-This window is primarily of interest to adminstrators, and will not be
-available to users without admin rights. The left hand side is a list of all
-users; the right hand side contains the editable details of the currently
-selected user.
-.PP
-When you select any user you can edit their email address or change their
-password. It is also possible to edit the individual user rights. Click on
-the "Apply" button to commit any changes you make.
-.PP
-The "Add" button creates a new user. You must enter at least a username.
-Default rights are setting according to local configuration, \fInot\fR server
-configuration (but this may be changed in the future). As above, click on
-"Apply" to actually create the new user.
-.PP
-The "Delete" button deletes the selected user. This operation cannot be
-undone.
-.SH "KEYBOARD SHORTCUTS"
-.TP
-.B CTRL+A
-Select all tracks (queue/recent)
-.TP
-.B CTRL+L
-Brings up the \fBLogin Details Window\fR.
-.TP
-.B CTRL+Q
-Quit.
-.SH "NETWORK PLAY"
-Network play uses a background
-.BR disorder\-playrtp (1)
-process.
-If you quit Disobedience the player will continue playing and can be
-disabled from a later run of Disobedience.
-.PP
-The player will log to
-.I ~/.disorder/HOSTNAME\-rtp.log
-so look there if it does not seem to be working.
-.PP
-You can stop it without running Disobedience by the command
-.BR "killall disorder\-playrtp" .
+Please refer to Disobedience's HTML manual for further information. This can
+be found at dochtmldir/index.html.
.SH OPTIONS
.TP
.B \-\-config \fIPATH\fR, \fB\-c \fIPATH
@@ -383,41 +55,6 @@ The screen number to use.
.\" .TP
.\" .B \-\-sync
.\" Make all X requests synchronously.
-.SH CONFIGURATION
-If you are using
-.B disobedience
-on the same host as the server then no additional configuration should be
-required.
-.PP
-If it is running on a different host then the easiest way to set it up is to
-use the login details window in Disobedience.
-Enter the connection details, use Login to connect to the server, and then
-use Save to store them for future sessions.
-.PP
-The other clients read their configuration from the same location so after
-setting up with Disobedience, tools such as
-.BR disorder (1)
-should work as well.
-.SH BUGS
-There is no particular provision for multiple users of the same computer
-sharing a single \fBdisorder\-playrtp\fR process.
-This shouldn't be too much of a problem in practice but something could
-perhaps be done given demand.
-.PP
-Try to do remote user management when the server is configured to refuse this
-produces rather horrible error behavior.
-.PP
-Only one track can be dragged at a time.
-.PP
-Resizing columns doesn't work very well.
-This is a GTK+ bug.
-.SH FILES
-.TP
-.I ~/.disorder/HOSTNAME\-rtp
-Socket for communication with RTP player.
-.TP
-.I ~/.disorder/HOSTNAME\-rtp.log
-Log file for RTP player.
.SH "SEE ALSO"
.BR disorder\-playrtp (1),
.BR disorder_config (5)
diff --git a/doc/disorder.3 b/doc/disorder.3
index 2b71bf9..cbd2278 100644
--- a/doc/disorder.3
+++ b/doc/disorder.3
@@ -318,8 +318,7 @@ A standalone player that writes directly to some suitable audio
device.
.TP
.B DISORDER_PLAYER_RAW
-A player that writes raw samples to \fB$DISORDER_RAW_FD\fR, for
-instance by using the \fBdisorder\fR libao driver.
+A player that writes raw samples to \fB$DISORDER_RAW_FD\fR.
.RE
.IP
Known capabilities are:
diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in
index ef3a70f..0c8f7c8 100644
--- a/doc/disorder_config.5.in
+++ b/doc/disorder_config.5.in
@@ -325,7 +325,6 @@ For \fBapi oss\fR the possible values are:
.RS
.TP 8
.B pcm
-
Output level for the audio device.
This is probably what you want and is the default.
.TP
@@ -457,6 +456,10 @@ The default is 0.
.IP
For \fBapi coreaudio\fR, volume setting is not currently supported.
.TP
+.B mount_rescan yes\fR|\fBno
+Determines whether mounts and unmounts will cause an automatic rescan.
+The default is \fByes\fR.
+.TP
.B multicast_loop yes\fR|\fBno
Determines whether multicast packets are loop backed to the sending host.
The default is \fByes\fR.
@@ -609,9 +612,6 @@ Identical to the \fBexec\fR except that the player is expected to use the
DisOrder raw player protocol.
.BR disorder-decode (8)
can decode several common audio file formats to this format.
-If your favourite format is not supported, but you have a player
-which uses libao, there is also a libao driver which supports this format;
-see below for more information about this.
.TP
.B shell \fR[\fISHELL\fR] \fICOMMAND\fR
The command is executed using the shell.
@@ -808,8 +808,14 @@ This must be set if you have online registration enabled.
.TP
.B refresh \fISECONDS\fR
Specifies the maximum refresh period in seconds.
+The refresh period is the time after which the web interface's queue and manage
+pages will automatically reload themselves.
Default 15.
.TP
+.B refresh_min \fISECONDS\fR
+Specifies the minimum refresh period in seconds.
+Default 1.
+.TP
.B sendmail \fIPATH\fR
The path to the Sendmail executable.
This must support the \fB-bs\fR option (Postfix, Exim and Sendmail should all
@@ -864,25 +870,6 @@ longer needs to be specified.
.IP
This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not
\fB/cgi-bin/jukebox\fR.
-.SH "LIBAO DRIVER"
-.SS "Raw Protocol Players"
-Raw protocol players are expected to use the \fBdisorder\fR libao driver.
-Programs that use libao generally have command line options to select the
-driver and pass options to it.
-.SS "Driver Options"
-The known driver options are:
-.TP
-.B fd
-The file descriptor to write to.
-If this is not specified then the driver looks like the environment
-variable \fBDISORDER_RAW_FD\fR.
-If that is not set then the default is 1 (i.e. standard output).
-.TP
-.B fragile
-If this is set to a nonzero value then the driver will call \fB_exit\fR(2) if a
-write to the output file descriptor fails.
-This is a workaround for buggy players such as \fBogg123\fR that ignore
-write errors.
.SH "REGEXP SUBSTITUTION RULES"
Regexps are PCRE regexps, as defined in \fBpcrepattern\fR(3).
The only option used is \fBPCRE_UTF8\fR.
diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in
index 6bc9c47..bb95ff0 100644
--- a/doc/disorder_protocol.5.in
+++ b/doc/disorder_protocol.5.in
@@ -122,7 +122,7 @@ List all the files in \fIDIRECTORY\fR in a response body.
If \fIREGEXP\fR is present only matching files are returned.
.TP
.B get \fITRACK\fR \fIPREF\fR
-Getsa preference value.
+Gets a preference value.
On success the second field of the response line will have the value.
.IP
If the track or preference do not exist then the response code is 555.
@@ -171,7 +171,7 @@ depending on how the tracks came to be added to the queue.
.TP
.B new \fR[\fIMAX\fR]
Send the most recently added \fIMAX\fR tracks in a response body.
-If the argument is ommitted, the \fBnew_max\fR most recent tracks are
+If the argument is omitted, the \fBnew_max\fR most recent tracks are
listed (see \fBdisorder_config\fR(5)).
.TP
.B nop
@@ -232,6 +232,7 @@ Requires permission to modify that playlist and the \fBplay\fR right.
.B playlist-get \fIPLAYLIST\fR
Get the contents of a playlist, in a response body.
Requires permission to read that playlist and the \fBread\fR right.
+If the playlist does not exist the response is 555.
.TP
.B playlist-get-share \fIPLAYLIST\fR
Get the sharing status of a playlist.
@@ -475,7 +476,7 @@ With two parameters sets each side independently.
Setting the volume requires the \fBvolume\fR right.
.SH RESPONSES
Responses are three-digit codes.
-The first digit distinguishes errors from succesful responses:
+The first digit distinguishes errors from successful responses:
.TP
.B 2
Operation succeeded.
diff --git a/driver/Makefile.am b/driver/Makefile.am
deleted file mode 100644
index df8fe1f..0000000
--- a/driver/Makefile.am
+++ /dev/null
@@ -1,50 +0,0 @@
-#
-# This file is part of DisOrder
-# Copyright (C) 2005, 2007, 2008 Richard Kettlewell
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-#
-
-
-aolib_LTLIBRARIES=libdisorder.la
-aolibdir=${libdir}/ao/plugins-2
-finkaolibdir=${finkdir}/lib/ao/plugins-2
-usraolibdir=/usr/lib/ao/plugins-2
-AM_CPPFLAGS=-I${top_srcdir}/lib
-
-libdisorder_la_SOURCES=disorder.c
-libdisorder_la_LDFLAGS=-module
-
-install-data-hook:
-
-# Link ao driver into right location. If you have some other location then
-# you'll need to modify this or link it manually.
-#
-# We don't mess with this for now; since disorder-decode covers some common
-# cases, the libao driver is less useful than it was.
-link-ao-driver:
- @if test -d ${DESTDIR}${finkaolibdir} \
- && test ${finkaolibdir} != ${aolibdir}; then \
- echo rm -f ${DESTDIR}${finkaolibdir}/libdisorder.*; \
- rm -f ${DESTDIR}${finkaolibdir}/libdisorder.*; \
- echo ln ${aolibdir}/libdisorder.* ${DESTDIR}${finkaolibdir}; \
- ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${finkaolibdir}; \
- fi
- @if test -d ${DESTDIR}${usraolibdir} \
- && test ${usraolibdir} != ${aolibdir}; then \
- echo rm -f ${DESTDIR}${usraolibdir}/libdisorder.*; \
- rm -f ${DESTDIR}${usraolibdir}/libdisorder.*; \
- echo ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${usraolibdir}; \
- ln ${DESTDIR}${aolibdir}/libdisorder.* ${DESTDIR}${usraolibdir}; \
- fi
diff --git a/driver/disorder.c b/driver/disorder.c
deleted file mode 100644
index e6b6575..0000000
--- a/driver/disorder.c
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * This file is part of DisOrder.
- * Copyright (C) 2005, 2007 Richard Kettlewell
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-/** @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
-#include
-#include
-#include
-#include
-
-#include "speaker-protocol.h"
-
-/* extra declarations to help out lazy */
-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:
-*/
diff --git a/images/Makefile.am b/images/Makefile.am
index ffe393c..36a6ad5 100644
--- a/images/Makefile.am
+++ b/images/Makefile.am
@@ -1,6 +1,6 @@
#
# This file is part of DisOrder.
-# Copyright (C) 2005-2008 Richard Kettlewell
+# Copyright (C) 2005-2010 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -27,6 +27,29 @@ disobedience32x32.xpm cross.svg go.svg notes.svg noteson.svg pause.svg \
query.svg queryon.svg speaker.svg speakeron.svg cross32.png \
pause32.png play32.png playdisabled32.png playenabled32.png \
randomdisabled32.png randomenabled32.png rtpdisabled32.png \
-rtpenabled32.png duck55.png
+rtpenabled32.png duck55.png cards24.png cards48.png \
+cards-simple-fanned.svg cards-thin.svg
-CLEANFILES=$(SEDFILES)
+DISOBEDIENCE_IMAGES=up.png down.png cards24.png logo256.png duck.png \
+propagate.png
+
+if GTK
+noinst_HEADERS=images.h
+
+images.h: $(DISOBEDIENCE_IMAGES)
+ set -e; \
+ exec > @$.new; \
+ for png in $^; do \
+ name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`; \
+ gdk-pixbuf-csource --raw --name=image_$$name $$png; \
+ done; \
+ echo "static const struct image images[] = {"; \
+ for png in `echo $^`; do \
+ name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`; \
+ echo " { \"$$name.png\", image_$$name },"; \
+ done|sort; \
+ echo "};"
+ mv @$.new $@
+endif
+
+CLEANFILES=$(SEDFILES) images.h
diff --git a/images/cards-simple-fanned.svg b/images/cards-simple-fanned.svg
new file mode 100644
index 0000000..9ae9730
--- /dev/null
+++ b/images/cards-simple-fanned.svg
@@ -0,0 +1,158 @@
+
+
+
diff --git a/images/cards-thin.svg b/images/cards-thin.svg
new file mode 100644
index 0000000..94e1255
--- /dev/null
+++ b/images/cards-thin.svg
@@ -0,0 +1,155 @@
+
+
+
diff --git a/images/cards24.png b/images/cards24.png
new file mode 100644
index 0000000..24e90fb
Binary files /dev/null and b/images/cards24.png differ
diff --git a/images/cards48.png b/images/cards48.png
new file mode 100644
index 0000000..e929b75
Binary files /dev/null and b/images/cards48.png differ
diff --git a/lib/Makefile.am b/lib/Makefile.am
index ec674f6..eea6220 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -52,6 +52,7 @@ libdisorder_a_SOURCES=charset.c charsetf.c charset.h \
heap.h \
hex.c hex.h \
hostname.c hostname.h \
+ hreader.c hreader.h \
ifreq.c ifreq.h \
inputline.c inputline.h \
kvp.c kvp.h \
@@ -91,6 +92,7 @@ libdisorder_a_SOURCES=charset.c charsetf.c charset.h \
unicode.h unicode.c \
unidata.h unidata.c \
vacopy.h \
+ validity.c validity.h \
vector.c vector.h \
version.c version.h \
wav.h wav.c \
@@ -121,6 +123,7 @@ definitions.h: Makefile
echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new
echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new
echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new
+ echo "#define DOCHTMLDIR \"${dochtmldir}\"" >> $@.new
echo "#define SBINDIR \"${sbindir}/\"" >> $@.new
echo "#define BINDIR \"${bindir}/\"" >> $@.new
echo "#define FINKBINDIR \"${finkbindir}/\"" >> $@.new
@@ -134,10 +137,6 @@ defs.lo: definitions.h versionstring.h
rebuild-unicode:
cd ${srcdir} && ${top_srcdir}/scripts/make-unidata
-%.i: %.c
- $(CPP) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) -c $< > $@.new
- mv $@.new $@
-
CLEANFILES=definitions.h definitions.h.new version-string versionstring.h \
*.gcda *.gcov *.gcno *.c.html index.html
diff --git a/lib/cgi.c b/lib/cgi.c
index 15b556c..9fd42f1 100644
--- a/lib/cgi.c
+++ b/lib/cgi.c
@@ -226,7 +226,6 @@ void cgi_clear(void) {
*/
char *cgi_sgmlquote(const char *src) {
uint32_t *ucs, c;
- int n;
struct dynstr d[1];
struct sink *s;
@@ -234,7 +233,6 @@ char *cgi_sgmlquote(const char *src) {
exit(1);
dynstr_init(d);
s = sink_dynstr(d);
- n = 1;
/* format the string */
while((c = *ucs++)) {
switch(c) {
diff --git a/lib/client.c b/lib/client.c
index 50f7184..2cbcfa7 100644
--- a/lib/client.c
+++ b/lib/client.c
@@ -519,7 +519,7 @@ int disorder_close(disorder_client *c) {
c->ident = 0;
xfree(c->user);
c->user = 0;
- return 0;
+ return ret;
}
/** @brief Play a track
diff --git a/lib/configuration.c b/lib/configuration.c
index c059bf7..53fddd5 100644
--- a/lib/configuration.c
+++ b/lib/configuration.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder.
- * Copyright (C) 2004-2009 Richard Kettlewell
+ * Copyright (C) 2004-2010 Richard Kettlewell
* Portions copyright (C) 2007 Mark Wooding
*
* This program is free software: you can redistribute it and/or modify
@@ -52,7 +52,7 @@
/** @brief Path to config file
*
- * set_configfile() sets the deafult if it is null.
+ * set_configfile() sets the default if it is null.
*/
char *configfile;
@@ -835,9 +835,7 @@ static int validate_positive(const struct config_state *cs,
static int validate_isauser(const struct config_state *cs,
int attribute((unused)) nvec,
char **vec) {
- struct passwd *pw;
-
- if(!(pw = getpwnam(vec[0]))) {
+ if(!getpwnam(vec[0])) {
disorder_error(0, "%s:%d: no such user as '%s'", cs->path, cs->line, vec[0]);
return -1;
}
@@ -1069,8 +1067,8 @@ static const struct conf conf[] = {
{ C(checkpoint_min), &type_integer, validate_non_negative },
{ C(collection), &type_collections, validate_any },
{ C(connect), &type_netaddress, validate_destaddr },
- { C(cookie_login_lifetime), &type_integer, validate_positive },
{ C(cookie_key_lifetime), &type_integer, validate_positive },
+ { C(cookie_login_lifetime), &type_integer, validate_positive },
{ C(dbversion), &type_integer, validate_positive },
{ C(default_rights), &type_rights, validate_any },
{ C(device), &type_string, validate_any },
@@ -1081,6 +1079,7 @@ static const struct conf conf[] = {
{ C(lock), &type_boolean, validate_any },
{ C(mail_sender), &type_string, validate_any },
{ C(mixer), &type_string, validate_any },
+ { C(mount_rescan), &type_boolean, validate_any },
{ C(multicast_loop), &type_boolean, validate_any },
{ C(multicast_ttl), &type_integer, validate_non_negative },
{ C(namepart), &type_namepart, validate_any },
@@ -1100,10 +1099,11 @@ static const struct conf conf[] = {
{ C(plugins), &type_string_accum, validate_isdir },
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
- { C(replay_min), &type_integer, validate_non_negative },
{ C(refresh), &type_integer, validate_positive },
+ { C(refresh_min), &type_integer, validate_non_negative },
{ C(reminder_interval), &type_integer, validate_positive },
{ C(remote_userman), &type_boolean, validate_any },
+ { C(replay_min), &type_integer, validate_non_negative },
{ C2(restrict, restrictions), &type_restrict, validate_any },
{ C(rtp_delay_threshold), &type_integer, validate_positive },
{ C(sample_format), &type_sample_format, validate_sample_format },
@@ -1340,6 +1340,7 @@ static struct config *config_default(void) {
logname = pw->pw_name;
c->username = xstrdup(logname);
c->refresh = 15;
+ c->refresh_min = 1;
c->prefsync = 0;
c->signal = SIGKILL;
c->alias = xstrdup("{/artist}{/album}{/title}{ext}");
@@ -1374,6 +1375,7 @@ static struct config *config_default(void) {
c->sox_generation = DEFAULT_SOX_GENERATION;
c->playlist_max = INT_MAX; /* effectively no limit */
c->playlist_lock_timeout = 10; /* 10s */
+ c->mount_rescan = 1;
/* Default stopwords */
if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
exit(1);
@@ -1732,6 +1734,19 @@ static int namepartlist_compare(const struct namepartlist *a,
return 0;
}
+/** @brief Verify configuration table.
+ * @return The number of problems found
+*/
+int config_verify(void) {
+ int fails = 0;
+ for(size_t n = 1; n < sizeof conf / sizeof *conf; ++n)
+ if(strcmp(conf[n-1].name, conf[n].name) >= 0) {
+ fprintf(stderr, "%s >= %s\n", conf[n-1].name, conf[n].name);
+ ++fails;
+ }
+ return fails;
+}
+
/*
Local Variables:
c-basic-offset:2
diff --git a/lib/configuration.h b/lib/configuration.h
index f1794aa..8255db2 100644
--- a/lib/configuration.h
+++ b/lib/configuration.h
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder.
- * Copyright (C) 2004-2009 Richard Kettlewell
+ * Copyright (C) 2004-2010 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -215,6 +215,9 @@ struct config {
/** @brief Maximum refresh interval for web interface (seconds) */
long refresh;
+ /** @brief Minimum refresh interval for web interface (seconds) */
+ long refresh_min;
+
/** @brief Facilities restricted to trusted users
*
* A bitmap of @ref RESTRICT_SCRATCH, @ref RESTRICT_REMOVE and @ref
@@ -287,7 +290,10 @@ struct config {
/** @brief Maximum bias */
long new_bias;
-
+
+ /** @brief Rescan on (un)mount */
+ int mount_rescan;
+
/* derived values: */
int nparts; /* number of distinct name parts */
char **parts; /* name part list */
@@ -320,6 +326,8 @@ char *config_usersysconf(const struct passwd *pw );
char *config_private(void);
/* get the private config file */
+int config_verify(void);
+
void config_free(struct config *c);
extern char *configfile;
diff --git a/lib/defs.c b/lib/defs.c
index 633d56a..f0e35b2 100644
--- a/lib/defs.c
+++ b/lib/defs.c
@@ -41,6 +41,9 @@ const char pkgstatedir[] = PKGSTATEDIR;
/** @brief Package fixed data directory */
const char pkgdatadir[] = PKGDATADIR;
+/** @brief Package HTML documentation directory */
+const char dochtmldir[] = DOCHTMLDIR;
+
/** @brief Binary directory */
const char bindir[] = BINDIR;
diff --git a/lib/defs.h b/lib/defs.h
index 41b65ea..e13b857 100644
--- a/lib/defs.h
+++ b/lib/defs.h
@@ -25,6 +25,7 @@ extern const char pkglibdir[];
extern const char pkgconfdir[];
extern const char pkgstatedir[];
extern const char pkgdatadir[];
+extern const char dochtmldir[];
extern const char bindir[];
extern const char sbindir[];
extern const char finkbindir[];
diff --git a/lib/hreader.c b/lib/hreader.c
new file mode 100644
index 0000000..bdf3c07
--- /dev/null
+++ b/lib/hreader.c
@@ -0,0 +1,114 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file lib/hreader.c
+ * @brief Hands-off reader - read files without keeping them open
+ */
+#include
+#include "hreader.h"
+#include "mem.h"
+#include
+#include
+#include
+#include
+#include
+
+static int hreader_fill(struct hreader *h, off_t offset);
+
+int hreader_init(const char *path, struct hreader *h) {
+ struct stat sb;
+ if(stat(path, &sb) < 0)
+ return -1;
+ memset(h, 0, sizeof *h);
+ h->path = xstrdup(path);
+ h->size = sb.st_size;
+ h->bufsize = 65536;
+ h->buffer = xmalloc_noptr(h->bufsize);
+ return 0;
+}
+
+void hreader_close(struct hreader *h) {
+ xfree(h->path);
+ xfree(h->buffer);
+}
+
+int hreader_read(struct hreader *h, void *buffer, size_t n) {
+ int r = hreader_pread(h, buffer, n, h->read_offset);
+ if(r > 0)
+ h->read_offset += r;
+ return r;
+}
+
+int hreader_pread(struct hreader *h, void *buffer, size_t n, off_t offset) {
+ size_t bytes_read = 0;
+
+ while(bytes_read < n) {
+ // If the desired byte range is outside the file, fetch new contents
+ if(offset < h->buf_offset || offset >= h->buf_offset + (off_t)h->bytes) {
+ int r = hreader_fill(h, offset);
+ if(r < 0)
+ return -1; /* disaster! */
+ else if(r == 0)
+ break; /* end of file */
+ }
+ // Figure out how much we can read this time round
+ size_t left = h->bytes - (offset - h->buf_offset);
+ // Truncate the read if we don't want that much
+ if(left > (n - bytes_read))
+ left = n - bytes_read;
+ memcpy((char *)buffer + bytes_read,
+ h->buffer + (offset - h->buf_offset),
+ left);
+ offset += left;
+ bytes_read += left;
+ }
+ return bytes_read;
+}
+
+static int hreader_fill(struct hreader *h, off_t offset) {
+ int fd = open(h->path, O_RDONLY);
+ if(fd < 0)
+ return -1;
+ int n = pread(fd, h->buffer, h->bufsize, offset);
+ close(fd);
+ if(n < 0)
+ return -1;
+ h->buf_offset = offset;
+ h->bytes = n;
+ return n;
+}
+
+off_t hreader_seek(struct hreader *h, off_t offset, int whence) {
+ switch(whence) {
+ case SEEK_SET: break;
+ case SEEK_CUR: offset += h->read_offset; break;
+ case SEEK_END: offset += h->size; break;
+ default: einval: errno = EINVAL; return -1;
+ }
+ if(offset < 0) goto einval;
+ h->read_offset = offset;
+ return offset;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/hreader.h b/lib/hreader.h
new file mode 100644
index 0000000..90431c1
--- /dev/null
+++ b/lib/hreader.h
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file lib/hreader.h
+ * @brief Hands-off reader - read files without keeping them open
+ */
+#ifndef HREADER_H
+#define HREADER_H
+
+#include
+
+/** @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:
+*/
diff --git a/lib/mime.c b/lib/mime.c
index 9cc54d6..48354b3 100644
--- a/lib/mime.c
+++ b/lib/mime.c
@@ -110,7 +110,7 @@ static const char *skipwhite(const char *s, int rfc822_comments) {
int c, depth;
for(;;) {
- switch(c = *s) {
+ switch(*s) {
case ' ':
case '\t':
case '\r':
@@ -402,7 +402,7 @@ int mime_multipart(const char *s,
* @param s Start of field
* @param dispositionp Where to store disposition
* @param parameternamep Where to store parameter name
- * @param parametervaluep Wher to store parameter value
+ * @param parametervaluep Where to store parameter value
* @return 0 on success, non-0 on error
*
* See RFC 2388 s3
diff --git a/lib/queue.h b/lib/queue.h
index c7dbfd2..2219763 100644
--- a/lib/queue.h
+++ b/lib/queue.h
@@ -188,6 +188,13 @@ struct queue_entry {
/** @brief How much of track has been played so far (seconds) */
long sofar;
+ /** @brief True if track preparation is underway
+ *
+ * This is set when a decoder has been started and is expected to connect to
+ * the speaker, but the speaker has not sent as @ref SM_ARRIVED message back
+ * yet. */
+ int preparing;
+
/** @brief True if decoder is connected to speaker
*
* This is not a @ref playing_state for a couple of reasons
diff --git a/lib/resample.c b/lib/resample.c
index c4c814e..39514ed 100644
--- a/lib/resample.c
+++ b/lib/resample.c
@@ -264,6 +264,7 @@ size_t resample_convert(const struct resampler *rs,
if(rs->state) {
/* A sample-rate conversion must be performed */
SRC_DATA data;
+ memset(&data, 0, sizeof data);
/* Compute how many frames are expected to come out. */
size_t maxframesout = nframesin * rs->output_rate / rs->input_rate + 1;
output = xcalloc(maxframesout * rs->output_channels, sizeof(float));
@@ -273,11 +274,16 @@ size_t resample_convert(const struct resampler *rs,
data.output_frames = maxframesout;
data.end_of_input = eof;
data.src_ratio = (double)rs->output_rate / rs->input_rate;
+ D(("nframesin=%zu maxframesout=%zu eof=%d ratio=%d.%06d",
+ nframesin, maxframesout, eof,
+ (int)data.src_ratio,
+ ((int)(data.src_ratio * 1000000) % 1000000)));
int error_ = src_process(rs->state, &data);
if(error_)
disorder_fatal(0, "calling src_process: %s", src_strerror(error_));
nframesin = data.input_frames_used;
nsamplesout = data.output_frames_gen * rs->output_channels;
+ D(("new nframesin=%zu nsamplesout=%zu", nframesin, nsamplesout));
}
#endif
if(!output) {
diff --git a/lib/sendmail.c b/lib/sendmail.c
index 524f68d..0c5f4db 100644
--- a/lib/sendmail.c
+++ b/lib/sendmail.c
@@ -118,7 +118,7 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
const char *encoding,
const char *content_type,
const char *body) {
- int rc, sol = 1;
+ int sol = 1;
const char *ptr;
uint8_t idbuf[20];
char *id;
@@ -131,23 +131,23 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
strftime(date, sizeof date, "%a, %d %b %Y %H:%M:%S +0000", &ut);
gcry_create_nonce(idbuf, sizeof idbuf);
id = mime_to_base64(idbuf, sizeof idbuf);
- if((rc = getresponse(tag, in)) / 100 != 2)
+ if(getresponse(tag, in) / 100 != 2)
return -1;
if(sendcommand(tag, out, "HELO %s", local_hostname()))
return -1;
- if((rc = getresponse(tag, in)) / 100 != 2)
+ if(getresponse(tag, in) / 100 != 2)
return -1;
if(sendcommand(tag, out, "MAIL FROM:<%s>", sender))
return -1;
- if((rc = getresponse(tag, in)) / 100 != 2)
+ if(getresponse(tag, in) / 100 != 2)
return -1;
if(sendcommand(tag, out, "RCPT TO:<%s>", recipient))
return -1;
- if((rc = getresponse(tag, in)) / 100 != 2)
+ if(getresponse(tag, in) / 100 != 2)
return -1;
if(sendcommand(tag, out, "DATA", sender))
return -1;
- if((rc = getresponse(tag, in)) / 100 != 3)
+ if(getresponse(tag, in) / 100 != 3)
return -1;
if(fprintf(out, "From: %s\r\n", pubsender) < 0
|| fprintf(out, "To: %s\r\n", recipient) < 0
@@ -181,7 +181,7 @@ static int sendmailfp(const char *tag, FILE *in, FILE *out,
if(fprintf(out, ".\r\n") < 0
|| fflush(out) < 0)
goto write_error;
- if((rc = getresponse(tag, in)) / 100 != 2)
+ if(getresponse(tag, in) / 100 != 2)
return -1;
return 0;
}
diff --git a/lib/speaker-protocol.h b/lib/speaker-protocol.h
index 3b21f0a..24fc970 100644
--- a/lib/speaker-protocol.h
+++ b/lib/speaker-protocol.h
@@ -43,6 +43,7 @@ struct speaker_message {
* - @ref SM_FINISHED
* - @ref SM_PLAYING
* - @ref SM_UNKNOWN
+ * - @ref SM_ARRIVED
*/
int type;
@@ -102,6 +103,9 @@ struct speaker_message {
/** @brief Cancelled track @c id which wasn't playing */
#define SM_STILLBORN 133
+/** @brief A connection for track @c id arrived */
+#define SM_ARRIVED 134
+
void speaker_send(int fd, const struct speaker_message *sm);
/* Send a message. */
diff --git a/lib/trackdb-int.h b/lib/trackdb-int.h
index b21b5d9..3be725e 100644
--- a/lib/trackdb-int.h
+++ b/lib/trackdb-int.h
@@ -153,7 +153,6 @@ int trackdb_get_global_tid(const char *name,
char **parsetags(const char *s);
int tag_intersection(char **a, char **b);
-int valid_username(const char *user);
#endif /* TRACKDB_INT_H */
diff --git a/lib/trackdb-playlists.c b/lib/trackdb-playlists.c
index fd33393..27d3319 100644
--- a/lib/trackdb-playlists.c
+++ b/lib/trackdb-playlists.c
@@ -33,6 +33,7 @@
#include "configuration.h"
#include "vector.h"
#include "eventlog.h"
+#include "validity.h"
static int trackdb_playlist_get_tid(const char *name,
const char *who,
@@ -54,43 +55,6 @@ static int trackdb_playlist_delete_tid(const char *name,
const char *who,
DB_TXN *tid);
-/** @brief Parse a playlist name
- * @param name Playlist name
- * @param ownerp Where to put owner, or NULL
- * @param sharep Where to put default sharing, or NULL
- * @return 0 on success, -1 on error
- *
- * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part
- * is alphanumeric and nonempty. USER is a username (see valid_username()).
- */
-int playlist_parse_name(const char *name,
- char **ownerp,
- char **sharep) {
- const char *dot = strchr(name, '.'), *share;
- char *owner;
-
- if(dot) {
- /* Owned playlist */
- owner = xstrndup(name, dot - name);
- if(!valid_username(owner))
- return -1;
- if(!valid_username(dot + 1))
- return -1;
- share = "private";
- } else {
- /* Shared playlist */
- if(!valid_username(name))
- return -1;
- owner = 0;
- share = "shared";
- }
- if(ownerp)
- *ownerp = owner;
- if(sharep)
- *sharep = xstrdup(share);
- return 0;
-}
-
/** @brief Check read access rights
* @param name Playlist name
* @param who Who wants to read
diff --git a/lib/trackdb.c b/lib/trackdb.c
index d2ebe07..da5685a 100644
--- a/lib/trackdb.c
+++ b/lib/trackdb.c
@@ -59,6 +59,7 @@
#include "unidata.h"
#include "base64.h"
#include "sendmail.h"
+#include "validity.h"
#define RESCAN "disorder-rescan"
#define DEADLOCK "disorder-deadlock"
@@ -839,7 +840,7 @@ static char **dedupe(char **vec, int nvec) {
int m, n;
qsort(vec, nvec, sizeof (char *), wordcmp);
- m = n = 0;
+ m = 0;
if(nvec) {
vec[m++] = vec[0];
for(n = 1; n < nvec; ++n)
@@ -1822,7 +1823,7 @@ int trackdb_set(const char *track,
if(trackdb_putdata(trackdb_prefsdb, track, p, tid, 0))
goto fail;
/* compute the new alias name */
- if((err = compute_alias(&newalias, track, p, tid))) goto fail;
+ if(compute_alias(&newalias, track, p, tid)) goto fail;
/* check whether alias has changed */
if(!(oldalias == newalias
|| (oldalias && newalias && !strcmp(oldalias, newalias)))) {
@@ -2175,13 +2176,13 @@ const char *trackdb_getpart(const char *track,
DB_TXN *tid;
char *pref;
const char *actual;
- int used_db, err;
+ int used_db;
/* construct the full pref */
byte_xasprintf(&pref, "trackname_%s_%s", context, part);
for(;;) {
tid = trackdb_begin_transaction();
- if((err = gettrackdata(track, 0, &p, &actual, 0, tid)) == DB_LOCK_DEADLOCK)
+ if(gettrackdata(track, 0, &p, &actual, 0, tid) == DB_LOCK_DEADLOCK)
goto fail;
break;
fail:
@@ -2492,6 +2493,9 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
}
if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
cursor = 0;
+ if(err)
+ goto fail;
+ cursor = 0;
/* do a naive search over that (hopefuly fairly small) list of tracks */
u.nvec = 0;
for(n = 0; n < v.nvec; ++n) {
@@ -2737,12 +2741,11 @@ void trackdb_set_global(const char *name,
const char *value,
const char *who) {
DB_TXN *tid;
- int err;
int state;
for(;;) {
tid = trackdb_begin_transaction();
- if(!(err = trackdb_set_global_tid(name, value, tid)))
+ if(!trackdb_set_global_tid(name, value, tid))
break;
trackdb_abort_transaction(tid);
}
@@ -2799,12 +2802,11 @@ int trackdb_set_global_tid(const char *name,
*/
const char *trackdb_get_global(const char *name) {
DB_TXN *tid;
- int err;
const char *r;
for(;;) {
tid = trackdb_begin_transaction();
- if(!(err = trackdb_get_global_tid(name, tid, &r)))
+ if(!trackdb_get_global_tid(name, tid, &r))
break;
trackdb_abort_transaction(tid);
}
@@ -2908,7 +2910,7 @@ static char **trackdb_new_tid(int *ntracksp,
default:
disorder_fatal(0, "error reading noticed.db: %s", db_strerror(err));
}
- if((err = trackdb_closecursor(c)))
+ if(trackdb_closecursor(c))
return 0; /* deadlock */
vector_terminate(tracks);
if(ntracksp)
@@ -3013,32 +3015,6 @@ static int trusted(const char *user) {
return n < config->trust.n;
}
-/** @brief Return non-zero for a valid username
- * @param user Candidate username
- * @return Nonzero if it's valid
- *
- * Currently we only allow the letters and digits in ASCII. We could be more
- * liberal than this but it is a nice simple test. It is critical that
- * semicolons are never allowed.
- *
- * NB also used by playlist_parse_name() to validate playlist names!
- */
-int valid_username(const char *user) {
- if(!*user)
- return 0;
- while(*user) {
- const uint8_t c = *user++;
- /* For now we are very strict */
- if((c >= 'a' && c <= 'z')
- || (c >= 'A' && c <= 'Z')
- || (c >= '0' && c <= '9'))
- /* ok */;
- else
- return 0;
- }
- return 1;
-}
-
/** @brief Add a user
* @param user Username
* @param password Initial password or NULL
diff --git a/lib/trackdb.h b/lib/trackdb.h
index 901a74a..1d7c8e9 100644
--- a/lib/trackdb.h
+++ b/lib/trackdb.h
@@ -184,9 +184,6 @@ void trackdb_add_rescanned(void (*rescanned)(void *ru),
void *ru);
int trackdb_rescan_underway(void);
-int playlist_parse_name(const char *name,
- char **ownerp,
- char **sharep);
int trackdb_playlist_get(const char *name,
const char *who,
char ***tracksp,
diff --git a/lib/trackname.c b/lib/trackname.c
index 4e2e06e..aa11e2e 100644
--- a/lib/trackname.c
+++ b/lib/trackname.c
@@ -51,6 +51,10 @@ const char *find_track_root(const char *track) {
const struct collection *c = find_track_collection(track);
if(c)
return c->root;
+ /* Suppress this message for scratches */
+ for(int n = 0; n < config->scratch.n; ++n)
+ if(!strcmp(track, config->scratch.s[n]))
+ return 0;
disorder_error(0, "found track in no collection '%s'", track);
return 0;
}
diff --git a/lib/trackname.h b/lib/trackname.h
index 63b881b..56f933e 100644
--- a/lib/trackname.h
+++ b/lib/trackname.h
@@ -51,7 +51,14 @@ int compare_path_raw(const unsigned char *ap, size_t an,
/* Comparison function for path names that groups all entries in a directory
* together */
-/* Convenient wrapper for compare_path_raw */
+/** @brief Compare two paths
+ * @param ap First path
+ * @param bp Second path
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ * A wrapper around compare_path_raw().
+ */
static inline int compare_path(const char *ap, const char *bp) {
return compare_path_raw((const unsigned char *)ap, strlen(ap),
(const unsigned char *)bp, strlen(bp));
diff --git a/lib/trackorder.c b/lib/trackorder.c
index d0ae448..94d2bd5 100644
--- a/lib/trackorder.c
+++ b/lib/trackorder.c
@@ -27,6 +27,22 @@
#include "log.h"
#include "unicode.h"
+/** @brief Compare two tracks
+ * @param sa First sort key
+ * @param sb Second sort key
+ * @param da First display string
+ * @param db Second display string
+ * @param ta First raw track
+ * @param tb Second raw track
+ * @return -ve, 0 or +ve for a <, = or > b
+ *
+ * Tries the following comparisons until a difference is found:
+ * - case-independent comparison of sort keys
+ * - case-dependent comparison of sort keys
+ * - case-independent comparison of display strings
+ * - case-dependent comparison of display strings
+ * - case-dependent comparison of paths (see compare_path())
+ */
int compare_tracks(const char *sa, const char *sb,
const char *da, const char *db,
const char *ta, const char *tb) {
@@ -43,6 +59,17 @@ int compare_tracks(const char *sa, const char *sb,
return compare_path(ta, tb);
}
+/** @brief Compare two paths
+ * @param ap First path
+ * @param an Length of @p ap
+ * @param bp Second path
+ * @param bn Length @p bp
+ * @return -ve, 0 or +ve for ap <, = or > bp
+ *
+ * Sorts files within a directory together.
+ *
+ * See also compare_path().
+ */
int compare_path_raw(const unsigned char *ap, size_t an,
const unsigned char *bp, size_t bn) {
/* Don't change this function! The database sort order depends on it */
diff --git a/lib/tracksort.c b/lib/tracksort.c
index 2f81739..b1fcc3a 100644
--- a/lib/tracksort.c
+++ b/lib/tracksort.c
@@ -32,6 +32,16 @@ static int tracksort_compare(const void *a, const void *b) {
ea->track, eb->track);
}
+/** @brief Sort tracks
+ * @param ntracks Number of tracks to sort
+ * @param tracks List of tracks
+ * @param type Comparison type
+ * @return Sorted track data
+ *
+ * Tracks are compared using compare_tracks(), with the sort key and display
+ * string set according to @p type, which should be "track" if the tracks are
+ * really tracks and "dir" if they are directories.
+ */
struct tracksort_data *tracksort_init(int ntracks,
char **tracks,
const char *type) {
diff --git a/lib/validity.c b/lib/validity.c
new file mode 100644
index 0000000..408c4d5
--- /dev/null
+++ b/lib/validity.c
@@ -0,0 +1,98 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2009 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/validity.c
+ * @brief Various validity checks
+ */
+#include "common.h"
+#include "validity.h"
+
+#include "mem.h"
+
+/** @brief Parse a playlist name
+ * @param name Playlist name
+ * @param ownerp Where to put owner, or NULL
+ * @param sharep Where to put default sharing, or NULL
+ * @return 0 on success, -1 on error
+ *
+ * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part
+ * is alphanumeric and nonempty. USER is a username (see valid_username()).
+ */
+int playlist_parse_name(const char *name,
+ char **ownerp,
+ char **sharep) {
+ const char *dot = strchr(name, '.'), *share;
+ char *owner;
+
+ if(dot) {
+ /* Owned playlist */
+ owner = xstrndup(name, dot - name);
+ if(!valid_username(owner))
+ return -1;
+ if(!valid_username(dot + 1))
+ return -1;
+ share = "private";
+ } else {
+ /* Shared playlist */
+ if(!valid_username(name))
+ return -1;
+ owner = 0;
+ share = "shared";
+ }
+ if(ownerp)
+ *ownerp = owner;
+ if(sharep)
+ *sharep = xstrdup(share);
+ return 0;
+}
+
+/** @brief Return non-zero for a valid username
+ * @param user Candidate username
+ * @return Nonzero if it's valid
+ *
+ * Currently we only allow the letters and digits in ASCII. We could be more
+ * liberal than this but it is a nice simple test. It is critical that
+ * semicolons are never allowed.
+ *
+ * NB also used by playlist_parse_name() to validate playlist names!
+ */
+int valid_username(const char *user) {
+ if(!*user)
+ return 0;
+ while(*user) {
+ const uint8_t c = *user++;
+ /* For now we are very strict */
+ if((c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || (c >= '0' && c <= '9'))
+ /* ok */;
+ else
+ return 0;
+ }
+ return 1;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/validity.h b/lib/validity.h
new file mode 100644
index 0000000..8ce2505
--- /dev/null
+++ b/lib/validity.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2009 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/validity.c
+ * @brief Various validity checks
+ */
+#ifndef VALIDITY_H
+#define VALIDITY_H
+
+#include "common.h"
+#include "validity.h"
+
+int playlist_parse_name(const char *name,
+ char **ownerp,
+ char **sharep);
+int valid_username(const char *user);
+
+#endif /* VALIDITY_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/wav.c b/lib/wav.c
index 455bb72..fe7ffbb 100644
--- a/lib/wav.c
+++ b/lib/wav.c
@@ -128,9 +128,8 @@ int wav_init(struct wavfile *f, const char *path) {
off_t where;
memset(f, 0, sizeof *f);
- f->fd = -1;
f->data = -1;
- if((f->fd = open(path, O_RDONLY)) < 0) goto error_errno;
+ if(hreader_init(path, f->input)) goto error_errno;
/* Read the file header
*
* offset size meaning
@@ -138,7 +137,7 @@ int wav_init(struct wavfile *f, const char *path) {
* 04 4 length of rest of file
* 08 4 'WAVE'
* */
- if((n = pread(f->fd, header, 12, 0)) < 0) goto error_errno;
+ if((n = hreader_pread(f->input, header, 12, 0)) < 0) goto error_errno;
else if(n < 12) goto einval;
if(strncmp(header, "RIFF", 4) || strncmp(header + 8, "WAVE", 4))
goto einval;
@@ -151,7 +150,7 @@ int wav_init(struct wavfile *f, const char *path) {
* 00 4 chunk ID
* 04 4 length of rest of chunk
*/
- if((n = pread(f->fd, header, 8, where)) < 0) goto error_errno;
+ if((n = hreader_pread(f->input, header, 8, where)) < 0) goto error_errno;
else if(n < 8) goto einval;
if(!strncmp(header,"fmt ", 4)) {
/* This is the format chunk
@@ -168,7 +167,8 @@ int wav_init(struct wavfile *f, const char *path) {
* 18 ? extra undocumented rubbish
*/
if(get32(header + 4) < 16) goto einval;
- if((n = pread(f->fd, header + 8, 16, where + 8)) < 0) goto error_errno;
+ if((n = hreader_pread(f->input, header + 8, 16, where + 8)) < 0)
+ goto error_errno;
else if(n < 16) goto einval;
f->channels = get16(header + 0x0A);
f->rate = get32(header + 0x0C);
@@ -197,13 +197,7 @@ error:
/** @brief Close a WAV file */
void wav_destroy(struct wavfile *f) {
- if(f) {
- const int save_errno = errno;
-
- if(f->fd >= 0)
- close(f->fd);
- errno = save_errno;
- }
+ hreader_close(f->input);
}
/** @brief Visit all the data in a WAV file
@@ -227,7 +221,7 @@ int wav_data(struct wavfile *f,
size_t want = (off_t)sizeof buffer > left ? (size_t)left : sizeof buffer;
want -= want % bytes_per_frame;
- if((n = pread(f->fd, buffer, want, where)) < 0) return errno;
+ if((n = hreader_pread(f->input, buffer, want, where)) < 0) return errno;
if((size_t)n < want) return EINVAL;
if((err = callback(f, buffer, n, u))) return err;
where += n;
diff --git a/lib/wav.h b/lib/wav.h
index d2de912..3c9c477 100644
--- a/lib/wav.h
+++ b/lib/wav.h
@@ -22,10 +22,12 @@
#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;
diff --git a/libtests/Makefile.am b/libtests/Makefile.am
index 7be6c51..de3562b 100644
--- a/libtests/Makefile.am
+++ b/libtests/Makefile.am
@@ -20,7 +20,8 @@ TESTS=t-addr t-arcfour t-basen t-bits t-cache t-casefold t-charset \
t-cookies t-dateparse t-event t-filepart t-hash t-heap t-hex \
t-kvp t-mime t-printf t-regsub t-selection t-signame t-sink \
t-split t-syscalls t-trackname t-unicode t-url t-utf8 t-vector \
- t-words t-wstat t-macros t-cgi t-eventdist t-resample
+ t-words t-wstat t-macros t-cgi t-eventdist t-resample \
+ t-configuration
noinst_PROGRAMS=$(TESTS)
@@ -63,6 +64,8 @@ t_wstat_SOURCES=t-wstat.c test.c test.h
t_eventdist_SOURCES=t-eventdist.c test.c test.h
t_resample_SOURCES=t-resample.c test.c test.h
t_resample_LDFLAGS=$(LIBSAMPLERATE)
+t_configuration_SOURCES=t-configuration.c test.c test.h
+t_configuration_LDFLAGS=$(LIBGCRYPT)
check-report: before-check check make-coverage-reports
before-check:
diff --git a/libtests/t-configuration.c b/libtests/t-configuration.c
new file mode 100644
index 0000000..4ff7b18
--- /dev/null
+++ b/libtests/t-configuration.c
@@ -0,0 +1,33 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#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:
+*/
diff --git a/libtests/t-hash.c b/libtests/t-hash.c
index 4d8b68a..e23ee1a 100644
--- a/libtests/t-hash.c
+++ b/libtests/t-hash.c
@@ -50,8 +50,10 @@ static void test_hash(void) {
for(i = 0; i < 10000; ++i) {
insist((ip = hash_find(h, do_printf("%d", i))) != 0);
- check_integer(*ip, i);
- insist(hash_add(h, do_printf("%d", i), &i, HASH_REPLACE) == 0);
+ if(ip) {
+ check_integer(*ip, i);
+ insist(hash_add(h, do_printf("%d", i), &i, HASH_REPLACE) == 0);
+ }
}
check_integer(hash_count(h), 10000);
keys = hash_keys(h);
diff --git a/libtests/t-hex.c b/libtests/t-hex.c
index a6f1b32..f2d5994 100644
--- a/libtests/t-hex.c
+++ b/libtests/t-hex.c
@@ -52,12 +52,15 @@ static void test_hex(void) {
check_string(hex(h, sizeof h), "00ff807f");
check_string(hex(0, 0), "");
u = unhex("00ff807f", &ul);
+ insist(u != 0);
insist(ul == 4);
insist(memcmp(u, h, 4) == 0);
u = unhex("00FF807F", &ul);
+ insist(u != 0);
insist(ul == 4);
insist(memcmp(u, h, 4) == 0);
u = unhex("", &ul);
+ insist(u != 0);
insist(ul == 0);
fprintf(stderr, "2 ERROR reports expected {\n");
insist(unhex("F", 0) == 0);
diff --git a/libtests/test.h b/libtests/test.h
index cada395..87f0224 100644
--- a/libtests/test.h
+++ b/libtests/test.h
@@ -95,7 +95,7 @@ extern int skipped;
const char *got = GOT; \
const char *want = WANT; \
\
- if(want == 0) { \
+ if(got == 0) { \
fprintf(stderr, "%s:%d: %s returned 0\n", \
__FILE__, __LINE__, #GOT); \
count_error(); \
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 7852152..76d7923 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -23,7 +23,10 @@ AM_CPPFLAGS=-I${top_srcdir}/lib
notify_la_SOURCES=notify.c
notify_la_LDFLAGS=-module
-disorder_tracklength_la_SOURCES=tracklength.c mad.c madshim.h ../lib/wav.h ../lib/wav.c
+disorder_tracklength_la_SOURCES=tracklength.c tracklength.h \
+tracklength-mp3.c tracklength-ogg.c tracklength-wav.c \
+tracklength-flac.c mad.c madshim.h ../lib/wav.h ../lib/wav.c \
+../lib/hreader.h ../lib/hreader.c
disorder_tracklength_la_LDFLAGS=-module
disorder_tracklength_la_LIBADD=$(LIBVORBISFILE) $(LIBMAD) $(LIBFLAC) -lm
diff --git a/plugins/exec.c b/plugins/exec.c
index f6ed8b3..e2e712c 100644
--- a/plugins/exec.c
+++ b/plugins/exec.c
@@ -39,7 +39,6 @@ void disorder_play_track(const char *const *parameters,
const char **vec;
vec = disorder_malloc((nparameters + 2) * sizeof (char *));
- i = 0;
j = 0;
for(i = 0; i < nparameters; ++i)
vec[j++] = parameters[i];
diff --git a/plugins/tracklength-flac.c b/plugins/tracklength-flac.c
new file mode 100644
index 0000000..a838966
--- /dev/null
+++ b/plugins/tracklength-flac.c
@@ -0,0 +1,97 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file plugins/tracklength-flac.c
+ * @brief Compute track lengths for FLAC files
+ */
+#include "tracklength.h"
+#include
+
+/* libFLAC's "simplified" interface is rather heavyweight... */
+
+struct flac_state {
+ long duration;
+ const char *path;
+};
+
+static void flac_metadata(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ const FLAC__StreamMetadata *metadata,
+ void *client_data) {
+ struct flac_state *const state = client_data;
+ const FLAC__StreamMetadata_StreamInfo *const stream_info
+ = &metadata->data.stream_info;
+
+ if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
+ /* FLAC uses 0 to mean unknown and conveniently so do we */
+ state->duration = (stream_info->total_samples
+ + stream_info->sample_rate - 1)
+ / stream_info->sample_rate;
+}
+
+static void flac_error(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__StreamDecoderErrorStatus status,
+ void *client_data) {
+ const struct flac_state *const state = client_data;
+
+ disorder_error(0, "error decoding %s: %s", state->path,
+ FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+static FLAC__StreamDecoderWriteStatus flac_write
+ (const FLAC__StreamDecoder attribute((unused)) *decoder,
+ const FLAC__Frame attribute((unused)) *frame,
+ const FLAC__int32 attribute((unused)) *const buffer_[],
+ void attribute((unused)) *client_data) {
+ const struct flac_state *const state = client_data;
+
+ if(state->duration >= 0)
+ return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
+ else
+ return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+long tl_flac(const char *path) {
+ FLAC__StreamDecoder *sd = 0;
+ FLAC__StreamDecoderInitStatus is;
+ struct flac_state state[1];
+
+ state->duration = -1; /* error */
+ state->path = path;
+ if(!(sd = FLAC__stream_decoder_new())) {
+ disorder_error(0, "FLAC__stream_decoder_new failed");
+ goto fail;
+ }
+ if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
+ flac_error, state))) {
+ disorder_error(0, "FLAC__stream_decoder_init_file %s: %s",
+ path, FLAC__StreamDecoderInitStatusString[is]);
+ goto fail;
+ }
+ FLAC__stream_decoder_process_until_end_of_metadata(sd);
+fail:
+ if(sd)
+ FLAC__stream_decoder_delete(sd);
+ return state->duration;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-mp3.c b/plugins/tracklength-mp3.c
new file mode 100644
index 0000000..ecd1c1e
--- /dev/null
+++ b/plugins/tracklength-mp3.c
@@ -0,0 +1,71 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file plugins/tracklength-mp3.c
+ * @brief Compute track lengths for MP3 files
+ */
+#include "tracklength.h"
+#include
+#include "madshim.h"
+
+static void *mmap_file(const char *path, size_t *lengthp) {
+ int fd;
+ void *base;
+ struct stat sb;
+
+ if((fd = open(path, O_RDONLY)) < 0) {
+ disorder_error(errno, "error opening %s", path);
+ return 0;
+ }
+ if(fstat(fd, &sb) < 0) {
+ disorder_error(errno, "error calling stat on %s", path);
+ goto error;
+ }
+ if(sb.st_size == 0) /* can't map 0-length files */
+ goto error;
+ if((base = mmap(0, sb.st_size, PROT_READ,
+ MAP_SHARED, fd, 0)) == (void *)-1) {
+ disorder_error(errno, "error calling mmap on %s", path);
+ goto error;
+ }
+ *lengthp = sb.st_size;
+ close(fd);
+ return base;
+error:
+ close(fd);
+ return 0;
+}
+
+long tl_mp3(const char *path) {
+ size_t length;
+ void *base;
+ buffer b;
+
+ if(!(base = mmap_file(path, &length))) return -1;
+ b.duration = mad_timer_zero;
+ scan_mp3(base, length, &b);
+ munmap(base, length);
+ return b.duration.seconds + !!b.duration.fraction;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-ogg.c b/plugins/tracklength-ogg.c
new file mode 100644
index 0000000..c5c90c6
--- /dev/null
+++ b/plugins/tracklength-ogg.c
@@ -0,0 +1,47 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file plugins/tracklength-ogg.c
+ * @brief Compute track lengths for OGG files
+ */
+#include "tracklength.h"
+#include
+
+long tl_ogg(const char *path) {
+ OggVorbis_File vf;
+ FILE *fp = 0;
+ double length;
+
+ if(!path) goto error;
+ if(!(fp = fopen(path, "rb"))) goto error;
+ if(ov_open(fp, &vf, 0, 0)) goto error;
+ fp = 0;
+ length = ov_time_total(&vf, -1);
+ ov_clear(&vf);
+ return ceil(length);
+error:
+ if(fp) fclose(fp);
+ return -1;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/
diff --git a/plugins/tracklength-wav.c b/plugins/tracklength-wav.c
new file mode 100644
index 0000000..bf501c5
--- /dev/null
+++ b/plugins/tracklength-wav.c
@@ -0,0 +1,49 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @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:
+*/
diff --git a/plugins/tracklength.c b/plugins/tracklength.c
index 86a1fc8..24d22b6 100644
--- a/plugins/tracklength.c
+++ b/plugins/tracklength.c
@@ -20,210 +20,7 @@
*
* Currently implements MP3, OGG, FLAC and WAV.
*/
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-/* libFLAC has had an API change and stupidly taken away the old API */
-#if HAVE_FLAC_FILE_DECODER_H
-# include
-#else
-# include
-#define FLAC__FileDecoder FLAC__StreamDecoder
-#define FLAC__FileDecoderState FLAC__StreamDecoderState
-#endif
-
-
-#include
-
-#include "madshim.h"
-#include "wav.h"
-
-static void *mmap_file(const char *path, size_t *lengthp) {
- int fd;
- void *base;
- struct stat sb;
-
- if((fd = open(path, O_RDONLY)) < 0) {
- disorder_error(errno, "error opening %s", path);
- return 0;
- }
- if(fstat(fd, &sb) < 0) {
- disorder_error(errno, "error calling stat on %s", path);
- goto error;
- }
- if(sb.st_size == 0) /* can't map 0-length files */
- goto error;
- if((base = mmap(0, sb.st_size, PROT_READ,
- MAP_SHARED, fd, 0)) == (void *)-1) {
- disorder_error(errno, "error calling mmap on %s", path);
- goto error;
- }
- *lengthp = sb.st_size;
- close(fd);
- return base;
-error:
- close(fd);
- return 0;
-}
-
-static long tl_mp3(const char *path) {
- size_t length;
- void *base;
- buffer b;
-
- if(!(base = mmap_file(path, &length))) return -1;
- b.duration = mad_timer_zero;
- scan_mp3(base, length, &b);
- munmap(base, length);
- return b.duration.seconds + !!b.duration.fraction;
-}
-
-static long tl_ogg(const char *path) {
- OggVorbis_File vf;
- FILE *fp = 0;
- double length;
-
- if(!path) goto error;
- if(!(fp = fopen(path, "rb"))) goto error;
- if(ov_open(fp, &vf, 0, 0)) goto error;
- fp = 0;
- length = ov_time_total(&vf, -1);
- ov_clear(&vf);
- return ceil(length);
-error:
- if(fp) fclose(fp);
- return -1;
-}
-
-static long tl_wav(const char *path) {
- struct wavfile f[1];
- int err, sample_frame_size;
- long duration;
-
- if((err = wav_init(f, path))) {
- disorder_error(err, "error opening %s", path);
- return -1;
- }
- sample_frame_size = (f->bits + 7) / 8 * f->channels;
- if(sample_frame_size) {
- const long long n_samples = f->datasize / sample_frame_size;
- duration = (n_samples + f->rate - 1) / f->rate;
- } else
- duration = -1;
- wav_destroy(f);
- return duration;
-}
-
-/* libFLAC's "simplified" interface is rather heavyweight... */
-
-struct flac_state {
- long duration;
- const char *path;
-};
-
-static void flac_metadata(const FLAC__FileDecoder attribute((unused)) *decoder,
- const FLAC__StreamMetadata *metadata,
- void *client_data) {
- struct flac_state *const state = client_data;
- const FLAC__StreamMetadata_StreamInfo *const stream_info
- = &metadata->data.stream_info;
-
- if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
- /* FLAC uses 0 to mean unknown and conveniently so do we */
- state->duration = (stream_info->total_samples
- + stream_info->sample_rate - 1)
- / stream_info->sample_rate;
-}
-
-static void flac_error(const FLAC__FileDecoder attribute((unused)) *decoder,
- FLAC__StreamDecoderErrorStatus status,
- void *client_data) {
- const struct flac_state *const state = client_data;
-
- disorder_error(0, "error decoding %s: %s", state->path,
- FLAC__StreamDecoderErrorStatusString[status]);
-}
-
-static FLAC__StreamDecoderWriteStatus flac_write
- (const FLAC__FileDecoder attribute((unused)) *decoder,
- const FLAC__Frame attribute((unused)) *frame,
- const FLAC__int32 attribute((unused)) *const buffer_[],
- void attribute((unused)) *client_data) {
- const struct flac_state *const state = client_data;
-
- if(state->duration >= 0)
- return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
- else
- return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
-}
-
-static long tl_flac(const char *path) {
- struct flac_state state[1];
-
- state->duration = -1; /* error */
- state->path = path;
-#if HAVE_FLAC_FILE_DECODER_H
- {
- FLAC__FileDecoder *fd = 0;
- FLAC__FileDecoderState fs;
-
- if(!(fd = FLAC__file_decoder_new())) {
- disorder_error(0, "FLAC__file_decoder_new failed");
- goto fail;
- }
- if(!(FLAC__file_decoder_set_filename(fd, path))) {
- disorder_error(0, "FLAC__file_set_filename failed");
- goto fail;
- }
- FLAC__file_decoder_set_metadata_callback(fd, flac_metadata);
- FLAC__file_decoder_set_error_callback(fd, flac_error);
- FLAC__file_decoder_set_write_callback(fd, flac_write);
- FLAC__file_decoder_set_client_data(fd, state);
- if((fs = FLAC__file_decoder_init(fd))) {
- disorder_error(0, "FLAC__file_decoder_init: %s",
- FLAC__FileDecoderStateString[fs]);
- goto fail;
- }
- FLAC__file_decoder_process_until_end_of_metadata(fd);
-fail:
- if(fd)
- FLAC__file_decoder_delete(fd);
- }
-#else
- {
- FLAC__StreamDecoder *sd = 0;
- FLAC__StreamDecoderInitStatus is;
-
- if(!(sd = FLAC__stream_decoder_new())) {
- disorder_error(0, "FLAC__stream_decoder_new failed");
- goto fail;
- }
- if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
- flac_error, state))) {
- disorder_error(0, "FLAC__stream_decoder_init_file %s: %s",
- path, FLAC__StreamDecoderInitStatusString[is]);
- goto fail;
- }
- FLAC__stream_decoder_process_until_end_of_metadata(sd);
-fail:
- if(sd)
- FLAC__stream_decoder_delete(sd);
- }
-#endif
- return state->duration;
-}
+#include "tracklength.h"
static const struct {
const char *ext;
diff --git a/plugins/tracklength.h b/plugins/tracklength.h
new file mode 100644
index 0000000..c8e22a4
--- /dev/null
+++ b/plugins/tracklength.h
@@ -0,0 +1,50 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef TRACKLENGTH_H
+#define TRACKLENGTH_H
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+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:
+*/
diff --git a/scripts/htmlman b/scripts/htmlman
index 21452dc..5290d1b 100755
--- a/scripts/htmlman
+++ b/scripts/htmlman
@@ -1,7 +1,7 @@
#! /bin/sh
#
# This file is part of DisOrder
-# Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007, 2008, 2010 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,52 +20,67 @@
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 ""
-echo " "
-if $stdhead; then
- echo "@quiethead@#"
-fi
-echo " $title"
-echo " "
-echo " "
-if $stdhead; then
- echo "@stdmenu{}@#"
-fi
-printf "
"
-# 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!\(.\)\1!\1!g;
- s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
- s!_\(.\)!\1!g;
- s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
- s!\([bi]\)><\1>!!g'
-echo "
"
-if $stdhead; then
- echo "@credits"
-fi
-echo " "
-echo ""
+for page; do
+ title=$(basename $page)
+ output=$(basename $page).$extension
+ echo "$page -> $output" >&2
+ exec > $output.new
+ echo ""
+ echo " "
+ if $stdhead; then
+ echo "@quiethead@#"
+ fi
+ echo " $title"
+ echo " "
+ echo " "
+ if $stdhead; then
+ echo "@stdmenu{}@#"
+ fi
+ printf "
"
+ # 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!\(.\)\1!\1!g;
+ s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
+ s!_\(.\)!\1!g;
+ s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
+ s!\([bi]\)><\1>!!g'
+ echo "
"
+ if $stdhead; then
+ echo "@credits"
+ fi
+ echo " "
+ echo ""
+ mv $output.new $output
+done
diff --git a/scripts/sedfiles.make b/scripts/sedfiles.make
index 5a28fac..a02e753 100644
--- a/scripts/sedfiles.make
+++ b/scripts/sedfiles.make
@@ -23,6 +23,7 @@ $(SEDFILES) : % : %.in Makefile
-e 's!pkgconfdir!${sysconfdir}/disorder!g;' \
-e 's!pkgstatedir!${localstatedir}/disorder!g;' \
-e 's!pkgdatadir!${pkgdatadir}!g;' \
+ -e 's!dochtmldir!${dochtmldir}!g;' \
-e 's!SENDMAIL!${SENDMAIL}!g;' \
-e 's!_version_!${VERSION}!g;' \
< $< > $@.new
diff --git a/server/Makefile.am b/server/Makefile.am
index e73e090..27c8fdd 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -1,6 +1,6 @@
#
# This file is part of DisOrder.
-# Copyright (C) 2004-2009 Richard Kettlewell
+# Copyright (C) 2004-2010 Richard Kettlewell
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -25,7 +25,7 @@ AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
disorderd_SOURCES=disorderd.c api.c api-server.c daemonize.c play.c \
server.c server-queue.c queue-ops.c state.c plugin.c \
- schedule.c dbparams.c background.c \
+ schedule.c dbparams.c background.c mount.c \
exports.c ../lib/memgc.c disorder-server.h
disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
$(LIBPCRE) $(LIBDB) $(LIBAO) $(LIBGC) $(LIBGCRYPT) $(LIBICONV) \
@@ -44,7 +44,8 @@ disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
$(LIBPTHREAD)
disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
-disorder_decode_SOURCES=decode.c disorder-server.h
+disorder_decode_SOURCES=decode.c decode.h disorder-server.h \
+decode-mp3.c decode-ogg.c decode-wav.c decode-flac.c
disorder_decode_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
$(LIBMAD) $(LIBVORBISFILE) $(LIBFLAC)
disorder_decode_DEPENDENCIES=../lib/libdisorder.a
@@ -121,7 +122,7 @@ check-decode: check-wav check-flac check-mp3
check-mp3: disorder-decode disorder-normalize
./disorder-decode ${top_srcdir}/sounds/scratch.mp3 | \
- ./disorder-normalize --config test-config > mp3ed.raw
+ ./disorder-normalize --config ${srcdir}/test-config > mp3ed.raw
cmp mp3ed.raw ${top_srcdir}/sounds/scratch-mp3.raw
rm -f mp3ed.raw
@@ -129,19 +130,19 @@ check-mp3: disorder-decode disorder-normalize
# or something. Makes it tricky to test!
check-ogg: disorder-decode disorder-normalize
./disorder-decode ${top_srcdir}/sounds/scratch.ogg | \
- ./disorder-normalize --config test-config > ogged.raw
+ ./disorder-normalize --config ${srcdir}/test-config > ogged.raw
cmp ogged.raw ${top_srcdir}/sounds/scratch.raw
rm -f ogged.raw
check-wav: disorder-decode disorder-normalize
./disorder-decode ${top_srcdir}/sounds/scratch.wav | \
- ./disorder-normalize --config test-config > waved.raw
+ ./disorder-normalize --config ${srcdir}/test-config > waved.raw
cmp waved.raw ${top_srcdir}/sounds/scratch.raw
rm -rf waved.raw
check-flac: disorder-decode disorder-normalize
./disorder-decode ${top_srcdir}/sounds/scratch.flac | \
- ./disorder-normalize --config test-config > flacced.raw
+ ./disorder-normalize --config ${srcdir}/test-config > flacced.raw
cmp flacced.raw ${top_srcdir}/sounds/scratch.raw
rm -f flacced.raw
diff --git a/server/decode-flac.c b/server/decode-flac.c
new file mode 100644
index 0000000..f1399fb
--- /dev/null
+++ b/server/decode-flac.c
@@ -0,0 +1,156 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file server/decode.c
+ * @brief General-purpose decoder for use by speaker process
+ */
+#include "decode.h"
+#include
+
+/** @brief Metadata callback for FLAC decoder
+ *
+ * This is a no-op here.
+ */
+static void flac_metadata(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ const FLAC__StreamMetadata attribute((unused)) *metadata,
+ void attribute((unused)) *client_data) {
+}
+
+/** @brief Error callback for FLAC decoder */
+static void flac_error(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__StreamDecoderErrorStatus status,
+ void attribute((unused)) *client_data) {
+ disorder_fatal(0, "error decoding %s: %s", path,
+ FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+/** @brief Write callback for FLAC decoder */
+static FLAC__StreamDecoderWriteStatus flac_write
+ (const FLAC__StreamDecoder attribute((unused)) *decoder,
+ const FLAC__Frame *frame,
+ const FLAC__int32 *const buffer[],
+ void attribute((unused)) *client_data) {
+ size_t n, c;
+
+ output_header(frame->header.sample_rate,
+ frame->header.channels,
+ frame->header.bits_per_sample,
+ (frame->header.channels * frame->header.blocksize
+ * frame->header.bits_per_sample) / 8,
+ ENDIAN_BIG);
+ for(n = 0; n < frame->header.blocksize; ++n) {
+ for(c = 0; c < frame->header.channels; ++c) {
+ switch(frame->header.bits_per_sample) {
+ case 8: output_8(buffer[c][n]); break;
+ case 16: output_16(buffer[c][n]); break;
+ case 24: output_24(buffer[c][n]); break;
+ case 32: output_32(buffer[c][n]); break;
+ }
+ }
+ }
+ return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+static FLAC__StreamDecoderReadStatus flac_read(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__byte buffer[],
+ size_t *bytes,
+ void *client_data) {
+ struct hreader *flacinput = client_data;
+ int n = hreader_read(flacinput, buffer, *bytes);
+ if(n == 0) {
+ *bytes = 0;
+ return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
+ }
+ if(n < 0) {
+ *bytes = 0;
+ return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
+ }
+ *bytes = n;
+ return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
+}
+
+static FLAC__StreamDecoderSeekStatus flac_seek(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__uint64 absolute_byte_offset,
+ void *client_data) {
+ struct hreader *flacinput = client_data;
+ if(hreader_seek(flacinput, absolute_byte_offset, SEEK_SET) < 0)
+ return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
+ else
+ return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
+}
+
+static FLAC__StreamDecoderTellStatus flac_tell(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__uint64 *absolute_byte_offset,
+ void *client_data) {
+ struct hreader *flacinput = client_data;
+ off_t offset = hreader_seek(flacinput, 0, SEEK_CUR);
+ if(offset < 0)
+ return FLAC__STREAM_DECODER_TELL_STATUS_ERROR;
+ *absolute_byte_offset = offset;
+ return FLAC__STREAM_DECODER_TELL_STATUS_OK;
+}
+
+static FLAC__StreamDecoderLengthStatus flac_length(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ FLAC__uint64 *stream_length,
+ void *client_data) {
+ struct hreader *flacinput = client_data;
+ *stream_length = hreader_size(flacinput);
+ return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
+}
+
+static FLAC__bool flac_eof(const FLAC__StreamDecoder attribute((unused)) *decoder,
+ void *client_data) {
+ struct hreader *flacinput = client_data;
+ return hreader_eof(flacinput);
+}
+
+/** @brief FLAC file decoder */
+void decode_flac(void) {
+ FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
+ FLAC__StreamDecoderInitStatus is;
+ struct hreader flacinput[1];
+
+ if (!sd)
+ disorder_fatal(0, "FLAC__stream_decoder_new failed");
+ if(hreader_init(path, flacinput))
+ disorder_fatal(errno, "error opening %s", path);
+
+ if((is = FLAC__stream_decoder_init_stream(sd,
+ flac_read,
+ flac_seek,
+ flac_tell,
+ flac_length,
+ flac_eof,
+ flac_write, flac_metadata,
+ flac_error,
+ flacinput)))
+ disorder_fatal(0, "FLAC__stream_decoder_init_stream %s: %s",
+ path, FLAC__StreamDecoderInitStatusString[is]);
+
+ FLAC__stream_decoder_process_until_end_of_stream(sd);
+ FLAC__stream_decoder_finish(sd);
+ FLAC__stream_decoder_delete(sd);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-mp3.c b/server/decode-mp3.c
new file mode 100644
index 0000000..6837beb
--- /dev/null
+++ b/server/decode-mp3.c
@@ -0,0 +1,187 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file server/decode-mp3.c
+ * @brief Decode MP3 files.
+ */
+#include "decode.h"
+#include
+
+static struct hreader input[1];
+
+/** @brief Dithering state
+ * Filched from mpg321, which credits it to Robert Leslie */
+struct audio_dither {
+ mad_fixed_t error[3];
+ mad_fixed_t random;
+};
+
+/** @brief 32-bit PRNG
+ * Filched from mpg321, which credits it to Robert Leslie */
+static inline unsigned long prng(unsigned long state)
+{
+ return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
+}
+
+/** @brief Generic linear sample quantize and dither routine
+ * Filched from mpg321, which credits it to Robert Leslie */
+static long audio_linear_dither(mad_fixed_t sample,
+ struct audio_dither *dither) {
+ unsigned int scalebits;
+ mad_fixed_t output, mask, rnd;
+ const int bits = 16;
+
+ enum {
+ MIN = -MAD_F_ONE,
+ MAX = MAD_F_ONE - 1
+ };
+
+ /* noise shape */
+ sample += dither->error[0] - dither->error[1] + dither->error[2];
+
+ dither->error[2] = dither->error[1];
+ dither->error[1] = dither->error[0] / 2;
+
+ /* bias */
+ output = sample + (1L << (MAD_F_FRACBITS + 1 - bits - 1));
+
+ scalebits = MAD_F_FRACBITS + 1 - bits;
+ mask = (1L << scalebits) - 1;
+
+ /* dither */
+ rnd = prng(dither->random);
+ output += (rnd & mask) - (dither->random & mask);
+
+ dither->random = rnd;
+
+ /* clip */
+ if (output > MAX) {
+ output = MAX;
+
+ if (sample > MAX)
+ sample = MAX;
+ }
+ else if (output < MIN) {
+ output = MIN;
+
+ if (sample < MIN)
+ sample = MIN;
+ }
+
+ /* quantize */
+ output &= ~mask;
+
+ /* error feedback */
+ dither->error[0] = sample - output;
+
+ /* scale */
+ return output >> scalebits;
+}
+
+/** @brief MP3 output callback */
+static enum mad_flow mp3_output(void attribute((unused)) *data,
+ struct mad_header const *header,
+ struct mad_pcm *pcm) {
+ size_t n = pcm->length;
+ const mad_fixed_t *l = pcm->samples[0], *r = pcm->samples[1];
+ static struct audio_dither ld[1], rd[1];
+
+ output_header(header->samplerate,
+ pcm->channels,
+ 16,
+ 2 * pcm->channels * pcm->length,
+ ENDIAN_BIG);
+ switch(pcm->channels) {
+ case 1:
+ while(n--)
+ output_16(audio_linear_dither(*l++, ld));
+ break;
+ case 2:
+ while(n--) {
+ output_16(audio_linear_dither(*l++, ld));
+ output_16(audio_linear_dither(*r++, rd));
+ }
+ break;
+ }
+ return MAD_FLOW_CONTINUE;
+}
+
+/** @brief MP3 input callback */
+static enum mad_flow mp3_input(void attribute((unused)) *data,
+ struct mad_stream *stream) {
+ int used, remain, n;
+
+ /* libmad requires its caller to do ALL the buffering work, including coping
+ * with partial frames. Given that it appears to be completely undocumented
+ * you could perhaps be forgiven for not discovering this... */
+ if(input_count) {
+ /* Compute total number of bytes consumed */
+ used = (char *)stream->next_frame - input_buffer;
+ /* Compute number of bytes left to consume */
+ remain = input_count - used;
+ memmove(input_buffer, input_buffer + used, remain);
+ } else {
+ remain = 0;
+ }
+ /* Read new data */
+ n = hreader_read(input,
+ input_buffer + remain,
+ (sizeof input_buffer) - remain);
+ if(n < 0)
+ disorder_fatal(errno, "reading from %s", path);
+ /* Compute total number of bytes available */
+ input_count = remain + n;
+ if(input_count)
+ mad_stream_buffer(stream, (unsigned char *)input_buffer, input_count);
+ if(n)
+ return MAD_FLOW_CONTINUE;
+ else
+ return MAD_FLOW_STOP;
+}
+
+/** @brief MP3 error callback */
+static enum mad_flow mp3_error(void attribute((unused)) *data,
+ struct mad_stream *stream,
+ struct mad_frame attribute((unused)) *frame) {
+ if(0)
+ /* Just generates pointless verbosity l-( */
+ disorder_error(0, "decoding %s: %s (%#04x)",
+ path, mad_stream_errorstr(stream), stream->error);
+ return MAD_FLOW_CONTINUE;
+}
+
+/** @brief MP3 decoder */
+void decode_mp3(void) {
+ struct mad_decoder mad[1];
+
+ if(hreader_init(path, input))
+ disorder_fatal(errno, "opening %s", path);
+ mad_decoder_init(mad, 0/*data*/, mp3_input, 0/*header*/, 0/*filter*/,
+ mp3_output, mp3_error, 0/*message*/);
+ if(mad_decoder_run(mad, MAD_DECODER_MODE_SYNC))
+ exit(1);
+ mad_decoder_finish(mad);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-ogg.c b/server/decode-ogg.c
new file mode 100644
index 0000000..d499955
--- /dev/null
+++ b/server/decode-ogg.c
@@ -0,0 +1,92 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file server/decode.c
+ * @brief General-purpose decoder for use by speaker process
+ */
+#include "decode.h"
+
+#include
+
+static size_t ogg_read_func(void *ptr, size_t size, size_t nmemb, void *datasource) {
+ struct hreader *h = datasource;
+
+ int n = hreader_read(h, ptr, size * nmemb);
+ if(n < 0) n = 0;
+ return n / size;
+}
+
+static int ogg_seek_func(void *datasource, ogg_int64_t offset, int whence) {
+ struct hreader *h = datasource;
+
+ return hreader_seek(h, offset, whence) < 0 ? -1 : 0;
+}
+
+static int ogg_close_func(void attribute((unused)) *datasource) {
+ return 0;
+}
+
+static long ogg_tell_func(void *datasource) {
+ struct hreader *h = datasource;
+
+ return hreader_seek(h, 0, SEEK_CUR);
+}
+
+static const ov_callbacks ogg_callbacks = {
+ ogg_read_func,
+ ogg_seek_func,
+ ogg_close_func,
+ ogg_tell_func,
+};
+
+/** @brief OGG decoder */
+void decode_ogg(void) {
+ struct hreader ogginput[1];
+ OggVorbis_File vf[1];
+ int err;
+ long n;
+ int bitstream;
+ vorbis_info *vi;
+
+ hreader_init(path, ogginput);
+ /* There doesn't seem to be any standard function for mapping the error codes
+ * to strings l-( */
+ if((err = ov_open_callbacks(ogginput, vf, 0/*initial*/, 0/*ibytes*/,
+ ogg_callbacks)))
+ disorder_fatal(0, "ov_open_callbacks %s: %d", path, err);
+ if(!(vi = ov_info(vf, 0/*link*/)))
+ disorder_fatal(0, "ov_info %s: failed", path);
+ while((n = ov_read(vf, input_buffer, sizeof input_buffer, 1/*bigendianp*/,
+ 2/*bytes/word*/, 1/*signed*/, &bitstream))) {
+ if(n < 0)
+ disorder_fatal(0, "ov_read %s: %ld", path, n);
+ if(bitstream > 0)
+ disorder_fatal(0, "only single-bitstream ogg files are supported");
+ output_header(vi->rate, vi->channels, 16/*bits*/, n, ENDIAN_BIG);
+ if(fwrite(input_buffer, 1, n, outputfp) < (size_t)n)
+ disorder_fatal(errno, "decoding %s: writing sample data", path);
+ }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/decode-wav.c b/server/decode-wav.c
new file mode 100644
index 0000000..fd58a14
--- /dev/null
+++ b/server/decode-wav.c
@@ -0,0 +1,53 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @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:
+*/
diff --git a/server/decode.c b/server/decode.c
index 11a0592..8a09013 100644
--- a/server/decode.c
+++ b/server/decode.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2007-2009 Richard Kettlewell
+ * Copyright (C) 2007-2010 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,23 +18,14 @@
/** @file server/decode.c
* @brief General-purpose decoder for use by speaker process
*/
-
-#include "disorder-server.h"
+#include "decode.h"
#include
#include
-/* libFLAC has had an API change and stupidly taken away the old API */
-#if HAVE_FLAC_FILE_DECODER_H
-# include
-#else
-# include
-#define FLAC__FileDecoder FLAC__StreamDecoder
-#define FLAC__FileDecoderState FLAC__StreamDecoderState
-#endif
+#include
#include "wav.h"
-#include "speaker-protocol.h"
/** @brief Encoding lookup table type */
@@ -45,50 +36,10 @@ struct decoder {
void (*decode)(void);
};
-/** @brief Input file */
-static int inputfd;
-
-/** @brief Output file */
-static FILE *outputfp;
-
-/** @brief Filename */
-static const char *path;
-
-/** @brief Input buffer */
-static char input_buffer[1048576];
-
-/** @brief Number of bytes read into buffer */
-static int input_count;
-
-/** @brief Write an 8-bit word */
-static inline void output_8(int n) {
- if(putc(n, outputfp) < 0)
- disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 16-bit word in bigendian format */
-static inline void output_16(uint16_t n) {
- if(putc(n >> 8, outputfp) < 0
- || putc(n, outputfp) < 0)
- disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 24-bit word in bigendian format */
-static inline void output_24(uint32_t n) {
- if(putc(n >> 16, outputfp) < 0
- || putc(n >> 8, outputfp) < 0
- || putc(n, outputfp) < 0)
- disorder_fatal(errno, "decoding %s: output error", path);
-}
-
-/** @brief Write a 32-bit word in bigendian format */
-static inline void output_32(uint32_t n) {
- if(putc(n >> 24, outputfp) < 0
- || putc(n >> 16, outputfp) < 0
- || putc(n >> 8, outputfp) < 0
- || putc(n, outputfp) < 0)
- disorder_fatal(errno, "decoding %s: output error", path);
-}
+FILE *outputfp;
+const char *path;
+char input_buffer[INPUT_BUFFER_SIZE];
+int input_count;
/** @brief Write a block header
* @param rate Sample rate in Hz
@@ -100,11 +51,11 @@ static inline void output_32(uint32_t n) {
* Checks that the sample format is a supported one (so other calls do not have
* to) and calls disorder_fatal() on error.
*/
-static void output_header(int rate,
- int channels,
- int bits,
- int nbytes,
- int endian) {
+void output_header(int rate,
+ int channels,
+ int bits,
+ int nbytes,
+ int endian) {
struct stream_header header;
if(bits <= 0 || bits % 8 || bits > 64)
@@ -124,288 +75,6 @@ static void output_header(int rate,
disorder_fatal(errno, "decoding %s: writing format header", path);
}
-/** @brief Dithering state
- * Filched from mpg321, which credits it to Robert Leslie */
-struct audio_dither {
- mad_fixed_t error[3];
- mad_fixed_t random;
-};
-
-/** @brief 32-bit PRNG
- * Filched from mpg321, which credits it to Robert Leslie */
-static inline unsigned long prng(unsigned long state)
-{
- return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
-}
-
-/** @brief Generic linear sample quantize and dither routine
- * Filched from mpg321, which credits it to Robert Leslie */
-static long audio_linear_dither(mad_fixed_t sample,
- struct audio_dither *dither) {
- unsigned int scalebits;
- mad_fixed_t output, mask, rnd;
- const int bits = 16;
-
- enum {
- MIN = -MAD_F_ONE,
- MAX = MAD_F_ONE - 1
- };
-
- /* noise shape */
- sample += dither->error[0] - dither->error[1] + dither->error[2];
-
- dither->error[2] = dither->error[1];
- dither->error[1] = dither->error[0] / 2;
-
- /* bias */
- output = sample + (1L << (MAD_F_FRACBITS + 1 - bits - 1));
-
- scalebits = MAD_F_FRACBITS + 1 - bits;
- mask = (1L << scalebits) - 1;
-
- /* dither */
- rnd = prng(dither->random);
- output += (rnd & mask) - (dither->random & mask);
-
- dither->random = rnd;
-
- /* clip */
- if (output > MAX) {
- output = MAX;
-
- if (sample > MAX)
- sample = MAX;
- }
- else if (output < MIN) {
- output = MIN;
-
- if (sample < MIN)
- sample = MIN;
- }
-
- /* quantize */
- output &= ~mask;
-
- /* error feedback */
- dither->error[0] = sample - output;
-
- /* scale */
- return output >> scalebits;
-}
-
-/** @brief MP3 output callback */
-static enum mad_flow mp3_output(void attribute((unused)) *data,
- struct mad_header const *header,
- struct mad_pcm *pcm) {
- size_t n = pcm->length;
- const mad_fixed_t *l = pcm->samples[0], *r = pcm->samples[1];
- static struct audio_dither ld[1], rd[1];
-
- output_header(header->samplerate,
- pcm->channels,
- 16,
- 2 * pcm->channels * pcm->length,
- ENDIAN_BIG);
- switch(pcm->channels) {
- case 1:
- while(n--)
- output_16(audio_linear_dither(*l++, ld));
- break;
- case 2:
- while(n--) {
- output_16(audio_linear_dither(*l++, ld));
- output_16(audio_linear_dither(*r++, rd));
- }
- break;
- }
- return MAD_FLOW_CONTINUE;
-}
-
-/** @brief MP3 input callback */
-static enum mad_flow mp3_input(void attribute((unused)) *data,
- struct mad_stream *stream) {
- int used, remain, n;
-
- /* libmad requires its caller to do ALL the buffering work, including coping
- * with partial frames. Given that it appears to be completely undocumented
- * you could perhaps be forgiven for not discovering this... */
- if(input_count) {
- /* Compute total number of bytes consumed */
- used = (char *)stream->next_frame - input_buffer;
- /* Compute number of bytes left to consume */
- remain = input_count - used;
- memmove(input_buffer, input_buffer + used, remain);
- } else {
- remain = 0;
- }
- /* Read new data */
- n = read(inputfd, input_buffer + remain, (sizeof input_buffer) - remain);
- if(n < 0)
- disorder_fatal(errno, "reading from %s", path);
- /* Compute total number of bytes available */
- input_count = remain + n;
- if(input_count)
- mad_stream_buffer(stream, (unsigned char *)input_buffer, input_count);
- if(n)
- return MAD_FLOW_CONTINUE;
- else
- return MAD_FLOW_STOP;
-}
-
-/** @brief MP3 error callback */
-static enum mad_flow mp3_error(void attribute((unused)) *data,
- struct mad_stream *stream,
- struct mad_frame attribute((unused)) *frame) {
- if(0)
- /* Just generates pointless verbosity l-( */
- disorder_error(0, "decoding %s: %s (%#04x)",
- path, mad_stream_errorstr(stream), stream->error);
- return MAD_FLOW_CONTINUE;
-}
-
-/** @brief MP3 decoder */
-static void decode_mp3(void) {
- struct mad_decoder mad[1];
-
- if((inputfd = open(path, O_RDONLY)) < 0)
- disorder_fatal(errno, "opening %s", path);
- mad_decoder_init(mad, 0/*data*/, mp3_input, 0/*header*/, 0/*filter*/,
- mp3_output, mp3_error, 0/*message*/);
- if(mad_decoder_run(mad, MAD_DECODER_MODE_SYNC))
- exit(1);
- mad_decoder_finish(mad);
-}
-
-/** @brief OGG decoder */
-static void decode_ogg(void) {
- FILE *fp;
- OggVorbis_File vf[1];
- int err;
- long n;
- int bitstream;
- vorbis_info *vi;
-
- if(!(fp = fopen(path, "rb")))
- disorder_fatal(errno, "cannot open %s", path);
- /* There doesn't seem to be any standard function for mapping the error codes
- * to strings l-( */
- if((err = ov_open(fp, vf, 0/*initial*/, 0/*ibytes*/)))
- disorder_fatal(0, "ov_fopen %s: %d", path, err);
- if(!(vi = ov_info(vf, 0/*link*/)))
- disorder_fatal(0, "ov_info %s: failed", path);
- while((n = ov_read(vf, input_buffer, sizeof input_buffer, 1/*bigendianp*/,
- 2/*bytes/word*/, 1/*signed*/, &bitstream))) {
- if(n < 0)
- disorder_fatal(0, "ov_read %s: %ld", path, n);
- if(bitstream > 0)
- disorder_fatal(0, "only single-bitstream ogg files are supported");
- output_header(vi->rate, vi->channels, 16/*bits*/, n, ENDIAN_BIG);
- if(fwrite(input_buffer, 1, n, outputfp) < (size_t)n)
- disorder_fatal(errno, "decoding %s: writing sample data", path);
- }
-}
-
-/** @brief Sample data callback used by decode_wav() */
-static int wav_write(struct wavfile attribute((unused)) *f,
- const char *data,
- size_t nbytes,
- void attribute((unused)) *u) {
- if(fwrite(data, 1, nbytes, outputfp) < nbytes)
- disorder_fatal(errno, "decoding %s: writing sample data", path);
- return 0;
-}
-
-/** @brief WAV file decoder */
-static void decode_wav(void) {
- struct wavfile f[1];
- int err;
-
- if((err = wav_init(f, path)))
- disorder_fatal(err, "opening %s", path);
- output_header(f->rate, f->channels, f->bits, f->datasize, ENDIAN_LITTLE);
- if((err = wav_data(f, wav_write, 0)))
- disorder_fatal(err, "error decoding %s", path);
-}
-
-/** @brief Metadata callback for FLAC decoder
- *
- * This is a no-op here.
- */
-static void flac_metadata(const FLAC__FileDecoder attribute((unused)) *decoder,
- const FLAC__StreamMetadata attribute((unused)) *metadata,
- void attribute((unused)) *client_data) {
-}
-
-/** @brief Error callback for FLAC decoder */
-static void flac_error(const FLAC__FileDecoder attribute((unused)) *decoder,
- FLAC__StreamDecoderErrorStatus status,
- void attribute((unused)) *client_data) {
- disorder_fatal(0, "error decoding %s: %s", path,
- FLAC__StreamDecoderErrorStatusString[status]);
-}
-
-/** @brief Write callback for FLAC decoder */
-static FLAC__StreamDecoderWriteStatus flac_write
- (const FLAC__FileDecoder attribute((unused)) *decoder,
- const FLAC__Frame *frame,
- const FLAC__int32 *const buffer[],
- void attribute((unused)) *client_data) {
- size_t n, c;
-
- output_header(frame->header.sample_rate,
- frame->header.channels,
- frame->header.bits_per_sample,
- (frame->header.channels * frame->header.blocksize
- * frame->header.bits_per_sample) / 8,
- ENDIAN_BIG);
- for(n = 0; n < frame->header.blocksize; ++n) {
- for(c = 0; c < frame->header.channels; ++c) {
- switch(frame->header.bits_per_sample) {
- case 8: output_8(buffer[c][n]); break;
- case 16: output_16(buffer[c][n]); break;
- case 24: output_24(buffer[c][n]); break;
- case 32: output_32(buffer[c][n]); break;
- }
- }
- }
- return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
-}
-
-
-/** @brief FLAC file decoder */
-static void decode_flac(void) {
-#if HAVE_FLAC_FILE_DECODER_H
- FLAC__FileDecoder *fd = 0;
- FLAC__FileDecoderState fs;
-
- if(!(fd = FLAC__file_decoder_new()))
- disorder_fatal(0, "FLAC__file_decoder_new failed");
- if(!(FLAC__file_decoder_set_filename(fd, path)))
- disorder_fatal(0, "FLAC__file_set_filename failed");
- FLAC__file_decoder_set_metadata_callback(fd, flac_metadata);
- FLAC__file_decoder_set_error_callback(fd, flac_error);
- FLAC__file_decoder_set_write_callback(fd, flac_write);
- if((fs = FLAC__file_decoder_init(fd)))
- disorder_fatal(0, "FLAC__file_decoder_init: %s", FLAC__FileDecoderStateString[fs]);
- FLAC__file_decoder_process_until_end_of_file(fd);
-#else
- FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
- FLAC__StreamDecoderInitStatus is;
-
- if (!sd)
- disorder_fatal(0, "FLAC__stream_decoder_new failed");
-
- if((is = FLAC__stream_decoder_init_file(sd, path, flac_write, flac_metadata,
- flac_error, 0)))
- disorder_fatal(0, "FLAC__stream_decoder_init_file %s: %s",
- path, FLAC__StreamDecoderInitStatusString[is]);
-
- FLAC__stream_decoder_process_until_end_of_stream(sd);
- FLAC__stream_decoder_finish(sd);
- FLAC__stream_decoder_delete(sd);
-#endif
-}
-
/** @brief Lookup table of decoders */
static const struct decoder decoders[] = {
{ "*.mp3", decode_mp3 },
diff --git a/server/decode.h b/server/decode.h
new file mode 100644
index 0000000..97bf687
--- /dev/null
+++ b/server/decode.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007-2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @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:
+*/
diff --git a/server/disorder-server.h b/server/disorder-server.h
index f980b6f..7cd1cf6 100644
--- a/server/disorder-server.h
+++ b/server/disorder-server.h
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2008, 2009 Richard Kettlewell
+ * Copyright (C) 2008-2010 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -140,6 +140,7 @@ struct queue_entry *queue_add(const char *track, const char *submitter,
#define WHERE_END 1 /* Add to end of queue */
#define WHERE_BEFORE_RANDOM 2 /* End, or before random track */
#define WHERE_AFTER 3 /* After the target */
+#define WHERE_NOWHERE 4 /* Don't add to queue at all */
/* add an entry to the queue. Return a pointer to the new entry. */
void queue_remove(struct queue_entry *q, const char *who);
@@ -371,6 +372,18 @@ int play_background(ev_source *ev,
#define START_HARDFAIL 1 /**< @brief Track is broken. */
#define START_SOFTFAIL 2 /**< @brief Track OK, system (temporarily?) broken */
+void periodic_mount_check(ev_source *ev_);
+
+#ifndef MOUNT_CHECK_INTERVAL
+# ifdef PATH_MTAB
+// statting a file is really cheap so check once a second
+# define MOUNT_CHECK_INTERVAL 1
+# else
+// hashing getfsstat() output could be more expensive so be less aggressive
+# define MOUNT_CHECK_INTERVAL 5
+# endif
+#endif
+
#endif /* DISORDER_SERVER_H */
/*
diff --git a/server/disorderd.c b/server/disorderd.c
index 14b459f..2000a2f 100644
--- a/server/disorderd.c
+++ b/server/disorderd.c
@@ -302,6 +302,8 @@ int main(int argc, char **argv) {
create_periodic(ev, periodic_play_check, 1, 0);
/* Try adding a random track immediately and once every two seconds */
create_periodic(ev, periodic_add_random, 2, 1);
+ /* Issue a rescan when devices are mounted or unmouted */
+ create_periodic(ev, periodic_mount_check, MOUNT_CHECK_INTERVAL, 1);
/* enter the event loop */
n = ev_run(ev);
/* if we exit the event loop, something must have gone wrong */
diff --git a/server/dump.c b/server/dump.c
index cdc3a44..fb07415 100644
--- a/server/dump.c
+++ b/server/dump.c
@@ -378,8 +378,8 @@ int main(int argc, char **argv) {
case 'd': dump = 1; break;
case 'u': undump = 1; break;
case 'D': debugging = 1; break;
- case 'r': recover = TRACKDB_NORMAL_RECOVER;
- case 'R': recover = TRACKDB_FATAL_RECOVER;
+ case 'r': recover = TRACKDB_NORMAL_RECOVER; break;
+ case 'R': recover = TRACKDB_FATAL_RECOVER; break;
case 'a': recompute = 1; break;
case 'P': remove_pathless = 1; break;
default: disorder_fatal(0, "invalid option");
diff --git a/server/mount.c b/server/mount.c
new file mode 100644
index 0000000..8b1c602
--- /dev/null
+++ b/server/mount.c
@@ -0,0 +1,99 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2010 Richard Kettlewell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+/** @file server/mount.c
+ * @brief Periodically check for devices being mounted and unmounted
+ */
+#include "disorder-server.h"
+#if HAVE_GETFSSTAT
+# include
+# include
+# include
+#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:
+*/
diff --git a/server/normalize.c b/server/normalize.c
index bcfc3a8..0f97a2f 100644
--- a/server/normalize.c
+++ b/server/normalize.c
@@ -188,6 +188,8 @@ int main(int argc, char attribute((unused)) **argv) {
}
if(!n)
break;
+ D(("NEW HEADER: %"PRIu32" bytes %"PRIu32"Hz %"PRIu8" channels %"PRIu8" bits %"PRIu8" endian",
+ header.nbytes, header.rate, header.channels, header.bits, header.endian));
/* Sanity check the header */
if(header.rate < 100 || header.rate > 1000000)
disorder_fatal(0, "implausible rate %"PRId32"Hz (%#"PRIx32")",
@@ -210,7 +212,8 @@ int main(int argc, char attribute((unused)) **argv) {
else {
/* If we have a resampler active already check it is suitable and destroy
* it if not */
- if(!formats_equal(&header, &latest_format) && rs_in_use) {
+ if(rs_in_use) {
+ D(("call resample_close"));
resample_close(rs);
rs_in_use = 0;
}
@@ -227,6 +230,7 @@ int main(int argc, char attribute((unused)) **argv) {
config->sample_format.endian);*/
if(!rs_in_use) {
/* Create a suitable resampler. */
+ D(("call resample_init"));
resample_init(rs,
header.bits,
header.channels,
@@ -260,17 +264,20 @@ int main(int argc, char attribute((unused)) **argv) {
left -= r;
used += r;
//syslog(LOG_INFO, "read %zd bytes", r);
+ D(("read %zd bytes", r));
}
/*syslog(LOG_INFO, " in: %02x %02x %02x %02x",
(uint8_t)buffer[0],
(uint8_t)buffer[1],
(uint8_t)buffer[2],
(uint8_t)buffer[3]);*/
+ D(("calling resample_convert used=%zu !left=%d", used, !left));
const size_t consumed = resample_convert(rs,
(uint8_t *)buffer, used,
!left,
converted, 0);
//syslog(LOG_INFO, "used=%zu consumed=%zu", used, consumed);
+ D(("consumed=%zu", consumed));
memmove(buffer, buffer + consumed, used - consumed);
used -= consumed;
}
diff --git a/server/play.c b/server/play.c
index f93fd5a..8406238 100644
--- a/server/play.c
+++ b/server/play.c
@@ -39,6 +39,7 @@ static int start_child(struct queue_entry *q,
static int prepare_child(struct queue_entry *q,
const struct pbgc_params *params,
void attribute((unused)) *bgdata);
+static void ensure_next_scratch(ev_source *ev);
/** @brief File descriptor of our end of the socket to the speaker */
static int speaker_fd = -1;
@@ -82,14 +83,32 @@ static int speaker_readable(ev_source *ev, int fd,
case SM_FINISHED: /* scratched the playing track */
case SM_STILLBORN: /* scratched too early */
case SM_UNKNOWN: /* scratched WAY too early */
- if(playing && !strcmp(sm.id, playing->id))
+ if(playing && !strcmp(sm.id, playing->id)) {
+ if((playing->state == playing_unplayed
+ || playing->state == playing_started)
+ && sm.type == SM_FINISHED)
+ playing->state = playing_ok;
finished(ev);
+ }
break;
case SM_PLAYING:
/* track ID is playing, DATA seconds played */
D(("SM_PLAYING %s %ld", sm.id, sm.data));
playing->sofar = sm.data;
break;
+ case SM_ARRIVED: {
+ /* track ID is now prepared */
+ struct queue_entry *q;
+ for(q = qhead.next; q != &qhead && strcmp(q->id, sm.id); q = q->next)
+ ;
+ if(q && q->preparing) {
+ q->preparing = 0;
+ q->prepared = 1;
+ /* We might be waiting to play the now-prepared track */
+ play(ev);
+ }
+ break;
+ }
default:
disorder_error(0, "unknown speaker message type %d", sm.type);
}
@@ -195,7 +214,9 @@ static void finished(ev_source *ev) {
* some time before the speaker reports it as finished) or when a non-raw
* (i.e. non-speaker) player terminates. In the latter case it's imaginable
* that the OS has buffered the last few samples.
- *
+ *
+ * NB. The finished track might NOT be in the queue (yet) - it might be a
+ * pre-chosen scratch.
*/
static int player_finished(ev_source *ev,
pid_t pid,
@@ -279,7 +300,7 @@ static int start(ev_source *ev,
D(("start %s", q->id));
/* Find the player plugin. */
- if(!(player = find_player(q)) < 0)
+ if(!(player = find_player(q)))
return START_HARDFAIL; /* No player */
if(!(q->pl = open_plugin(player->s[1], 0)))
return START_HARDFAIL;
@@ -371,19 +392,21 @@ int prepare(ev_source *ev,
if(q->pid >= 0)
return START_OK;
/* If the track is already prepared, do nothing */
- if(q->prepared)
+ if(q->prepared || q->preparing)
return START_OK;
/* Find the player plugin */
- if(!(player = find_player(q)) < 0)
+ if(!(player = find_player(q)))
return START_HARDFAIL; /* No player */
q->pl = open_plugin(player->s[1], 0);
q->type = play_get_type(q->pl);
if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
return START_OK; /* Not a raw player */
- const int rc = play_background(ev, player, q, prepare_child, NULL);
+ int rc = play_background(ev, player, q, prepare_child, NULL);
if(rc == START_OK) {
ev_child(ev, q->pid, 0, player_finished, q);
- q->prepared = 1;
+ q->preparing = 1;
+ /* Actually the track is still "in flight" */
+ rc = START_SOFTFAIL;
}
return rc;
}
@@ -609,6 +632,8 @@ void play(ev_source *ev) {
* potentially be a just-added random track. */
if(qhead.next != &qhead)
prepare(ev, qhead.next);
+ /* Make sure there is a prepared scratch */
+ ensure_next_scratch(ev);
break;
}
}
@@ -656,12 +681,27 @@ void disable_random(const char *who) {
/* Scratching --------------------------------------------------------------- */
+/** @brief Track to play next time something is scratched */
+static struct queue_entry *next_scratch;
+
+/** @brief Ensure there isa prepared scratch */
+static void ensure_next_scratch(ev_source *ev) {
+ if(next_scratch) /* There's one already */
+ return;
+ if(!config->scratch.n) /* There are no scratches */
+ return;
+ int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
+ next_scratch = queue_add(config->scratch.s[r], NULL,
+ WHERE_NOWHERE, NULL, origin_scratch);
+ if(ev)
+ prepare(ev, next_scratch);
+}
+
/** @brief Scratch a track
* @param who User responsible (or NULL)
* @param id Track ID (or NULL for current)
*/
void scratch(const char *who, const char *id) {
- struct queue_entry *q;
struct speaker_message sm;
D(("scratch playing=%p state=%d id=%s playing->id=%s",
@@ -692,12 +732,20 @@ void scratch(const char *who, const char *id) {
speaker_send(speaker_fd, &sm);
D(("sending SM_CANCEL for %s", playing->id));
}
- /* put a scratch track onto the front of the queue (but don't
- * bother if playing is disabled) */
- if(playing_is_enabled() && config->scratch.n) {
- int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
- q = queue_add(config->scratch.s[r], who, WHERE_START, NULL,
- origin_scratch);
+ /* If playing is enabled then add a scratch to the queue. Having a scratch
+ * appear in the queue when further play is disabled is weird and
+ * contradicts implicit assumptions made elsewhere, so we try to avoid
+ * it. */
+ if(playing_is_enabled()) {
+ /* Try to make sure there is a scratch */
+ ensure_next_scratch(NULL);
+ /* Insert it at the head of the queue */
+ if(next_scratch){
+ next_scratch->submitter = who;
+ queue_insert_entry(&qhead, next_scratch);
+ eventlog_raw("queue", queue_marshall(next_scratch), (const char *)0);
+ next_scratch = NULL;
+ }
}
notify_scratch(playing->track, playing->submitter, who,
xtime(0) - playing->played);
diff --git a/server/queue-ops.c b/server/queue-ops.c
index e513d16..7dcaa84 100644
--- a/server/queue-ops.c
+++ b/server/queue-ops.c
@@ -104,6 +104,8 @@ struct queue_entry *queue_add(const char *track, const char *submitter,
}
queue_insert_entry(afterme, q);
break;
+ case WHERE_NOWHERE:
+ return q;
}
/* submitter will be a null pointer for a scratch */
if(submitter)
diff --git a/server/rescan.c b/server/rescan.c
index bc99681..7cb24b6 100644
--- a/server/rescan.c
+++ b/server/rescan.c
@@ -160,7 +160,7 @@ done:
if(fp)
xfclose(fp);
if(pid)
- while((r = waitpid(pid, &w, 0)) == -1 && errno == EINTR)
+ while((waitpid(pid, &w, 0)) == -1 && errno == EINTR)
;
}
diff --git a/server/schedule.c b/server/schedule.c
index f1b20a1..c7b4eed 100644
--- a/server/schedule.c
+++ b/server/schedule.c
@@ -471,14 +471,12 @@ static int schedule_lookup(const char *id,
static int schedule_trigger(ev_source *ev,
const struct timeval attribute((unused)) *now,
void *u) {
- const char *action, *id = u;
+ const char *id = u;
struct kvp *actiondata = schedule_get(id);
int n;
if(!actiondata)
return 0;
- /* schedule_get() enforces these being present */
- action = kvp_get(actiondata, "action");
/* Look up the action */
n = schedule_lookup(id, actiondata);
if(n < 0)
diff --git a/server/server.c b/server/server.c
index 858edbc..4dafabb 100644
--- a/server/server.c
+++ b/server/server.c
@@ -1177,7 +1177,7 @@ static int c_nop(struct conn *c,
static int c_new(struct conn *c,
char **vec,
int nvec) {
- int max, n;
+ int max;
char **tracks;
if(nvec > 0)
@@ -1188,7 +1188,6 @@ static int c_new(struct conn *c,
max = config->new_max;
tracks = trackdb_new(0, max);
sink_printf(ev_writer_sink(c->w), "253 New track list follows\n");
- n = 0;
while(*tracks) {
sink_printf(ev_writer_sink(c->w), "%s%s\n",
**tracks == '.' ? "." : "", *tracks);
diff --git a/server/speaker.c b/server/speaker.c
index de5692b..3af36aa 100644
--- a/server/speaker.c
+++ b/server/speaker.c
@@ -1,6 +1,6 @@
/*
* This file is part of DisOrder
- * Copyright (C) 2005-2009 Richard Kettlewell
+ * Copyright (C) 2005-2010 Richard Kettlewell
* Portions (C) 2007 Mark Wooding
*
* This program is free software: you can redistribute it and/or modify
@@ -316,12 +316,16 @@ static int speaker_fill(struct track *t) {
n = read(t->fd, t->buffer + where, left);
} while(n < 0 && errno == EINTR);
pthread_mutex_lock(&lock);
- if(n < 0) {
- if(errno != EAGAIN)
- disorder_fatal(errno, "error reading sample stream");
+ if(n < 0 && errno == EAGAIN) {
+ /* EAGAIN means more later */
rc = 0;
- } else if(n == 0) {
- D(("fill %s: eof detected", t->id));
+ } else if(n <= 0) {
+ /* n=0 means EOF. n<0 means some error occurred. We log the error but
+ * otherwise treat it as identical to EOF. */
+ if(n < 0)
+ disorder_error(errno, "error reading sample stream for %s", t->id);
+ else
+ D(("fill %s: eof detected", t->id));
t->eof = 1;
/* A track always becomes playable at EOF; we're not going to see any
* more data. */
@@ -336,7 +340,8 @@ static int speaker_fill(struct track *t) {
t->playable = 1;
rc = 0;
}
- }
+ } else
+ rc = 0;
return rc;
}
@@ -521,7 +526,8 @@ static void mainloop(void) {
D(("id %s fd %d", id, fd));
t = findtrack(id, 1/*create*/);
if (write(fd, "", 1) < 0) /* write an ack */
- disorder_error(errno, "writing ack to inbound connection");
+ disorder_error(errno, "writing ack to inbound connection for %s",
+ id);
if(t->fd != -1) {
disorder_error(0, "%s: already got a connection", id);
xclose(fd);
@@ -529,6 +535,10 @@ static void mainloop(void) {
nonblock(fd);
t->fd = fd; /* yay */
}
+ /* Notify the server that the connection arrived */
+ sm.type = SM_ARRIVED;
+ strcpy(sm.id, id);
+ speaker_send(1, &sm);
}
} else
disorder_error(errno, "accept");
diff --git a/sounds/Makefile.am b/sounds/Makefile.am
index a5598b7..eda0767 100644
--- a/sounds/Makefile.am
+++ b/sounds/Makefile.am
@@ -19,4 +19,5 @@
pkgdata_DATA=slap.ogg scratch.ogg
EXTRA_DIST=${pkgdata_DATA} \
- scratch.wav scratch.flac scratch.mp3 scratch.raw long.ogg
+ scratch.wav scratch.flac scratch.mp3 scratch.raw long.ogg \
+ scratch-mp3.raw
diff --git a/tests/dtest.py b/tests/dtest.py
index 235d746..a5a8be9 100644
--- a/tests/dtest.py
+++ b/tests/dtest.py
@@ -85,7 +85,7 @@ Make track with relative path S exist"""
trackdir = os.path.dirname(trackpath)
if not os.path.exists(trackdir):
os.makedirs(trackdir)
- copyfile("%s/sounds/long.ogg" % top_builddir, trackpath)
+ copyfile("%s/sounds/long.ogg" % top_srcdir, trackpath)
# We record the tracks we created so they can be tested against
# server responses. We put them into NFC since that's what the server
# uses internally.
@@ -286,6 +286,7 @@ def stop_daemon():
Stop the daemon if it has not stopped already"""
global daemon
if daemon == None:
+ print " (daemon not running)"
return
rc = daemon.poll()
if rc == None:
@@ -337,7 +338,9 @@ def run(module=None, report=True):
except Exception, e:
traceback.print_exc(None, sys.stderr)
failures += 1
- stop_daemon()
+ finally:
+ stop_daemon()
+ os.system("ps -ef | grep disorderd")
if report:
if failures:
print " FAILED"