From: Richard Kettlewell Date: Sun, 18 Jul 2010 16:08:21 +0000 (+0100) Subject: Merge memory hygeine branch X-Git-Tag: branchpoint-5.1~70 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/cdabf44d4bf72678b402c0fd7dac394eb36513da?hp=ecb2bc3c357e16d5696f98c9bff54a0c13dbd0ef Merge memory hygeine branch --- diff --git a/.bzrignore b/.bzrignore index 0cb8173..150c85c 100644 --- a/.bzrignore +++ b/.bzrignore @@ -107,7 +107,7 @@ doc/disorder-normalize.8.html doc/disorder-decode.8.html doc/disorder-decode.8 doc/plumbing.png -disobedience/images.h +images/images.h debian/disorder-server doc/disorder-stats.8 doc/disorder-stats.8.html @@ -172,6 +172,7 @@ libtests/t-words libtests/t-wstat libtests/t-macros libtests/t-cgi +libtests/t-configuration doc/*.tmpl doc/disorder_templates.5 oc/disorder_templates.5.html @@ -203,3 +204,4 @@ server/endian clients/rtpmon libtests/t-resample clients/resample +disobedience/manual/Makefile diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..57c71b4 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +See the end of README for authorship details. diff --git a/CHANGES.html b/CHANGES.html index 90101b0..53c9fa0 100644 --- a/CHANGES.html +++ b/CHANGES.html @@ -39,11 +39,21 @@ h4 { } table.bugs { - width: 100% + width: 100%; + font-size: 12pt; + border-collapse: collapse; + border:1px } table.bugs th { - text-align: left + text-align: left; + border: 1px solid black; + background-color: black; + color: white +} + +table.bugs td { + border: 1px solid } span.command { @@ -58,7 +68,27 @@ span.command {

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.

+ +

Disobedience has a new + manual.

+

Web Interface

@@ -105,7 +148,7 @@ span.command {

Confirmation URLs should be cleaner (and in particular not end with punctuation). (Please see README.upgrades for more about this.)

- +

RTP Player

@@ -148,12 +191,27 @@ span.command { ID Description - + + + #22 + Background decoders interact badly with server reload + + #27 Mac DisOrder uses wrong sound device + + #30 + mini disobedience interface + + + + #32 + Excessively verbose log chatter on shutdown + + #33 (Some) plugins need -lm. @@ -194,14 +252,39 @@ span.command { disobedience doesn't configure its back end + + #46 + Sort search results in web interface + + #48 build-time dependency on oggdec removed - + + #49 + Disobedience's 'When' column gets out of date + + + + #51 + Improved speaker process robustness + + + + (none) + “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 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Server + + + Web interface + + + RTP player + + Sound card + + + + Local Network + + + Disobedience + + + + 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.

+ +

1. Introduction

+
    +
  • What DisOrder and Disobedience are, and how to get them
  • +
  • How to get a DisOrder login
  • +
  • How to start Disobedience
  • +
+ +

2. Window Layout

+ +
    +
  • A tour of the Disobedience window
  • +
+ +

3. Tabs

+ +
    +
  • Detailed descriptions of + the Queue, Recent, Choose and Added + tabs
  • +
+ +

4. Track Properties

+ +
    +
  • How to edit track properties
  • +
+ +

5. Playlists

+ +
    +
  • What playlists are
  • +
  • Editing playlists
  • +
+ +

Appendix

+ +
    +
  • Network play
  • +
  • Reporting bugs
  • +
  • Copyright notice
  • +
+ + + diff --git a/disobedience/manual/intro.html b/disobedience/manual/intro.html new file mode 100644 index 0000000..3fb0451 --- /dev/null +++ b/disobedience/manual/intro.html @@ -0,0 +1,201 @@ + + + + + Disobedience: Introduction + + + +

1. Introduction

+ +

This chapter covers the following topics:

+ +
    +
  • What DisOrder and Disobedience are, and how to get them
  • +
  • How to get a DisOrder login
  • +
  • How to start Disobedience
  • +
+ +

1.1 What is 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.

+ + + +

1.2 Getting DisOrder

+ +

There are two ways to get DisOrder.

+ +

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).

+ +

1.3 Getting a DisOrder login

+ +

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.

+ +

1.4 Starting Disobedience

+ +

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.)

+ +

1.5 Initial Login

+ +

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 + + + +

Appendix

+ +

Network Play

+ +

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.

+ +

Reporting Bugs

+ +

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.
  • + +
+ +

Copyright Notice

+ +

Copyright © 2003-2009 Richard Kettlewell
+ + Portions copyright © 2007 Ross + Younger
+ + Portions copyright © 2007, 2008 Mark Wooding
+ + Portions extracted from MPG321, Copyright © 2001 Joe + Drew, Copyright © 2000-2001 Robert Leslie
+ + Portions copyright © 1997-2006 Free Software Foundation, Inc
+ + Portions Copyright © 2000 Red Hat, + Inc., Jonathan Blandford

+ +

This program is free software: you can 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.

+ +

5.1 What are playlists?

+ +

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:

+ + + + + + + + + + + + + + + + + +
SharedShared playlists have no owner and can be seen and edited + by anybody.
PublicPublic playlist are owned by their creator and can be seen + by anybody. Only their creator can edit them, however.
PrivatePrivate 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.

+ +

5.2 Creating Playlists

+ +

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.

+ +

+ +

5.3 Editing Playlists

+ +

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.

+ +

+ +

5.3 Playing Playlists

+ +

There are three ways to play a playlist:

+ +
    + +
  1. You can use Control > Activate Playlist from the main + menu.
  2. + +
  3. Right clicking in either half of the playlist editor will + create a pop-up menu. There is an option to play this + playlist.
  4. + +
  5. You can select all the tracks and drag them to the desired + point in the Queue tab.
  6. + +
+ +
+ + Back to contents + + + diff --git a/disobedience/manual/properties.html b/disobedience/manual/properties.html new file mode 100644 index 0000000..34d215f --- /dev/null +++ b/disobedience/manual/properties.html @@ -0,0 +1,97 @@ + + + + + Disobedience: Track Properties + + + +

4. Track Properties

+ +

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.

+ +

4.1 Track Name Parts

+ +

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:

+ +
    + +
  1. In the Choose tab, select all the tracks in the album.
  2. + +
  3. Select Edit > Track Properties to bring up the track + properties window.
  4. + +
  5. Edit the album name in the first track.
  6. + +
  7. Click the arrow button to the right of the corrected version.
  8. + +
  9. Click the OK button.
  10. + +
+ +

4.2 Tags

+ +

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.

+ +

4.3 Track Weight

+ +

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.

+ +

3.1 The Queue

+ +

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
WhenThis 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.
WhoThis 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.
ArtistThe 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.
AlbumThe album that the track came from.
TitleThe title of the track.
LengthThe 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.)

+ +

3.2 Recently Played Tracks

+ +

+ +

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.

+ +

3.3 Choosing Tracks

+ +

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.

+ +

To clear the search, press the Cancel button.

+ +

3.4 Newly Added Tracks

+ +

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.

+ +

2.1 The 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.

+ +

2.2 Buttons

+ +

The meaning of the buttons is as follows:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
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).

+ +

2.3 The Menu Bar

+ +

+ +

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 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + 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!\(.\)\1!\1!g;
-                       s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
-                       s!_\(.\)!\1!g;
-                       s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
-                       s!<\1>!!g'
-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!\(.\)\1!\1!g;
+                         s!\(&[#0-9a-z][0-9a-z]*;\)\1!\1!g;
+                         s!_\(.\)!\1!g;
+                         s!_\(&[#0-9a-z][0-9a-z]*;\)!\1!g;
+                         s!<\1>!!g'
+  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"