libmpd 0.8.0.5 → 0.9.0
raw patch · 61 files changed
+3337/−2750 lines, 61 filesdep −HUnitdep ~attoparsecdep ~basedep ~containers
Dependencies removed: HUnit
Dependency ranges changed: attoparsec, base, containers, data-default, filepath, hspec, network, old-locale, text, utf8-string
Files
- LICENSE +17/−507
- NEWS +0/−99
- Network/MPD.hs +0/−87
- Network/MPD/Commands.hs +0/−523
- Network/MPD/Commands/Arg.hs +0/−73
- Network/MPD/Commands/Extensions.hs +0/−147
- Network/MPD/Commands/Parse.hs +0/−183
- Network/MPD/Commands/Query.hs +0/−56
- Network/MPD/Commands/Types.hs +0/−285
- Network/MPD/Commands/Util.hs +0/−69
- Network/MPD/Core.hs +0/−249
- Network/MPD/Core/Class.hs +0/−39
- Network/MPD/Core/Error.hs +0/−56
- Network/MPD/Util.hs +0/−113
- README.md +29/−69
- changelog.md +118/−0
- libmpd.cabal +63/−32
- src/Network/MPD.hs +92/−0
- src/Network/MPD/Applicative.hs +47/−0
- src/Network/MPD/Applicative/Connection.hs +29/−0
- src/Network/MPD/Applicative/CurrentPlaylist.hs +149/−0
- src/Network/MPD/Applicative/Database.hs +119/−0
- src/Network/MPD/Applicative/Internal.hs +100/−0
- src/Network/MPD/Applicative/Output.hs +36/−0
- src/Network/MPD/Applicative/PlaybackControl.hs +62/−0
- src/Network/MPD/Applicative/PlaybackOptions.hs +83/−0
- src/Network/MPD/Applicative/Reflection.hs +77/−0
- src/Network/MPD/Applicative/Status.hs +133/−0
- src/Network/MPD/Applicative/Stickers.hs +71/−0
- src/Network/MPD/Applicative/StoredPlaylists.hs +93/−0
- src/Network/MPD/Applicative/Util.hs +34/−0
- src/Network/MPD/Commands.hs +68/−0
- src/Network/MPD/Commands/Arg.hs +77/−0
- src/Network/MPD/Commands/Connection.hs +31/−0
- src/Network/MPD/Commands/CurrentPlaylist.hs +128/−0
- src/Network/MPD/Commands/Database.hs +79/−0
- src/Network/MPD/Commands/Extensions.hs +109/−0
- src/Network/MPD/Commands/Output.hs +36/−0
- src/Network/MPD/Commands/Parse.hs +124/−0
- src/Network/MPD/Commands/PlaybackControl.hs +61/−0
- src/Network/MPD/Commands/PlaybackOptions.hs +63/−0
- src/Network/MPD/Commands/Query.hs +60/−0
- src/Network/MPD/Commands/Reflection.hs +50/−0
- src/Network/MPD/Commands/Status.hs +60/−0
- src/Network/MPD/Commands/Stickers.hs +62/−0
- src/Network/MPD/Commands/StoredPlaylists.hs +81/−0
- src/Network/MPD/Commands/Types.hs +337/−0
- src/Network/MPD/Core.hs +253/−0
- src/Network/MPD/Core/Class.hs +36/−0
- src/Network/MPD/Core/Error.hs +66/−0
- src/Network/MPD/Util.hs +113/−0
- tests/Arbitrary.hs +2/−2
- tests/CommandSpec.hs +41/−70
- tests/Defaults.hs +0/−21
- tests/EnvSpec.hs +2/−3
- tests/Main.hs +1/−14
- tests/ParserSpec.hs +3/−5
- tests/StringConn.hs +12/−33
- tests/TestUtil.hs +19/−5
- tests/Unparse.hs +9/−6
- tests/UtilSpec.hs +2/−4
LICENSE view
@@ -1,510 +1,20 @@-- GNU LESSER GENERAL PUBLIC LICENSE- Version 2.1, February 1999-- Copyright (C) 1991, 1999 Free Software Foundation, Inc.- 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA- Everyone is permitted to copy and distribute verbatim copies- of this license document, but changing it is not allowed.--[This is the first released version of the Lesser GPL. It also counts- as the successor of the GNU Library Public License, version 2, hence- the version number 2.1.]-- Preamble-- The licenses for most software are designed to take away your-freedom to share and change it. By contrast, the GNU General Public-Licenses are intended to guarantee your freedom to share and change-free software--to make sure the software is free for all its users.-- This license, the Lesser General Public License, applies to some-specially designated software packages--typically libraries--of the-Free Software Foundation and other authors who decide to use it. You-can use it too, but we suggest you first think carefully about whether-this license or the ordinary General Public License is the better-strategy to use in any particular case, based on the explanations-below.-- When we speak of free software, we are referring to freedom of use,-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 this service if you wish); that you receive source code or can get-it if you want it; that you can change the software and use pieces of-it in new free programs; and that you are informed that you can do-these things.-- To protect your rights, we need to make restrictions that forbid-distributors to deny you these rights or to ask you to surrender these-rights. These restrictions translate to certain responsibilities for-you if you distribute copies of the library or if you modify it.-- For example, if you distribute copies of the library, whether gratis-or for a fee, you must give the recipients all the rights that we gave-you. You must make sure that they, too, receive or can get the source-code. If you link other code with the library, you must provide-complete object files to the recipients, so that they can relink them-with the library after making changes to the library and recompiling-it. And you must show them these terms so they know their rights.-- We protect your rights with a two-step method: (1) we copyright the-library, and (2) we offer you this license, which gives you legal-permission to copy, distribute and/or modify the library.-- To protect each distributor, we want to make it very clear that-there is no warranty for the free library. Also, if the library is-modified by someone else and passed on, the recipients should know-that what they have is not the original version, so that the original-author's reputation will not be affected by problems that might be-introduced by others.-- Finally, software patents pose a constant threat to the existence of-any free program. We wish to make sure that a company cannot-effectively restrict the users of a free program by obtaining a-restrictive license from a patent holder. Therefore, we insist that-any patent license obtained for a version of the library must be-consistent with the full freedom of use specified in this license.-- Most GNU software, including some libraries, is covered by the-ordinary GNU General Public License. This license, the GNU Lesser-General Public License, applies to certain designated libraries, and-is quite different from the ordinary General Public License. We use-this license for certain libraries in order to permit linking those-libraries into non-free programs.-- When a program is linked with a library, whether statically or using-a shared library, the combination of the two is legally speaking a-combined work, a derivative of the original library. The ordinary-General Public License therefore permits such linking only if the-entire combination fits its criteria of freedom. The Lesser General-Public License permits more lax criteria for linking other code with-the library.-- We call this license the "Lesser" General Public License because it-does Less to protect the user's freedom than the ordinary General-Public License. It also provides other free software developers Less-of an advantage over competing non-free programs. These disadvantages-are the reason we use the ordinary General Public License for many-libraries. However, the Lesser license provides advantages in certain-special circumstances.-- For example, on rare occasions, there may be a special need to-encourage the widest possible use of a certain library, so that it-becomes a de-facto standard. To achieve this, non-free programs must-be allowed to use the library. A more frequent case is that a free-library does the same job as widely used non-free libraries. In this-case, there is little to gain by limiting the free library to free-software only, so we use the Lesser General Public License.-- In other cases, permission to use a particular library in non-free-programs enables a greater number of people to use a large body of-free software. For example, permission to use the GNU C Library in-non-free programs enables many more people to use the whole GNU-operating system, as well as its variant, the GNU/Linux operating-system.-- Although the Lesser General Public License is Less protective of the-users' freedom, it does ensure that the user of a program that is-linked with the Library has the freedom and the wherewithal to run-that program using a modified version of the Library.-- The precise terms and conditions for copying, distribution and-modification follow. Pay close attention to the difference between a-"work based on the library" and a "work that uses the library". The-former contains code derived from the library, whereas the latter must-be combined with the library in order to run.-- GNU LESSER GENERAL PUBLIC LICENSE- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION-- 0. This License Agreement applies to any software library or other-program which contains a notice placed by the copyright holder or-other authorized party saying it may be distributed under the terms of-this Lesser General Public License (also called "this License").-Each licensee is addressed as "you".-- A "library" means a collection of software functions and/or data-prepared so as to be conveniently linked with application programs-(which use some of those functions and data) to form executables.-- The "Library", below, refers to any such software library or work-which has been distributed under these terms. A "work based on the-Library" means either the Library or any derivative work under-copyright law: that is to say, a work containing the Library or a-portion of it, either verbatim or with modifications and/or translated-straightforwardly into another language. (Hereinafter, translation is-included without limitation in the term "modification".)-- "Source code" for a work means the preferred form of the work for-making modifications to it. For a library, complete source code means-all the source code for all modules it contains, plus any associated-interface definition files, plus the scripts used to control-compilation and installation of the library.-- Activities other than copying, distribution and modification are not-covered by this License; they are outside its scope. The act of-running a program using the Library is not restricted, and output from-such a program is covered only if its contents constitute a work based-on the Library (independent of the use of the Library in a tool for-writing it). Whether that is true depends on what the Library does-and what the program that uses the Library does.-- 1. You may copy and distribute verbatim copies of the Library's-complete source code as you receive it, in any medium, provided that-you conspicuously and appropriately publish on each copy an-appropriate copyright notice and disclaimer of warranty; keep intact-all the notices that refer to this License and to the absence of any-warranty; and distribute a copy of this License along with the-Library.-- You may charge a fee for the physical act of transferring a copy,-and you may at your option offer warranty protection in exchange for a-fee.-- 2. You may modify your copy or copies of the Library or any portion-of it, thus forming a work based on the Library, and copy and-distribute such modifications or work under the terms of Section 1-above, provided that you also meet all of these conditions:-- a) The modified work must itself be a software library.-- b) You must cause the files modified to carry prominent notices- stating that you changed the files and the date of any change.-- c) You must cause the whole of the work to be licensed at no- charge to all third parties under the terms of this License.-- d) If a facility in the modified Library refers to a function or a- table of data to be supplied by an application program that uses- the facility, other than as an argument passed when the facility- is invoked, then you must make a good faith effort to ensure that,- in the event an application does not supply such function or- table, the facility still operates, and performs whatever part of- its purpose remains meaningful.-- (For example, a function in a library to compute square roots has- a purpose that is entirely well-defined independent of the- application. Therefore, Subsection 2d requires that any- application-supplied function or table used by this function must- be optional: if the application does not supply it, the square- root function must still compute square roots.)--These requirements apply to the modified work as a whole. If-identifiable sections of that work are not derived from the Library,-and can be reasonably considered independent and separate works in-themselves, then this License, and its terms, do not apply to those-sections when you distribute them as separate works. But when you-distribute the same sections as part of a whole which is a work based-on the Library, the distribution of the whole must be on the terms of-this License, whose permissions for other licensees extend to the-entire whole, and thus to each and every part regardless of who wrote-it.--Thus, it is not the intent of this section to claim rights or contest-your rights to work written entirely by you; rather, the intent is to-exercise the right to control the distribution of derivative or-collective works based on the Library.--In addition, mere aggregation of another work not based on the Library-with the Library (or with a work based on the Library) on a volume of-a storage or distribution medium does not bring the other work under-the scope of this License.-- 3. You may opt to apply the terms of the ordinary GNU General Public-License instead of this License to a given copy of the Library. To do-this, you must alter all the notices that refer to this License, so-that they refer to the ordinary GNU General Public License, version 2,-instead of to this License. (If a newer version than version 2 of the-ordinary GNU General Public License has appeared, then you can specify-that version instead if you wish.) Do not make any other change in-these notices.-- Once this change is made in a given copy, it is irreversible for-that copy, so the ordinary GNU General Public License applies to all-subsequent copies and derivative works made from that copy.-- This option is useful when you wish to copy part of the code of-the Library into a program that is not a library.-- 4. You may copy and distribute the Library (or a portion or-derivative of it, under Section 2) in object code or executable form-under the terms of Sections 1 and 2 above provided that you accompany-it with the complete corresponding machine-readable source code, which-must be distributed under the terms of Sections 1 and 2 above on a-medium customarily used for software interchange.-- If distribution of object code is made by offering access to copy-from a designated place, then offering equivalent access to copy the-source code from the same place satisfies the requirement to-distribute the source code, even though third parties are not-compelled to copy the source along with the object code.-- 5. A program that contains no derivative of any portion of the-Library, but is designed to work with the Library by being compiled or-linked with it, is called a "work that uses the Library". Such a-work, in isolation, is not a derivative work of the Library, and-therefore falls outside the scope of this License.-- However, linking a "work that uses the Library" with the Library-creates an executable that is a derivative of the Library (because it-contains portions of the Library), rather than a "work that uses the-library". The executable is therefore covered by this License.-Section 6 states terms for distribution of such executables.-- When a "work that uses the Library" uses material from a header file-that is part of the Library, the object code for the work may be a-derivative work of the Library even though the source code is not.-Whether this is true is especially significant if the work can be-linked without the Library, or if the work is itself a library. The-threshold for this to be true is not precisely defined by law.-- If such an object file uses only numerical parameters, data-structure layouts and accessors, and small macros and small inline-functions (ten lines or less in length), then the use of the object-file is unrestricted, regardless of whether it is legally a derivative-work. (Executables containing this object code plus portions of the-Library will still fall under Section 6.)-- Otherwise, if the work is a derivative of the Library, you may-distribute the object code for the work under the terms of Section 6.-Any executables containing that work also fall under Section 6,-whether or not they are linked directly with the Library itself.-- 6. As an exception to the Sections above, you may also combine or-link a "work that uses the Library" with the Library to produce a-work containing portions of the Library, and distribute that work-under terms of your choice, provided that the terms permit-modification of the work for the customer's own use and reverse-engineering for debugging such modifications.-- You must give prominent notice with each copy of the work that the-Library is used in it and that the Library and its use are covered by-this License. You must supply a copy of this License. If the work-during execution displays copyright notices, you must include the-copyright notice for the Library among them, as well as a reference-directing the user to the copy of this License. Also, you must do one-of these things:-- a) Accompany the work with the complete corresponding- machine-readable source code for the Library including whatever- changes were used in the work (which must be distributed under- Sections 1 and 2 above); and, if the work is an executable linked- with the Library, with the complete machine-readable "work that- uses the Library", as object code and/or source code, so that the- user can modify the Library and then relink to produce a modified- executable containing the modified Library. (It is understood- that the user who changes the contents of definitions files in the- Library will not necessarily be able to recompile the application- to use the modified definitions.)-- b) Use a suitable shared library mechanism for linking with the- Library. A suitable mechanism is one that (1) uses at run time a- copy of the library already present on the user's computer system,- rather than copying library functions into the executable, and (2)- will operate properly with a modified version of the library, if- the user installs one, as long as the modified version is- interface-compatible with the version that the work was made with.-- c) Accompany the work with a written offer, valid for at least- three years, to give the same user the materials specified in- Subsection 6a, above, for a charge no more than the cost of- performing this distribution.-- d) If distribution of the work is made by offering access to copy- from a designated place, offer equivalent access to copy the above- specified materials from the same place.-- e) Verify that the user has already received a copy of these- materials or that you have already sent this user a copy.-- For an executable, the required form of the "work that uses the-Library" must include any data and utility programs needed for-reproducing the executable from it. However, as a special exception,-the materials to be distributed need not include anything that is-normally distributed (in either source or binary form) with the major-components (compiler, kernel, and so on) of the operating system on-which the executable runs, unless that component itself accompanies-the executable.-- It may happen that this requirement contradicts the license-restrictions of other proprietary libraries that do not normally-accompany the operating system. Such a contradiction means you cannot-use both them and the Library together in an executable that you-distribute.-- 7. You may place library facilities that are a work based on the-Library side-by-side in a single library together with other library-facilities not covered by this License, and distribute such a combined-library, provided that the separate distribution of the work based on-the Library and of the other library facilities is otherwise-permitted, and provided that you do these two things:-- a) Accompany the combined library with a copy of the same work- based on the Library, uncombined with any other library- facilities. This must be distributed under the terms of the- Sections above.-- b) Give prominent notice with the combined library of the fact- that part of it is a work based on the Library, and explaining- where to find the accompanying uncombined form of the same work.-- 8. You may not copy, modify, sublicense, link with, or distribute-the Library except as expressly provided under this License. Any-attempt otherwise to copy, modify, sublicense, link with, or-distribute the Library is void, and will automatically terminate your-rights under this License. However, parties who have received copies,-or rights, from you under this License will not have their licenses-terminated so long as such parties remain in full compliance.-- 9. You are not required to accept this License, since you have not-signed it. However, nothing else grants you permission to modify or-distribute the Library or its derivative works. These actions are-prohibited by law if you do not accept this License. Therefore, by-modifying or distributing the Library (or any work based on the-Library), you indicate your acceptance of this License to do so, and-all its terms and conditions for copying, distributing or modifying-the Library or works based on it.-- 10. Each time you redistribute the Library (or any work based on the-Library), the recipient automatically receives a license from the-original licensor to copy, distribute, link with or modify the Library-subject to these terms and conditions. You may not impose any further-restrictions on the recipients' exercise of the rights granted herein.-You are not responsible for enforcing compliance by third parties with-this License.-- 11. If, as a consequence of a court judgment or allegation of patent-infringement or for any other reason (not limited to patent issues),-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-distribute so as to satisfy simultaneously your obligations under this-License and any other pertinent obligations, then as a consequence you-may not distribute the Library at all. For example, if a patent-license would not permit royalty-free redistribution of the Library by-all those who receive copies directly or indirectly through you, then-the only way you could satisfy both it and this License would be to-refrain entirely from distribution of the Library.--If any portion of this section is held invalid or unenforceable under-any particular circumstance, the balance of the section is intended to-apply, and the section as a whole is intended to apply in other-circumstances.--It is not the purpose of this section to induce you to infringe any-patents or other property right claims or to contest validity of any-such claims; this section has the sole purpose of protecting the-integrity of the free software distribution system which is-implemented by public license practices. Many people have made-generous contributions to the wide range of software distributed-through that system in reliance on consistent application of that-system; it is up to the author/donor to decide if he or she is willing-to distribute software through any other system and a licensee cannot-impose that choice.--This section is intended to make thoroughly clear what is believed to-be a consequence of the rest of this License.-- 12. If the distribution and/or use of the Library is restricted in-certain countries either by patents or by copyrighted interfaces, the-original copyright holder who places the Library under this License-may add an explicit geographical distribution limitation excluding those-countries, so that distribution is permitted only in or among-countries not thus excluded. In such case, this License incorporates-the limitation as if written in the body of this License.-- 13. The Free Software Foundation may publish revised and/or new-versions of the Lesser 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 Library-specifies a version number of this License which applies to it and-"any later version", you have the option of following the terms and-conditions either of that version or of any later version published by-the Free Software Foundation. If the Library does not specify a-license version number, you may choose any version ever published by-the Free Software Foundation.-- 14. If you wish to incorporate parts of the Library into other free-programs whose distribution conditions are incompatible with these,-write to the author to ask for permission. For software which is-copyrighted by the Free Software Foundation, write to the Free-Software Foundation; we sometimes make exceptions for this. Our-decision will be guided by the two goals of preserving the free status-of all derivatives of our free software and of promoting the sharing-and reuse of software generally.-- NO WARRANTY-- 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO-WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR-OTHER PARTIES PROVIDE THE LIBRARY "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-LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.-- 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY-AND/OR REDISTRIBUTE THE LIBRARY 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-LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH-DAMAGES.-- END OF TERMS AND CONDITIONS-- How to Apply These Terms to Your New Libraries-- If you develop a new library, and you want it to be of the greatest-possible use to the public, we recommend making it free software that-everyone can redistribute and change. You can do so by permitting-redistribution under these terms (or, alternatively, under the terms-of the ordinary General Public License).-- To apply these terms, attach the following notices to the library.-It is safest to attach them to the start of each source file to most-effectively convey the exclusion of warranty; and each file should-have at least the "copyright" line and a pointer to where the full-notice is found.--- <one line to give the library's name and a brief idea of what it does.>- Copyright (C) <year> <name of author>-- This library is free software; you can redistribute it and/or- modify it under the terms of the GNU Lesser General Public- License as published by the Free Software Foundation; either- version 2.1 of the License, or (at your option) any later version.-- This library is distributed in the hope that it will be useful,- but WITHOUT ANY WARRANTY; without even the implied warranty of- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU- Lesser General Public License for more details.-- You should have received a copy of the GNU Lesser General Public- License along with this library; if not, write to the Free Software- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA--Also add information on how to contact you by electronic and paper mail.--You should also get your employer (if you work as a programmer) or-your school, if any, to sign a "copyright disclaimer" for the library,-if necessary. Here is a sample; alter the names:-- Yoyodyne, Inc., hereby disclaims all copyright interest in the- library `Frob' (a library for tweaking knobs) written by James- Random Hacker.-- <signature of Ty Coon>, 1 April 1990- Ty Coon, President of Vice+Copyright (c) 2012,2014 Joachim Fasting -That's all there is to it!+Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions: +The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
− NEWS
@@ -1,99 +0,0 @@-* v0.8.0, 2012-04-21- - Use bytestring for wire communication (sol)- - Increased type safety (sol)- - Improved memory usage (sol)- - `lsinfo` supports playlists (nandykins)- - `idle` now takes a list of subsystems (sol)- - `currentSong` works when playback is stopped (sol)- - Fixes failure on songs without associated paths (sol)- - `LsResult` replaces `EntryType` (nandykins)- - hspec based testing added to the test-suite- - More extensive parser testing- - 'MPDError' now has an 'Exception' instance- - Lower bound on Cabal bumped to 1.10--* v0.7.2, 2012-02-13- - Release connections. Reported by Kanisterschleife on GitHub.- - Some minor internal changes (sol)--* v0.7.1, 2012-02-07- - Compatible with GHC 7.4.1--* v0.7.0, 2011-11-22- - Several fixes to the test harness (Simon Hengel)- - Fixed issue with the (<$>) operator (Simon Hengel)- - Type safe handling of song IDs (Simon Hengel)- - Check MPD version on connect (now depends on MPD >= 0.15) (Simon Hengel)- - Compatibility with GHC 7.2 (Daniel Wagner)--* v0.6.0, 2011-04-01- - Reverted some changes from 0.5.0 that caused problems,- most notably the parser improvements have been removed for now.- - Support for GHC 7- - Removed support for building against the deprecated base 3 package- - Added an `Enum' instance for `Metadata'- - Removed the `old_base' flag--* v0.5.0, 2010-09-08- - Moved extensions to Network.MPD.Commands.Extensions- These might be removed in a later version- - Non-blocking `idle'- - The API is closer to the MPD spec, by untangling functionality- - Better MPD API coverage- - Improved parser implementation, now runs in constant space- - Constructors of the `Subsystem' type have been renamed- - Passwords can be changed using `setPassword'- - The connection handle can be accessed via `getHandle'- - The version of the MPD server is available via `getVersion'- - Added support for connecting via unix sockets--* v0.4.2, 2010-08-31- - Only depend on QuickCheck when building the test target--* v0.4.1, 2010-03-26- - Fix building test and coverage targets--* v0.4.0, 2010-03-26- - New maintainer: Joachim Fasting <joachim.fasting@gmail.com>- - Support QuickCheck 2- - Better MPD api support- Should be mostly compatible with mpd 0.16- - Separated operations on current playlist from those on specific- playlists- - Fixed password sending- - Several minor fixes and cleanups--* v0.3.1, 2008-09-14- - Now reconnects if MPD closes the connection.--* v0.3.0, 2008-05-06- - UTF-8 support (now depends on utf8-string package).- - Fixed corruption by `show' of command parameters.- - Tidied up `Query' interface.- - Moved StringConn out of Network.MPD to the tests directory.--* v0.2.1, 2008-04-14- - Cleaned up libmpd.cabal.--* v0.2.0, 2008-04-14- - A connection stub for testing purposes.- - QuickCheck tests for parsing.- - Partial unit test coverage.- - Many bug fixes.- - Precise error handling.- - Parsing improvements.- - Code coverage generation.- - Cabal 1.2 support.- - Uniform command names.--* v0.1.3, 2007-10-02- - Bugfixes.--* v0.1.2, 2007-09-29- - Changed name to libmpd.--* v0.1.1, 2007-09-28- - Missing files added to the source distribution.--* v0.1, 2007-09-28- - Initial public release.
− Network/MPD.hs
@@ -1,87 +0,0 @@-{-# LANGUAGE CPP #-}--- | Module : Network.MPD--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ An MPD client library. MPD is a daemon for playing music that is--- controlled over a network socket. Its site is at <http://www.musicpd.org/>.------ To use the library, do:--- --- > {-# LANGUAGE OverloadedStrings #-}--- > import qualified Network.MPD as MPD--module Network.MPD (- -- * Basic data types- MonadMPD, MPD, MPDError(..), ACKType(..), Response,- Host, Port, Password,- -- * Connections- withMPD, withMPD_, withMPDEx,- module Network.MPD.Commands,-#ifdef TEST- getConnectionSettings, getEnvDefault-#endif- ) where--import Prelude-import qualified Control.Exception as E-import Network.MPD.Commands-import Network.MPD.Core--import System.Environment (getEnv)-import System.IO.Error (isDoesNotExistError)-import Data.Maybe (listToMaybe)---- | A wrapper for 'withMPDEx' that uses localhost:6600 as the default--- host:port, or whatever is found in the environment variables MPD_HOST and--- MPD_PORT. If MPD_HOST is of the form \"password\@host\" the password--- will be supplied as well.------ Examples:------ > withMPD $ play Nothing--- > withMPD $ add_ "tool" >> play Nothing >> currentSong-withMPD :: MPD a -> IO (Response a)-withMPD = withMPD_ Nothing Nothing---- | Same as `withMPD`, but takes optional arguments that override MPD_HOST and--- MPD_PORT.------ This is e.g. useful for clients that optionally take @--port@ and @--host@--- as command line arguments, and fall back to `withMPD`'s defaults if those--- arguments are not given.-withMPD_ :: Maybe String -- ^ optional override for MPD_HOST- -> Maybe String -- ^ optional override for MPD_PORT- -> MPD a -> IO (Response a)-withMPD_ mHost mPort action = do- settings <- getConnectionSettings mHost mPort- case settings of- Right (host, port, pw) -> withMPDEx host port pw action- Left err -> (return . Left . Custom) err--getConnectionSettings :: Maybe String -> Maybe String -> IO (Either String (Host, Port, Password))-getConnectionSettings mHost mPort = do- (host, pw) <- parseHost `fmap`- maybe (getEnvDefault "MPD_HOST" "localhost") return mHost- port <- maybe (getEnvDefault "MPD_PORT" "6600") return mPort- case maybeRead port of- Just p -> (return . Right) (host, p, pw)- Nothing -> (return . Left) (show port ++ " is not a valid port!")- where- parseHost s = case breakChar '@' s of- (host, "") -> (host, "")- (pw, host) -> (host, pw)--getEnvDefault :: String -> String -> IO String-getEnvDefault x dflt =- E.catch (getEnv x) (\e -> if isDoesNotExistError e- then return dflt else ioError e)---- Break a string by character, removing the separator.-breakChar :: Char -> String -> (String, String)-breakChar c s = let (x, y) = break (== c) s in (x, drop 1 y)--maybeRead :: Read a => String -> Maybe a-maybeRead = fmap fst . listToMaybe . reads
− Network/MPD/Commands.hs
@@ -1,523 +0,0 @@-{-# LANGUAGE PatternGuards, OverloadedStrings #-}---- | Module : Network.MPD.Commands--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Interface to the user commands supported by MPD.--module Network.MPD.Commands (- -- * Command related data types- module Network.MPD.Commands.Types,-- -- * Query interface- module Network.MPD.Commands.Query,-- -- * Querying MPD's status- clearError, currentSong, idle, noidle, status, stats,-- -- * Playback options- consume, crossfade, random, repeat, setVolume, single, replayGainMode,- replayGainStatus,-- -- * Controlling playback- next, pause, play, playId, previous, seek, seekId, stop,-- -- * The current playlist- add, add_, addId, clear, delete, deleteId, move, moveId, playlist, playlistId,- playlistFind, playlistInfo, playlistSearch, plChanges, plChangesPosId, shuffle, swap,- swapId,-- -- * Stored playlist- listPlaylist, listPlaylistInfo, listPlaylists, load, playlistAdd,- playlistAdd_, playlistClear, playlistDelete, playlistMove, rename, rm,- save,-- -- * The music database- count, find, findAdd, list, listAll, listAllInfo, lsInfo, search, update,- rescan,-- -- * Stickers- stickerGet, stickerSet, stickerDelete, stickerList, stickerFind,-- -- * Connection- close, kill, password, ping,-- -- * Audio output devices- disableOutput, enableOutput, outputs,-- -- * Reflection- commands, notCommands, tagTypes, urlHandlers, decoders,- ) where--import Network.MPD.Commands.Arg-import Network.MPD.Commands.Parse-import Network.MPD.Commands.Query-import Network.MPD.Commands.Types-import Network.MPD.Commands.Util-import Network.MPD.Core-import Network.MPD.Util--import Control.Monad (liftM)-import Control.Monad.Error (throwError)-import Prelude hiding (repeat)--import qualified Data.ByteString.UTF8 as UTF8-import Data.ByteString (ByteString)------- Querying MPD's status------- | Clear the current error message in status.-clearError :: MonadMPD m => m ()-clearError = getResponse_ "clearerror"---- | Get the currently playing song.-currentSong :: (Functor m, MonadMPD m) => m (Maybe Song)-currentSong = getResponse "currentsong" >>= runParser parseMaybeSong . toAssocList---- | Wait until there is a noteworthy change in one or more of MPD's--- susbystems.------ The first argument is a list of subsystems that should be considered. An--- empty list specifies that all subsystems should be considered.------ A list of subsystems that have noteworthy changes is returned.------ Note that running this command will block until either 'idle' returns or is--- cancelled by 'noidle'.-idle :: MonadMPD m => [Subsystem] -> m [Subsystem]-idle subsystems =- mapM f =<< toAssocList `liftM` getResponse ("idle" <$> foldr (<++>) (Args []) subsystems)- where- f ("changed", system) =- case system of- "database" -> return DatabaseS- "update" -> return UpdateS- "stored_playlist" -> return StoredPlaylistS- "playlist" -> return PlaylistS- "player" -> return PlayerS- "mixer" -> return MixerS- "output" -> return OutputS- "options" -> return OptionsS- k -> fail ("Unknown subsystem: " ++ UTF8.toString k)- f x = fail ("idle: Unexpected " ++ show x)---- | Cancel 'idle'.-noidle :: MonadMPD m => m ()-noidle = getResponse_ "noidle"---- | Get server statistics.-stats :: MonadMPD m => m Stats-stats = getResponse "stats" >>= runParser parseStats---- | Get the server's status.-status :: MonadMPD m => m Status-status = getResponse "status" >>= runParser parseStatus------- Playback options------- | Set consume mode-consume :: MonadMPD m => Bool -> m ()-consume = getResponse_ . ("consume" <$>)---- | Set crossfading between songs.-crossfade :: MonadMPD m => Seconds -> m ()-crossfade secs = getResponse_ ("crossfade" <$> secs)---- | Set random playing.-random :: MonadMPD m => Bool -> m ()-random = getResponse_ . ("random" <$>)---- | Set repeating.-repeat :: MonadMPD m => Bool -> m ()-repeat = getResponse_ . ("repeat" <$>)---- | Set the volume (0-100 percent).-setVolume :: MonadMPD m => Int -> m ()-setVolume = getResponse_ . ("setvol" <$>)---- | Set single mode-single :: MonadMPD m => Bool -> m ()-single = getResponse_ . ("single" <$>)---- | Set the replay gain mode.-replayGainMode :: MonadMPD m => ReplayGainMode -> m ()-replayGainMode = getResponse_ . ("replay_gain_mode" <$>)---- | Get the replay gain options.-replayGainStatus :: MonadMPD m => m [String]-replayGainStatus = map UTF8.toString `liftM` getResponse "replay_gain_status"------- Controlling playback------- | Play the next song.-next :: MonadMPD m => m ()-next = getResponse_ "next"---- | Pause playing.-pause :: MonadMPD m => Bool -> m ()-pause = getResponse_ . ("pause" <$>)---- | Begin\/continue playing.-play :: MonadMPD m => Maybe Int -> m ()-play (Just pos) = getResponse_ ("play" <$> pos)-play _ = getResponse_ "play"---- | Play a file with given id.-playId :: MonadMPD m => Id -> m ()-playId id' = getResponse_ ("playid" <$> id')---- | Play the previous song.-previous :: MonadMPD m => m ()-previous = getResponse_ "previous"---- | Seek to some point in a song.-seek :: MonadMPD m => Int -> Seconds -> m ()-seek pos time = getResponse_ ("seek" <$> pos <++> time)---- | Seek to some point in a song (id version)-seekId :: MonadMPD m => Id -> Seconds -> m ()-seekId id' time = getResponse_ ("seekid" <$> id' <++> time)---- | Stop playing.-stop :: MonadMPD m => m ()-stop = getResponse_ "stop"------- The current playlist------- This might do better to throw an exception than silently return 0.--- | Like 'add', but returns a playlist id.-addId :: MonadMPD m => Path -> Maybe Integer -- ^ Optional playlist position- -> m Id-addId p pos = liftM (parse parseNum Id (Id 0) . snd . head . toAssocList)- $ getResponse1 ("addid" <$> p <++> pos)---- | Like 'add_' but returns a list of the files added.-add :: MonadMPD m => Path -> m [Path]-add x = add_ x >> listAll x---- | Add a song (or a whole directory) to the current playlist.-add_ :: MonadMPD m => Path -> m ()-add_ path = getResponse_ ("add" <$> path)---- | Clear the current playlist.-clear :: MonadMPD m => m ()-clear = getResponse_ "clear"---- | Remove a song from the current playlist.-delete :: MonadMPD m => Int -> m ()-delete pos = getResponse_ ("delete" <$> pos)---- | Remove a song from the current playlist.-deleteId :: MonadMPD m => Id -> m ()-deleteId id' = getResponse_ ("deleteid" <$> id')---- | Move a song to a given position in the current playlist.-move :: MonadMPD m => Int -> Int -> m ()-move pos to = getResponse_ ("move" <$> pos <++> to)---- | Move a song from (songid) to (playlist index) in the playlist. If to is--- negative, it is relative to the current song in the playlist (if there is one).-moveId :: MonadMPD m => Id -> Int -> m ()-moveId id' to = getResponse_ ("moveid" <$> id' <++> to)---- | Retrieve file paths and positions of songs in the current playlist.--- Note that this command is only included for completeness sake; it's--- deprecated and likely to disappear at any time, please use 'playlistInfo'--- instead.-playlist :: MonadMPD m => m [(Int, Path)]-playlist = mapM f =<< getResponse "playlist"- where f s | (pos, name) <- breakChar ':' s- , Just pos' <- parseNum pos- = return (pos', Path name)- | otherwise = throwError . Unexpected $ show s---- | Search for songs in the current playlist with strict matching.-playlistFind :: MonadMPD m => Query -> m [Song]-playlistFind q = takeSongs =<< getResponse ("playlistfind" <$> q)---- | Retrieve metadata for songs in the current playlist.-playlistInfo :: MonadMPD m => Maybe (Int, Int) -> m [Song]-playlistInfo range = takeSongs =<< getResponse ("playlistinfo" <$> range)---- | Displays a list of songs in the playlist.--- If id is specified, only its info is returned.-playlistId :: MonadMPD m => Maybe Id -> m [Song]-playlistId id' = takeSongs =<< getResponse ("playlistinfo" <$> id')---- | Search case-insensitively with partial matches for songs in the--- current playlist.-playlistSearch :: MonadMPD m => Query -> m [Song]-playlistSearch q = takeSongs =<< getResponse ("playlistsearch" <$> q)---- | Retrieve a list of changed songs currently in the playlist since--- a given playlist version.-plChanges :: MonadMPD m => Integer -> m [Song]-plChanges version = takeSongs =<< getResponse ("plchanges" <$> version)---- | Like 'plChanges' but only returns positions and ids.-plChangesPosId :: MonadMPD m => Integer -> m [(Int, Id)]-plChangesPosId plver =- getResponse ("plchangesposid" <$> plver) >>=- mapM f . splitGroups ["cpos"] . toAssocList- where f xs | [("cpos", x), ("Id", y)] <- xs- , Just (x', y') <- pair parseNum (x, y)- = return (x', Id y')- | otherwise = throwError . Unexpected $ show xs---- | Shuffle the playlist.-shuffle :: MonadMPD m => Maybe (Int, Int) -- ^ Optional range (start, end)- -> m ()-shuffle range = getResponse_ ("shuffle" <$> range)---- | Swap the positions of two songs.-swap :: MonadMPD m => Int -> Int -> m ()-swap pos1 pos2 = getResponse_ ("swap" <$> pos1 <++> pos2)---- | Swap the positions of two songs (Id version-swapId :: MonadMPD m => Id -> Id -> m ()-swapId id1 id2 = getResponse_ ("swapid" <$> id1 <++> id2)------- Stored playlists------- | Retrieve a list of files in a given playlist.-listPlaylist :: MonadMPD m => PlaylistName -> m [Path]-listPlaylist plname =- (map Path . takeValues) `liftM` getResponse ("listplaylist" <$> plname)---- | Retrieve metadata for files in a given playlist.-listPlaylistInfo :: MonadMPD m => PlaylistName -> m [Song]-listPlaylistInfo plname =- takeSongs =<< getResponse ("listplaylistinfo" <$> plname)---- | Retreive a list of stored playlists.-listPlaylists :: MonadMPD m => m [PlaylistName]-listPlaylists = (map PlaylistName . go [] . toAssocList) `liftM` getResponse "listplaylists"- where- -- After each playlist name we get a timestamp- go acc [] = acc- go acc ((_, b):_:xs) = go (b : acc) xs- go _ _ = error "listPlaylists: bug"---- | Load an existing playlist.-load :: MonadMPD m => PlaylistName -> m ()-load plname = getResponse_ ("load" <$> plname)---- | Like 'playlistAdd' but returns a list of the files added.-playlistAdd :: MonadMPD m => PlaylistName -> Path -> m [Path]-playlistAdd plname path = playlistAdd_ plname path >> listAll path---- | Add a song (or a whole directory) to a stored playlist.--- Will create a new playlist if the one specified does not already exist.-playlistAdd_ :: MonadMPD m => PlaylistName -> Path -> m ()-playlistAdd_ plname path = getResponse_ ("playlistadd" <$> plname <++> path)---- | Clear a playlist. If the specified playlist does not exist, it will be--- created.-playlistClear :: MonadMPD m => PlaylistName -> m ()-playlistClear = getResponse_ . ("playlistclear" <$>)---- | Remove a song from a playlist.-playlistDelete :: MonadMPD m => PlaylistName- -> Integer -- ^ Playlist position- -> m ()-playlistDelete name pos = getResponse_ ("playlistdelete" <$> name <++> pos)---- | Move a song to a given position in the playlist specified.-playlistMove :: MonadMPD m => PlaylistName -> Integer -> Integer -> m ()-playlistMove name from to =- getResponse_ ("playlistmove" <$> name <++> from <++> to)---- | Rename an existing playlist.-rename :: MonadMPD m- => PlaylistName -- ^ Original playlist- -> PlaylistName -- ^ New playlist name- -> m ()-rename plname new = getResponse_ ("rename" <$> plname <++> new)---- | Delete existing playlist.-rm :: MonadMPD m => PlaylistName -> m ()-rm plname = getResponse_ ("rm" <$> plname)---- | Save the current playlist.-save :: MonadMPD m => PlaylistName -> m ()-save plname = getResponse_ ("save" <$> plname)------- The music database------- | Count the number of entries matching a query.-count :: MonadMPD m => Query -> m Count-count query = getResponse ("count" <$> query) >>= runParser parseCount---- | Search the database for entries exactly matching a query.-find :: MonadMPD m => Query -> m [Song]-find query = getResponse ("find" <$> query) >>= takeSongs---- | Adds songs matching a query to the current playlist.-findAdd :: MonadMPD m => Query -> m ()-findAdd q = getResponse_ ("findadd" <$> q)---- | List all tags of the specified type.-list :: MonadMPD m- => Metadata -- ^ Metadata to list- -> Query -> m [Value]-list mtype query = (map Value . takeValues) `liftM` getResponse ("list" <$> mtype <++> query)----- | List the songs (without metadata) in a database directory recursively.-listAll :: MonadMPD m => Path -> m [Path]-listAll path = liftM (map (Path . snd) . filter ((== "file") . fst) . toAssocList)- (getResponse $ "listall" <$> path)---- Helper for lsInfo and listAllInfo.-lsInfo' :: MonadMPD m => Command -> Path -> m [LsResult]-lsInfo' cmd path = getResponse (cmd <$> path) >>= takeEntries---- | Recursive 'lsInfo'.-listAllInfo :: MonadMPD m => Path -> m [LsResult]-listAllInfo = lsInfo' "listallinfo"---- | Non-recursively list the contents of a database directory.-lsInfo :: MonadMPD m => Path -> m [LsResult]-lsInfo = lsInfo' "lsinfo"---- | Search the database using case insensitive matching.-search :: MonadMPD m => Query -> m [Song]-search query = getResponse ("search" <$> query) >>= takeSongs---- | Update the server's database.--- If no paths are given, all paths will be scanned.--- Unreadable or non-existent paths are silently ignored.-update :: MonadMPD m => [Path] -> m ()-update [] = getResponse_ "update"-update [x] = getResponse_ ("update" <$> x)-update xs = getResponses (map ("update" <$>) xs) >> return ()---- | Like 'update' but also rescans unmodified files.-rescan :: MonadMPD m => [Path] -> m ()-rescan [] = getResponse_ "rescan"-rescan [x] = getResponse_ ("rescan" <$> x)-rescan xs = getResponses (map ("rescan" <$>) xs) >> return ()------- Stickers------- | Reads a sticker value for the specified object.-stickerGet :: MonadMPD m => ObjectType- -> String -- ^ Object URI- -> String -- ^ Sticker name- -> m [String]-stickerGet typ uri name = (map UTF8.toString . takeValues) `liftM` getResponse ("sticker get" <$> typ <++> uri <++> name)---- | Adds a sticker value to the specified object.-stickerSet :: MonadMPD m => ObjectType- -> String -- ^ Object URI- -> String -- ^ Sticker name- -> String -- ^ Sticker value- -> m ()-stickerSet typ uri name value =- getResponse_ ("sticker set" <$> typ <++> uri <++> name <++> value)---- | Delete a sticker value from the specified object.-stickerDelete :: MonadMPD m => ObjectType- -> String -- ^ Object URI- -> String -- ^ Sticker name- -> m ()-stickerDelete typ uri name =- getResponse_ ("sticker delete" <$> typ <++> uri <++> name)---- an internal helper function-decodePair :: (ByteString, ByteString) -> (String, String)-decodePair (x, y) = (UTF8.toString x, UTF8.toString y)---- | Lists the stickers for the specified object.-stickerList :: MonadMPD m => ObjectType- -> String -- ^ Object URI- -> m [(String, String)] -- ^ Sticker name\/sticker value-stickerList typ uri =- (map decodePair . toAssocList) `liftM` getResponse ("sticker list" <$> typ <++> uri)---- | Searches the sticker database for stickers with the specified name, below--- the specified path.-stickerFind :: MonadMPD m => ObjectType- -> String -- ^ Path- -> String -- ^ Sticker name- -> m [(String, String)] -- ^ URI\/sticker value-stickerFind typ uri name =- (map decodePair . toAssocList) `liftM`- getResponse ("sticker find" <$> typ <++> uri <++> name)------- Connection------- XXX should the password be quoted? Change "++" to "<$>" if so. If--- it should, it also needs to be fixed in N.M.Core.--- | Send password to server to authenticate session.--- Password is sent as plain text.-password :: MonadMPD m => String -> m ()-password = getResponse_ . ("password " ++)---- | Check that the server is still responding.-ping :: MonadMPD m => m ()-ping = getResponse_ "ping"------- Audio output devices------- | Turn off an output device.-disableOutput :: MonadMPD m => Int -> m ()-disableOutput = getResponse_ . ("disableoutput" <$>)---- | Turn on an output device.-enableOutput :: MonadMPD m => Int -> m ()-enableOutput = getResponse_ . ("enableoutput" <$>)---- | Retrieve information for all output devices.-outputs :: MonadMPD m => m [Device]-outputs = getResponse "outputs" >>= runParser parseOutputs------- Reflection------- | Retrieve a list of available commands.-commands :: MonadMPD m => m [String]-commands = (map UTF8.toString . takeValues) `liftM` getResponse "commands"---- | Retrieve a list of unavailable (due to access restrictions) commands.-notCommands :: MonadMPD m => m [String]-notCommands = (map UTF8.toString . takeValues) `liftM` getResponse "notcommands"---- | Retrieve a list of available song metadata.-tagTypes :: MonadMPD m => m [String]-tagTypes = (map UTF8.toString . takeValues) `liftM` (getResponse "tagtypes")---- | Retrieve a list of supported urlhandlers.-urlHandlers :: MonadMPD m => m [String]-urlHandlers = (map UTF8.toString . takeValues) `liftM` (getResponse "urlhandlers")---- | Retreive a list of decoder plugins with associated suffix and mime types.-decoders :: MonadMPD m => m [(String, [(String, String)])]-decoders = (takeDecoders . toAssocList) `liftM` getResponse "decoders"- where- takeDecoders [] = []- takeDecoders ((_, p):xs) =- let (info, rest) = break ((==) "plugin" . fst) xs- in (UTF8.toString p, map decodePair info) : takeDecoders rest
− Network/MPD/Commands/Arg.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, GeneralizedNewtypeDeriving #-}---- | Module : Network.MPD.Commands.Arg--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Prepare command arguments.--module Network.MPD.Commands.Arg (Command, Args(..), MPDArg(..), (<++>), (<$>)) where--import Network.MPD.Util (showBool)--import Data.ByteString (ByteString)-import qualified Data.ByteString.UTF8 as UTF8-import Data.String---- | Arguments for getResponse are accumulated as strings in values of--- this type after being converted from whatever type (an instance of--- MPDArg) they were to begin with.-newtype Args = Args [String]- deriving Show---- | A uniform interface for argument preparation--- The basic idea is that one should be able--- to magically prepare an argument for use with--- an MPD command, without necessarily knowing/\caring--- how it needs to be represented internally.-class Show a => MPDArg a where- prep :: a -> Args- -- Note that because of this, we almost- -- never have to actually provide- -- an implementation of 'prep'- prep = Args . return . show---- | Groups together arguments to getResponse.-infixl 3 <++>-(<++>) :: (MPDArg a, MPDArg b) => a -> b -> Args-x <++> y = Args $ xs ++ ys- where Args xs = prep x- Args ys = prep y--newtype Command = Command String- deriving IsString---- | Converts a command name and a string of arguments into the string--- to hand to getResponse.-infix 2 <$>-(<$>) :: (MPDArg a) => Command -> a -> String-Command x <$> y = unwords $ x : filter (not . null) y'- where Args y' = prep y--instance MPDArg Args where prep = id--instance MPDArg String where- -- We do this to avoid mangling- -- non-ascii characters with 'show'- prep x = Args ['"' : x ++ "\""]--instance MPDArg ByteString where- prep = prep . UTF8.toString--instance (MPDArg a) => MPDArg (Maybe a) where- prep Nothing = Args []- prep (Just x) = prep x--instance (MPDArg a, MPDArg b) => MPDArg (a, b) where- prep (x, y) = Args [show x ++ ":" ++ show y]--instance MPDArg Int-instance MPDArg Integer-instance MPDArg Bool where prep = Args . return . showBool
− Network/MPD/Commands/Extensions.hs
@@ -1,147 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Module : Network.MPD.Commands.Extensions--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Extensions and shortcuts to the standard MPD command set.--module Network.MPD.Commands.Extensions where--import Network.MPD.Core-import Network.MPD.Commands-import Network.MPD.Commands.Arg-import Network.MPD.Commands.Util-import Network.MPD.Util (read)--import Prelude hiding (read)-import Control.Monad (liftM)---- | Like 'update', but returns the update job id.-updateId :: MonadMPD m => [Path] -> m Integer-updateId paths = liftM (read . head . takeValues) cmd- where cmd = case paths of- [] -> getResponse "update"- [x] -> getResponse ("update" <$> x)- xs -> getResponses $ map ("update" <$>) xs---- | Toggles play\/pause. Plays if stopped.-toggle :: MonadMPD m => m ()-toggle = status >>= \st -> case stState st of Playing -> pause True- _ -> play Nothing---- | Add a list of songs\/folders to a playlist.--- Should be more efficient than running 'add' many times.-addMany :: MonadMPD m => PlaylistName -> [Path] -> m ()-addMany _ [] = return ()-addMany "" [x] = add_ x-addMany plname [x] = playlistAdd_ plname x-addMany plname xs = getResponses (map cmd xs) >> return ()- where cmd x = case plname of- "" -> "add" <$> x- pl -> "playlistadd" <$> pl <++> x---- | Delete a list of songs from a playlist.--- If there is a duplicate then no further songs will be deleted, so--- take care to avoid them (see 'prune' for this).-{- deleteMany :: MonadMPD m => PlaylistName -> [PLIndex] -> m ()-deleteMany _ [] = return ()-deleteMany plname [(Pos x)] = playlistDelete plname x-deleteMany "" xs = getResponses (map cmd xs) >> return ()- where cmd (Pos x) = "delete" <$> x- cmd (ID x) = "deleteid" <$> x-deleteMany plname xs = getResponses (map cmd xs) >> return ()- where cmd (Pos x) = "playlistdelete" <$> plname <++> x- cmd _ = ""---- | Returns all songs and directories that match the given partial--- path name.-complete :: MonadMPD m => String -> m [Either Path Song]-complete path = do- xs <- liftM matches . lsInfo $ dropFileName path- case xs of- [Left dir] -> complete $ dir ++ "/"- _ -> return xs- where- matches = filter (isPrefixOf path . takePath)- takePath = either id sgFilePath---- | Crop playlist.--- The bounds are inclusive.--- If 'Nothing' is passed the cropping will leave your playlist alone--- on that side.--- Using 'ID' will automatically find the absolute playlist position and use--- that as the cropping boundary.-crop :: MonadMPD m => Maybe PLIndex -> Maybe PLIndex -> m ()-crop x y = do- pl <- playlistInfo Nothing- let x' = case x of Just (Pos p) -> fromInteger p- Just (ID i) -> fromMaybe 0 (findByID i pl)- Nothing -> 0- -- ensure that no songs are deleted twice with 'max'.- ys = case y of Just (Pos p) -> drop (max (fromInteger p) x') pl- Just (ID i) -> maybe [] (flip drop pl . max x' . (+1))- (findByID i pl)- Nothing -> []- deleteMany "" . mapMaybe sgIndex $ take x' pl ++ ys- where findByID i = findIndex ((==) i . (\(ID j) -> j) . fromJust . sgIndex)---- | Remove duplicate playlist entries.-prune :: MonadMPD m => m ()-prune = findDuplicates >>= deleteMany ""---- Find duplicate playlist entries.-findDuplicates :: MonadMPD m => m [PLIndex]-findDuplicates =- liftM (map ((\(ID x) -> ID x) . fromJust . sgIndex) . flip dups ([],[])) $- playlistInfo Nothing- where dups [] (_, dup) = dup- dups (x:xs) (ys, dup)- | x `mSong` xs && not (x `mSong` ys) = dups xs (ys, x:dup)- | otherwise = dups xs (x:ys, dup)- mSong x = let m = sgFilePath x in any ((==) m . sgFilePath)---- | List directories non-recursively.-lsDirs :: MonadMPD m => Path -> m [Path]-lsDirs path =- liftM (extractEntries (const Nothing,const Nothing, Just)) $- takeEntries =<< getResponse ("lsinfo" <$> path)---- | List files non-recursively.-lsFiles :: MonadMPD m => Path -> m [Path]-lsFiles path =- liftM (extractEntries (Just . sgFilePath, const Nothing, const Nothing)) $- takeEntries =<< getResponse ("lsinfo" <$> path)---- | List all playlists.-lsPlaylists :: MonadMPD m => m [PlaylistName]-lsPlaylists = liftM (extractEntries (const Nothing, Just, const Nothing)) $- takeEntries =<< getResponse "lsinfo" -}---- | List the artists in the database.-listArtists :: MonadMPD m => m [Artist]-listArtists = (map Value . takeValues) `liftM` (getResponse "list artist")---- | List the albums in the database, optionally matching a given--- artist.-listAlbums :: MonadMPD m => Maybe Artist -> m [Album]-listAlbums artist = (map Value . takeValues) `liftM`- getResponse ("list album" <$> fmap (("artist" :: String) <++>) artist)---- | List the songs in an album of some artist.-listAlbum :: MonadMPD m => Artist -> Album -> m [Song]-listAlbum artist album = find (Artist =? artist <&> Album =? album)---- | Retrieve the current playlist.--- Equivalent to @playlistinfo Nothing@.-getPlaylist :: MonadMPD m => m [Song]-getPlaylist = playlistInfo Nothing---- | Increase or decrease volume by a given percent, e.g.--- 'volume 10' will increase the volume by 10 percent, while--- 'volume (-10)' will decrease it by the same amount.-volume :: MonadMPD m => Int -> m ()-volume n = do- current <- (fromIntegral . stVolume) `liftM` status- setVolume . round $ (fromIntegral n / (100 :: Double)) * current + current
− Network/MPD/Commands/Parse.hs
@@ -1,183 +0,0 @@-{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}---- | Module : Network.MPD.Commands.Parse--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Parsers for MPD data types.--module Network.MPD.Commands.Parse where--import Network.MPD.Commands.Types--import Control.Arrow ((***))-import Control.Monad.Error-import Data.Maybe (fromMaybe)--import Network.MPD.Util-import Network.MPD.Core (MonadMPD, MPDError(Unexpected))--import Data.ByteString.Char8 (ByteString)-import qualified Data.ByteString.UTF8 as UTF8---- | Builds a 'Count' instance from an assoc. list.-parseCount :: [ByteString] -> Either String Count-parseCount = foldM f defaultCount . toAssocList- where f :: Count -> (ByteString, ByteString) -> Either String Count- f a ("songs", x) = return $ parse parseNum- (\x' -> a { cSongs = x'}) a x- f a ("playtime", x) = return $ parse parseNum- (\x' -> a { cPlaytime = x' }) a x- f _ x = Left $ show x---- | Builds a list of 'Device' instances from an assoc. list-parseOutputs :: [ByteString] -> Either String [Device]-parseOutputs = mapM (foldM f defaultDevice)- . splitGroups ["outputid"]- . toAssocList- where f a ("outputid", x) = return $ parse parseNum- (\x' -> a { dOutputID = x' }) a x- f a ("outputname", x) = return a { dOutputName = UTF8.toString x }- f a ("outputenabled", x) = return $ parse parseBool- (\x' -> a { dOutputEnabled = x'}) a x- f _ x = fail $ show x---- | Builds a 'Stats' instance from an assoc. list.-parseStats :: [ByteString] -> Either String Stats-parseStats = foldM f defaultStats . toAssocList- where- f a ("artists", x) = return $ parse parseNum- (\x' -> a { stsArtists = x' }) a x- f a ("albums", x) = return $ parse parseNum- (\x' -> a { stsAlbums = x' }) a x- f a ("songs", x) = return $ parse parseNum- (\x' -> a { stsSongs = x' }) a x- f a ("uptime", x) = return $ parse parseNum- (\x' -> a { stsUptime = x' }) a x- f a ("playtime", x) = return $ parse parseNum- (\x' -> a { stsPlaytime = x' }) a x- f a ("db_playtime", x) = return $ parse parseNum- (\x' -> a { stsDbPlaytime = x' }) a x- f a ("db_update", x) = return $ parse parseNum- (\x' -> a { stsDbUpdate = x' }) a x- f _ x = fail $ show x--parseMaybeSong :: [(ByteString, ByteString)] -> Either String (Maybe Song)-parseMaybeSong xs | null xs = Right Nothing- | otherwise = Just `fmap` parseSong xs---- | Builds a 'Song' instance from an assoc. list.-parseSong :: [(ByteString, ByteString)] -> Either String Song-parseSong xs = case xs of- ("file", path):ys -> foldM f (defaultSong $ Path path) ys- _ -> Left "Got a song without a file path! This indicates a bug in either libmpd-haskell or MPD itself!"-- where- f :: Song -> (ByteString, ByteString) -> Either String Song- f s ("Last-Modified", v) =- return s { sgLastModified = parseIso8601 v }- f s ("Time", v) =- return s { sgLength = fromMaybe 0 $ parseNum v }- f s ("Id", v) =- return $ parse parseNum (\v' -> s { sgId = Just $ Id v' }) s v- f s ("Pos", v) =- maybe (return $ parse parseNum- (\v' -> s { sgIndex = Just v' }) s v)- (const $ return s)- (sgIndex s)- f s (k, v) = return . maybe s (\m -> sgAddTag m (Value v) s) $- readMeta k-- -- Custom-made Read instance- readMeta "Artist" = Just Artist- readMeta "Album" = Just Album- readMeta "Title" = Just Title- readMeta "Genre" = Just Genre- readMeta "Name" = Just Name- readMeta "Composer" = Just Composer- readMeta "Performer" = Just Performer- readMeta "Date" = Just Date- readMeta "Track" = Just Track- readMeta "Disc" = Just Disc- readMeta "MUSICBRAINZ_ARTISTID" = Just MUSICBRAINZ_ARTISTID- readMeta "MUSICBRAINZ_TRACKID" = Just MUSICBRAINZ_TRACKID- readMeta _ = Nothing---- | Builds a 'Status' instance from an assoc. list.-parseStatus :: [ByteString] -> Either String Status-parseStatus = foldM f defaultStatus . toAssocList- where f a ("state", x)- = return $ parse state (\x' -> a { stState = x' }) a x- f a ("volume", x)- = return $ parse parseNum (\x' -> a { stVolume = x' }) a x- f a ("repeat", x)- = return $ parse parseBool (\x' -> a { stRepeat = x' }) a x- f a ("random", x)- = return $ parse parseBool (\x' -> a { stRandom = x' }) a x- f a ("playlist", x)- = return $ parse parseNum (\x' -> a { stPlaylistVersion = x' }) a x- f a ("playlistlength", x)- = return $ parse parseNum (\x' -> a { stPlaylistLength = x' }) a x- f a ("xfade", x)- = return $ parse parseNum (\x' -> a { stXFadeWidth = x' }) a x- f a ("mixrampdb", x)- = return $ parse parseFrac (\x' -> a { stMixRampdB = x' }) a x- f a ("mixrampdelay", x)- = return $ parse parseFrac (\x' -> a { stMixRampDelay = x' }) a x- f a ("song", x)- = return $ parse parseNum (\x' -> a { stSongPos = Just x' }) a x- f a ("songid", x)- = return $ parse parseNum (\x' -> a { stSongID = Just $ Id x' }) a x- f a ("time", x)- = return $ parse time (\x' -> a { stTime = x' }) a x- f a ("elapsed", x)- = return $ parse parseFrac (\x' -> a { stTime = (x', snd $ stTime a) }) a x- f a ("bitrate", x)- = return $ parse parseNum (\x' -> a { stBitrate = x' }) a x- f a ("audio", x)- = return $ parse audio (\x' -> a { stAudio = x' }) a x- f a ("updating_db", x)- = return $ parse parseNum (\x' -> a { stUpdatingDb = x' }) a x- f a ("error", x)- = return a { stError = Just (UTF8.toString x) }- f a ("single", x)- = return $ parse parseBool (\x' -> a { stSingle = x' }) a x- f a ("consume", x)- = return $ parse parseBool (\x' -> a { stConsume = x' }) a x- f a ("nextsong", x)- = return $ parse parseNum (\x' -> a { stNextSongPos = Just x' }) a x- f a ("nextsongid", x)- = return $ parse parseNum (\x' -> a { stNextSongID = Just $ Id x' }) a x- f _ x- = fail $ show x-- state "play" = Just Playing- state "pause" = Just Paused- state "stop" = Just Stopped- state _ = Nothing-- time s = case parseFrac *** parseNum $ breakChar ':' s of- (Just a, Just b) -> Just (a, b)- _ -> Nothing-- audio = parseTriple ':' parseNum---- | Run a parser and lift the result into the 'MPD' monad-runParser :: (MonadMPD m, MonadError MPDError m)- => (input -> Either String a) -> input -> m a-runParser f = either (throwError . Unexpected) return . f---- | A helper that runs a parser on a string and, depending on the--- outcome, either returns the result of some command applied to the--- result, or a default value. Used when building structures.-parse :: (ByteString -> Maybe a) -> (a -> b) -> b -> ByteString -> b-parse parser f x = maybe x f . parser---- | A helper for running a parser returning Maybe on a pair of strings.--- Returns Just if both strings where parsed successfully, Nothing otherwise.-pair :: (ByteString -> Maybe a) -> (ByteString, ByteString) -> Maybe (a, a)-pair p (x, y) = case (p x, p y) of- (Just a, Just b) -> Just (a, b)- _ -> Nothing
− Network/MPD/Commands/Query.hs
@@ -1,56 +0,0 @@--- | Module : Network.MPD.Commands.Query--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Query interface.--module Network.MPD.Commands.Query (Query, (=?), (<&>), anything) where--import Network.MPD.Commands.Arg-import Network.MPD.Commands.Types--import Data.Monoid---- | An interface for creating MPD queries.------ For example, to match any song where the value of artist is \"Foo\", we--- use:------ > Artist =? "Foo"------ We can also compose queries, thus narrowing the search. For example, to--- match any song where the value of artist is \"Foo\" and the value of album--- is \"Bar\", we use:------ > Artist =? "Foo" <&> Album =? "Bar"-newtype Query = Query [Match] deriving Show---- A single query clause, comprising a metadata key and a desired value.-data Match = Match Metadata Value--instance Show Match where- show (Match meta query) = show meta ++ " \"" ++ toString query ++ "\""- showList xs _ = unwords $ map show xs--instance Monoid Query where- mempty = Query []- Query a `mappend` Query b = Query (a ++ b)--instance MPDArg Query where- prep = foldl (<++>) (Args []) . f- where f (Query ms) = map (\(Match m q) -> Args [show m] <++> q) ms---- | An empty query. Matches anything.-anything :: Query-anything = mempty---- | Create a query.-(=?) :: Metadata -> Value -> Query-m =? s = Query [Match m s]---- | Combine queries.-infixr 6 <&>-(<&>) :: Query -> Query -> Query-(<&>) = mappend
− Network/MPD/Commands/Types.hs
@@ -1,285 +0,0 @@-{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, GeneralizedNewtypeDeriving #-}--- | Module : Network.MPD.Commands.Types--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Various MPD data structures and types--module Network.MPD.Commands.Types where--import Network.MPD.Commands.Arg (MPDArg(prep), Args(Args))--import qualified Data.Map as M-import Data.Time.Clock (UTCTime)-import Data.String--import Data.Text (Text)-import qualified Data.Text.Encoding as Text-import Data.ByteString (ByteString)-import qualified Data.ByteString.UTF8 as UTF8---- The purpose of this class is to allow users to choose the optimal--- representation of response values.--- | A type class for values that can be converted to `String`s.-class ToString a where-- -- | Convert given value to `String`.- toString :: a -> String-- -- | Convert given value to `Text`.- toText :: a -> Text-- -- | Convert given value an UTF-8 encoded `ByteString`.- toUtf8 :: a -> ByteString--type Artist = Value-type Album = Value-type Title = Value---- | Used for commands which require a playlist name.--- If empty, the current playlist is used.-newtype PlaylistName = PlaylistName ByteString- deriving (Eq, Show, MPDArg)--instance ToString PlaylistName where- toString (PlaylistName x) = UTF8.toString x- toText (PlaylistName x) = Text.decodeUtf8 x- toUtf8 (PlaylistName x) = x--instance IsString PlaylistName where- fromString = PlaylistName . UTF8.fromString---- | Used for commands which require a path within the database.--- If empty, the root path is used.-newtype Path = Path ByteString- deriving (Eq, Show, MPDArg)--instance ToString Path where- toString (Path x) = UTF8.toString x- toText (Path x) = Text.decodeUtf8 x- toUtf8 (Path x) = x--instance IsString Path where- fromString = Path . UTF8.fromString---- | Available metadata types\/scope modifiers, used for searching the--- database for entries with certain metadata values.-data Metadata = Artist- -- NOTE: The commented constructors are not yet handled by the- -- parser. If you uncomment them, some test will fail.- -- | ArtistSort- | Album- -- | AlbumArtist- -- | AlbumArtistSort- | Title- | Track- | Name- | Genre- | Date- | Composer- | Performer- -- | Comment- | Disc- | MUSICBRAINZ_ARTISTID- -- | MUSICBRAINZ_ALBUMID- -- | MUSICBRAINZ_ALBUMARTISTID- | MUSICBRAINZ_TRACKID- deriving (Eq, Enum, Ord, Bounded, Show)--instance MPDArg Metadata---- | A metadata value.-newtype Value = Value ByteString- deriving (Eq, Show, MPDArg)--instance ToString Value where- toString (Value x) = UTF8.toString x- toText (Value x) = Text.decodeUtf8 x- toUtf8 (Value x) = x--instance IsString Value where- fromString = Value . UTF8.fromString---- | Object types.-data ObjectType = SongObj- deriving (Eq, Show)--instance MPDArg ObjectType where- prep SongObj = Args ["song"]--type Seconds = Integer---- | Represents the different playback states.-data State = Playing- | Stopped- | Paused- deriving (Show, Eq)---- | Represents the various MPD subsystems.-data Subsystem- = DatabaseS -- ^ The song database- | UpdateS -- ^ Database updates- | StoredPlaylistS -- ^ Stored playlists- | PlaylistS -- ^ The current playlist- | PlayerS -- ^ The player- | MixerS -- ^ The volume mixer- | OutputS -- ^ Audio outputs- | OptionsS -- ^ Playback options- deriving (Eq, Show)--instance MPDArg Subsystem where- prep DatabaseS = Args ["database"]- prep UpdateS = Args ["update"]- prep StoredPlaylistS = Args ["stored_playlist"]- prep PlaylistS = Args ["playlist"]- prep PlayerS = Args ["player"]- prep MixerS = Args ["mixer"]- prep OutputS = Args ["output"]- prep OptionsS = Args ["options"]--data ReplayGainMode- = Off -- ^ Disable replay gain- | TrackMode -- ^ Per track mode- | AlbumMode -- ^ Per album mode- deriving (Eq, Show)--instance MPDArg ReplayGainMode where- prep Off = Args ["off"]- prep TrackMode = Args ["track"]- prep AlbumMode = Args ["album"]---- | Represents the result of running 'count'.-data Count =- Count { cSongs :: Integer -- ^ Number of songs matching the query- , cPlaytime :: Seconds -- ^ Total play time of matching songs- }- deriving (Eq, Show)--defaultCount :: Count-defaultCount = Count { cSongs = 0, cPlaytime = 0 }---- | Result of the lsInfo operation-data LsResult- = LsDirectory Path -- ^ Directory- | LsSong Song -- ^ Song- | LsPlaylist PlaylistName -- ^ Playlist- deriving (Eq, Show)---- | Represents an output device.-data Device =- Device { dOutputID :: Int -- ^ Output's ID number- , dOutputName :: String -- ^ Output's name as defined in the MPD- -- configuration file- , dOutputEnabled :: Bool }- deriving (Eq, Show)--defaultDevice :: Device-defaultDevice =- Device { dOutputID = 0, dOutputName = "", dOutputEnabled = False }---- | Represents a single song item.-data Song = Song- { sgFilePath :: Path- -- | Map of available tags (multiple occurences of one tag type allowed)- , sgTags :: M.Map Metadata [Value]- -- | Last modification date- , sgLastModified :: Maybe UTCTime- -- | Length of the song in seconds- , sgLength :: Seconds- -- | Id in playlist- , sgId :: Maybe Id- -- | Position in playlist- , sgIndex :: Maybe Int- } deriving (Eq, Show)--newtype Id = Id Int- deriving (Eq, Show)--instance (MPDArg Id) where- prep (Id x) = prep x---- | Get list of specific tag type-sgGetTag :: Metadata -> Song -> Maybe [Value]-sgGetTag meta s = M.lookup meta $ sgTags s---- | Add metadata tag value.-sgAddTag :: Metadata -> Value -> Song -> Song-sgAddTag meta value s = s { sgTags = M.insertWith' (++) meta [value] (sgTags s) }--defaultSong :: Path -> Song-defaultSong path =- Song { sgFilePath = path, sgTags = M.empty, sgLastModified = Nothing- , sgLength = 0, sgId = Nothing, sgIndex = Nothing }---- | Container for database statistics.-data Stats =- Stats { stsArtists :: Integer -- ^ Number of artists.- , stsAlbums :: Integer -- ^ Number of albums.- , stsSongs :: Integer -- ^ Number of songs.- , stsUptime :: Seconds -- ^ Daemon uptime in seconds.- , stsPlaytime :: Seconds -- ^ Total playing time.- , stsDbPlaytime :: Seconds -- ^ Total play time of all the songs in- -- the database.- , stsDbUpdate :: Integer -- ^ Last database update in UNIX time.- }- deriving (Eq, Show)--defaultStats :: Stats-defaultStats =- Stats { stsArtists = 0, stsAlbums = 0, stsSongs = 0, stsUptime = 0- , stsPlaytime = 0, stsDbPlaytime = 0, stsDbUpdate = 0 }---- | Container for MPD status.-data Status =- Status { stState :: State- -- | A percentage (0-100)- , stVolume :: Int- , stRepeat :: Bool- , stRandom :: Bool- -- | A value that is incremented by the server every time the- -- playlist changes.- , stPlaylistVersion :: Integer- -- | The number of items in the current playlist.- , stPlaylistLength :: Integer- -- | Current song's position in the playlist.- , stSongPos :: Maybe Int- -- | Current song's playlist ID.- , stSongID :: Maybe Id- -- | Next song's position in the playlist.- , stNextSongPos :: Maybe Int- -- | Next song's playlist ID.- , stNextSongID :: Maybe Id- -- | Time elapsed\/total time.- , stTime :: (Double, Seconds)- -- | Bitrate (in kilobytes per second) of playing song (if any).- , stBitrate :: Int- -- | Crossfade time.- , stXFadeWidth :: Seconds- -- | MixRamp threshold in dB- , stMixRampdB :: Double- -- | MixRamp extra delay in seconds- , stMixRampDelay :: Double- -- | Samplerate\/bits\/channels for the chosen output device- -- (see mpd.conf).- , stAudio :: (Int, Int, Int)- -- | Job ID of currently running update (if any).- , stUpdatingDb :: Integer- -- | If True, MPD will play only one song and stop after finishing it.- , stSingle :: Bool- -- | If True, a song will be removed after it has been played.- , stConsume :: Bool- -- | Last error message (if any).- , stError :: Maybe String }- deriving (Eq, Show)--defaultStatus :: Status-defaultStatus =- Status { stState = Stopped, stVolume = 0, stRepeat = False- , stRandom = False, stPlaylistVersion = 0, stPlaylistLength = 0- , stSongPos = Nothing, stSongID = Nothing, stTime = (0,0)- , stNextSongPos = Nothing, stNextSongID = Nothing- , stBitrate = 0, stXFadeWidth = 0, stMixRampdB = 0- , stMixRampDelay = 0, stAudio = (0,0,0), stUpdatingDb = 0- , stSingle = False, stConsume = False, stError = Nothing }
− Network/MPD/Commands/Util.hs
@@ -1,69 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Module : Network.MPD.Commands.Util--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Internal utilities for implementing MPD commands.--module Network.MPD.Commands.Util where--import Network.MPD.Commands.Parse-import Network.MPD.Commands.Types-import Network.MPD.Core-import Network.MPD.Util--import Control.Monad.Error-import Data.List (intersperse)-import Data.Maybe (mapMaybe)--import Data.ByteString.Char8 (ByteString)---- Run getResponse but discard the response.-getResponse_ :: MonadMPD m => String -> m ()-getResponse_ x = getResponse x >> return ()---- Get the lines of the daemon's response to a list of commands.-getResponses :: MonadMPD m => [String] -> m [ByteString]-getResponses cmds = getResponse . concat $ intersperse "\n" cmds'- where cmds' = "command_list_begin" : cmds ++ ["command_list_end"]---- Helper that throws unexpected error if input is empty.-failOnEmpty :: MonadMPD m => [ByteString] -> m [ByteString]-failOnEmpty [] = throwError $ Unexpected "Non-empty response expected."-failOnEmpty xs = return xs---- A wrapper for getResponse that fails on non-empty responses.-getResponse1 :: MonadMPD m => String -> m [ByteString]-getResponse1 x = getResponse x >>= failOnEmpty---- Run 'toAssocList' and return only the values.-takeValues :: [ByteString] -> [ByteString]-takeValues = snd . unzip . toAssocList---- Separate the result of an lsinfo\/listallinfo call into directories,--- playlists, and songs.-takeEntries :: MonadMPD m => [ByteString] -> m [LsResult]-takeEntries = mapM toEntry . splitGroups groupHeads . toAssocList- where- toEntry xs@(("file",_):_) = LsSong `liftM` runParser parseSong xs- toEntry (("directory",d):_) = (return . LsDirectory . Path) d- toEntry (("playlist",pl):_) = (return . LsPlaylist . PlaylistName) pl- toEntry _ = error "takeEntries: splitGroups is broken"- groupHeads = ["file", "directory", "playlist"]---- Extract a subset of songs, directories, and playlists.-extractEntries :: (Song -> Maybe a, PlaylistName -> Maybe a, Path -> Maybe a)- -> [LsResult] -> [a]-extractEntries (fSong,fPlayList,fDir) = mapMaybe f- where- f (LsSong s) = fSong s- f (LsPlaylist pl) = fPlayList pl- f (LsDirectory d) = fDir d---- Build a list of song instances from a response.-takeSongs :: MonadMPD m => [ByteString] -> m [Song]-takeSongs = mapM (runParser parseSong)- . splitGroups ["file"]- . toAssocList
− Network/MPD/Core.hs
@@ -1,249 +0,0 @@-{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving, OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}---- | Module : Network.MPD.Core--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ The core datatypes and operations are defined here, including the--- primary instance of the 'MonadMPD' class, 'MPD'.--module Network.MPD.Core (- -- * Classes- MonadMPD(..),- -- * Data types- MPD, MPDError(..), ACKType(..), Response, Host, Port, Password,- -- * Running- withMPDEx,- -- * Interacting- getResponse, kill,- ) where--import Network.MPD.Util-import Network.MPD.Core.Class-import Network.MPD.Core.Error--import Data.Char (isDigit)-import Control.Applicative (Applicative(..), (<$>), (<*))-import qualified Control.Exception as E-import Control.Monad (ap, unless)-import Control.Monad.Error (ErrorT(..), MonadError(..))-import Control.Monad.Reader (ReaderT(..), ask)-import Control.Monad.State (StateT, MonadIO(..), modify, get, evalStateT)-import qualified Data.Foldable as F-import Network (PortID(..), withSocketsDo, connectTo)-import System.IO (Handle, hPutStrLn, hReady, hClose, hFlush)-import System.IO.Error (isEOFError)-import qualified System.IO.UTF8 as U-import Text.Printf (printf)--import qualified Prelude-import Prelude hiding (break, drop, dropWhile, read)-import Data.ByteString.Char8 (ByteString, isPrefixOf, break, drop, dropWhile)-import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.UTF8 as UTF8------- Data types.-----type Host = String-type Port = Integer------- IO based MPD client implementation.------- | The main implementation of an MPD client. It actually connects--- to a server and interacts with it.------ To use the error throwing\/catching capabilities:------ > import Control.Monad.Error (throwError, catchError)------ To run IO actions within the MPD monad:------ > import Control.Monad.Trans (liftIO)--newtype MPD a =- MPD { runMPD :: ErrorT MPDError- (StateT MPDState- (ReaderT (Host, Port) IO)) a- } deriving (Functor, Monad, MonadIO, MonadError MPDError)--instance Applicative MPD where- (<*>) = ap- pure = return--instance MonadMPD MPD where- open = mpdOpen- close = mpdClose- send = mpdSend- getHandle = MPD $ stHandle <$> get- getPassword = MPD $ stPassword <$> get- setPassword pw = MPD $ modify (\st -> st { stPassword = pw })- getVersion = MPD $ stVersion <$> get---- | Inner state for MPD-data MPDState =- MPDState { stHandle :: Maybe Handle- , stPassword :: String- , stVersion :: (Int, Int, Int)- }---- | A response is either an 'MPDError' or some result.-type Response = Either MPDError---- | The most configurable API for running an MPD action.-withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)-withMPDEx host port pw x = withSocketsDo $- runReaderT (evalStateT (runErrorT . runMPD $ open >> (x <* close)) initState)- (host, port)- where initState = MPDState Nothing pw (0, 0, 0)--mpdOpen :: MPD ()-mpdOpen = MPD $ do- (host, port) <- ask- runMPD close- handle <- liftIO (safeConnectTo host port)- modify (\st -> st { stHandle = handle })- F.forM_ handle (const $ runMPD checkConn >>= flip unless (runMPD close))- where- safeConnectTo host@('/':_) _ =- (Just <$> connectTo "" (UnixSocket host))- `E.catch` (\(_ :: E.SomeException) -> return Nothing)- safeConnectTo host port =- (Just <$> connectTo host (PortNumber $ fromInteger port))- `E.catch` (\(_ :: E.SomeException) -> return Nothing)- checkConn = do- [msg] <- send ""- if "OK MPD" `isPrefixOf` msg- then MPD $ checkVersion $ parseVersion msg- else return False-- checkVersion Nothing = throwError $ Custom "Couldn't determine MPD version"- checkVersion (Just version)- | version < requiredVersion =- throwError $ Custom $ printf- "MPD %s is not supported, upgrade to MPD %s or above!"- (formatVersion version) (formatVersion requiredVersion)- | otherwise = do- modify (\st -> st { stVersion = version })- return True- where- requiredVersion = (0, 15, 0)-- parseVersion = parseTriple '.' parseNum . dropWhile (not . isDigit)-- formatVersion :: (Int, Int, Int) -> String- formatVersion (x, y, z) = printf "%d.%d.%d" x y z---mpdClose :: MPD ()-mpdClose =- MPD $ do- get >>= F.mapM_ (liftIO . sendClose) . stHandle- modify (\st -> st { stHandle = Nothing })- where- sendClose handle =- (hPutStrLn handle "close" >> hReady handle >> hClose handle)- `E.catch` whenEOF (return ())-- whenEOF result err- | isEOFError err = result- | otherwise = ioError err--mpdSend :: String -> MPD [ByteString]-mpdSend str = send' `catchError` handler- where- handler TimedOut = mpdOpen >> send'- handler err = throwError err-- send' = MPD $ get >>= maybe (throwError NoMPD) go . stHandle-- go handle = do- unless (null str) $- liftIO $ U.hPutStrLn handle str >> hFlush handle- liftIO ((Right <$> getLines handle []) `E.catch` (return . Left))- >>= either (\err -> if isEOFError err then- modify (\st -> st { stHandle = Nothing })- >> throwError TimedOut- else liftIO (ioError err))- return-- getLines :: Handle -> [ByteString] -> IO [ByteString]- getLines handle acc = do- l <- B.hGetLine handle- if "OK" `isPrefixOf` l || "ACK" `isPrefixOf` l- then (return . reverse) (l:acc)- else getLines handle (l:acc)-------- Other operations.-----ignore :: (Monad m) => m a -> m ()-ignore x = x >> return ()---- | Kill the server. Obviously, the connection is then invalid.-kill :: (MonadMPD m) => m ()-kill = ignore (send "kill") `catchError` cleanup- where- cleanup e = if e == TimedOut then close else throwError e---- | Send a command to the MPD server and return the result.-getResponse :: (MonadMPD m) => String -> m [ByteString]-getResponse cmd = (send cmd >>= parseResponse) `catchError` sendpw- where- sendpw e@(ACK Auth _) = do- pw <- getPassword- if null pw then throwError e- else send ("password " ++ pw) >>= parseResponse- >> send cmd >>= parseResponse- sendpw e =- throwError e---- Consume response and return a Response.-parseResponse :: (MonadError MPDError m) => [ByteString] -> m [ByteString]-parseResponse xs- | null xs = throwError $ NoMPD- | "ACK" `isPrefixOf` x = throwError $ parseAck x- | otherwise = return $ Prelude.takeWhile ("OK" /=) xs- where- x = head xs---- Turn MPD ACK into the corresponding 'MPDError'-parseAck :: ByteString -> MPDError-parseAck s = ACK ack (UTF8.toString msg)- where- ack = case code of- 2 -> InvalidArgument- 3 -> InvalidPassword- 4 -> Auth- 5 -> UnknownCommand- 50 -> FileNotFound- 51 -> PlaylistMax- 52 -> System- 53 -> PlaylistLoad- 54 -> Busy- 55 -> NotPlaying- 56 -> FileExists- _ -> UnknownACK- (code, _, msg) = splitAck s---- Break an ACK into (error code, current command, message).--- ACKs are of the form:--- ACK [error@command_listNum] {current_command} message_text\n-splitAck :: ByteString -> (Int, ByteString, ByteString)-splitAck s = (read code, cmd, msg)- where- (code, notCode) = between '[' '@' s- (cmd, notCmd) = between '{' '}' notCode- msg = drop 1 $ dropWhile (' ' ==) notCmd-- -- take whatever is between 'f' and 'g'.- between a b xs = let (_, y) = break (== a) xs- in break (== b) (drop 1 y)
− Network/MPD/Core/Class.hs
@@ -1,39 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}---- | Module : Network.MPD.Core.Class--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ The MPD typeclass.--module Network.MPD.Core.Class where--import System.IO (Handle)-import Data.ByteString (ByteString)--import Network.MPD.Core.Error (MPDError)--import Control.Monad.Error (MonadError)--type Password = String---- | A typeclass to allow for multiple implementations of a connection--- to an MPD server.-class (Monad m, MonadError MPDError m) => MonadMPD m where- -- | Open (or re-open) a connection to the MPD server.- open :: m ()- -- | Close the connection.- close :: m ()- -- | Send a string to the server and return its response.- send :: String -> m [ByteString]- -- | Get underlying Handle (or Nothing, if no connection is estabilished)- getHandle :: m (Maybe Handle)- -- | Produce a password to send to the server should it ask for- -- one.- getPassword :: m Password- -- | Alters password to be sent to the server.- setPassword :: Password -> m ()- -- | Get MPD protocol version- getVersion :: m (Int, Int, Int)
− Network/MPD/Core/Error.hs
@@ -1,56 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}---- | Module : Network.MPD.Core.Error--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ MPD errors.--module Network.MPD.Core.Error where--import qualified Control.Exception as E-import Control.Monad.Error (Error(..))-import Data.Typeable---- | The MPDError type is used to signal errors, both from the MPD and--- otherwise.-data MPDError = NoMPD -- ^ MPD not responding- | TimedOut -- ^ The connection timed out- | Unexpected String -- ^ MPD returned an unexpected response.- -- This is a bug, either in the library or- -- in MPD itself.- | Custom String -- ^ Used for misc. errors- | ACK ACKType String -- ^ ACK type and a message from the- -- server- deriving (Eq, Typeable)--instance E.Exception MPDError--instance Show MPDError where- show NoMPD = "Could not connect to MPD"- show TimedOut = "MPD connection timed out"- show (Unexpected s) = "MPD returned an unexpected response: " ++ s- show (Custom s) = s- show (ACK _ s) = s--instance Error MPDError where- noMsg = Custom "An error occurred"- strMsg = Custom---- | Represents various MPD errors (aka. ACKs).-data ACKType = InvalidArgument -- ^ Invalid argument passed (ACK 2)- | InvalidPassword -- ^ Invalid password supplied (ACK 3)- | Auth -- ^ Authentication required (ACK 4)- | UnknownCommand -- ^ Unknown command (ACK 5)- | FileNotFound -- ^ File or directory not found ACK 50)- | PlaylistMax -- ^ Playlist at maximum size (ACK 51)- | System -- ^ A system error (ACK 52)- | PlaylistLoad -- ^ Playlist loading failed (ACK 53)- | Busy -- ^ Update already running (ACK 54)- | NotPlaying -- ^ An operation requiring playback- -- got interrupted (ACK 55)- | FileExists -- ^ File already exists (ACK 56)- | UnknownACK -- ^ An unknown ACK (aka. bug)- deriving Eq
− Network/MPD/Util.hs
@@ -1,113 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Module : Network.MPD.Util--- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010--- License : LGPL (see LICENSE)--- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>--- Stability : alpha------ Utilities.--module Network.MPD.Util (- parseDate, parseIso8601, formatIso8601, parseNum, parseFrac,- parseBool, showBool, breakChar, parseTriple,- toAssoc, toAssocList, splitGroups, read- ) where--import Data.Char (isDigit)-import Data.Time.Format (ParseTime, parseTime, FormatTime, formatTime)-import System.Locale (defaultTimeLocale)--import qualified Prelude-import Prelude hiding (break, take, drop, takeWhile, dropWhile, read, reads)-import Data.ByteString.Char8 (break, take, drop, takeWhile, dropWhile, ByteString)-import qualified Data.ByteString.UTF8 as UTF8-import Data.String---- | Like Prelude.read, but works with ByteString.-read :: Read a => ByteString -> a-read = Prelude.read . UTF8.toString---- | Like Prelude.reads, but works with ByteString.-reads :: Read a => ByteString -> [(a, String)]-reads = Prelude.reads . UTF8.toString---- Break a string by character, removing the separator.-breakChar :: Char -> ByteString -> (ByteString, ByteString)-breakChar c s = let (x, y) = break (== c) s in (x, drop 1 y)---- XXX: need a more robust date parser.--- Parse a date value.--- > parseDate "2008" = Just 2008--- > parseDate "2008-03-01" = Just 2008-parseDate :: ByteString -> Maybe Int-parseDate = parseNum . takeWhile isDigit---- Parse date in iso 8601 format-parseIso8601 :: (ParseTime t) => ByteString -> Maybe t-parseIso8601 = parseTime defaultTimeLocale iso8601Format . UTF8.toString--formatIso8601 :: FormatTime t => t -> String-formatIso8601 = formatTime defaultTimeLocale iso8601Format--iso8601Format :: String-iso8601Format = "%FT%TZ"---- Parse a positive or negative integer value, returning 'Nothing' on failure.-parseNum :: (Read a, Integral a) => ByteString -> Maybe a-parseNum s = do- [(x, "")] <- return (reads s)- return x---- Parse C style floating point value, returning 'Nothing' on failure.-parseFrac :: (Fractional a, Read a) => ByteString -> Maybe a-parseFrac s =- case s of- "nan" -> return $ Prelude.read "NaN"- "inf" -> return $ Prelude.read "Infinity"- "-inf" -> return $ Prelude.read "-Infinity"- _ -> do [(x, "")] <- return $ reads s- return x---- Inverts 'parseBool'.-showBool :: IsString a => Bool -> a--- FIXME: can we change the type to (Bool -> ByteString)?-showBool x = if x then "1" else "0"---- Parse a boolean response value.-parseBool :: ByteString -> Maybe Bool-parseBool s = case take 1 s of- "1" -> Just True- "0" -> Just False- _ -> Nothing---- Break a string into triple.-parseTriple :: Char -> (ByteString -> Maybe a) -> ByteString -> Maybe (a, a, a)-parseTriple c f s = let (u, u') = breakChar c s- (v, w) = breakChar c u' in- case (f u, f v, f w) of- (Just a, Just b, Just c') -> Just (a, b, c')- _ -> Nothing---- Break a string into an key-value pair, separating at the first ':'.-toAssoc :: ByteString -> (ByteString, ByteString)-toAssoc x = (k, dropWhile (== ' ') $ drop 1 v)- where- (k,v) = break (== ':') x--toAssocList :: [ByteString] -> [(ByteString, ByteString)]-toAssocList = map toAssoc---- Takes an association list with recurring keys and groups each cycle of keys--- with their values together. There can be several keys that begin cycles,--- (the elements of the first parameter).-splitGroups :: [ByteString] -> [(ByteString, ByteString)] -> [[(ByteString, ByteString)]]-splitGroups groupHeads = go- where- go [] = []- go (x:xs) =- let- (ys, zs) = Prelude.break isGroupHead xs- in- (x:ys) : go zs-- isGroupHead = (`elem` groupHeads) . fst
README.md view
@@ -1,110 +1,70 @@ # libmpd-haskell: a client library for MPD ## About-libmpd-haskell is a client library for [MPD] written in [Haskell] that-aims to provide a safe and flexible yet consistent and intuitive-interface to MPD's external API.+libmpd-haskell is a pure [Haskell] client library for [MPD], the+music playing daemon. [MPD]: http://www.musicpd.org [Haskell]: http://www.haskell.org ## Getting-* [Latest release]-* `git clone git://github.com/joachifm/libmpd-haskell.git`--[Latest release]: http://hackage.haskell.org/package/libmpd "libmpd-haskell on Hackage"+* [Latest release on Hackage]+* `git clone git://github.com/vimus/libmpd-haskell.git` -## Building-The preferred method of building libmpd-haskell is using [cabal-install], which-takes care of dependency resolution and other minutiae.+[Latest release on Hackage]: http://hackage.haskell.org/package/libmpd "libmpd-haskell on Hackage" -To install libmpd-haskell, simply run:+## Installation+With [cabal-install], do `cd libmpd-haskell && cabal install` [cabal-install]: http://hackage.haskell.org/package/cabal-install -## Running tests-To run the libmpd testsuite, do:--`cabal configure --enable-tests && cabal build && cabal test`--## Compiler support-We try to support the two last major versions of GHC, but only the latest-version (provided by the [haskell-platform]) is actually tested for.--[haskell-platform]: http://www.haskell.org/platform--## MPD API compliance-We try to comply with the latest version of the MPD protocol specification;-any deviation from this is a bug.- ## Usage With GHCi: > :set -XOverloadedStrings > import Network.MPD > withMPD $ lsInfo ""- Right [Left "Tool", Left "Tom Waits",...]+ Right [LsDirectory "Tool", LsDirectory "Tom Waits",...] > withMPD $ add "Tom Waits/Big Time" Right ["Tom Waits/Big Time/01 - 16 Shells from a Thirty-Ought-Six.mp3",...] -## Development--### Getting started-Create the clone thus:--`git clone git://github.com/joachifm/libmpd-haskell.git master`--To pull in new changes from upstream, use:--`git pull origin master`+## MPD API compliance+Any deviation from the latest version of the [MPD protocol reference]+is a bug. -To set up GIT hooks, see `hooks/README` in the source distribution.+## Submitting bug reports+See our [bug tracker]. Test cases are highly appreciated. -### Submitting patches+## Submitting patches To submit a patch, use `git format-patch` and email the resulting file(s) to one of the developers or upload it to the [bug tracker]. -Alternatively you can create your own fork of the [repository] and send a pull-request.+Alternatively you can create your own fork of the [GitHub repository] and+send a pull request. Well-formatted patches are appreciated. New features should have a test case. -### Submitting bug reports-See our [bug tracker]. Test cases are highly appreciated.--### Resources+## Resources * [API documentation]-* [libmpd-haskell mailing list]-* [Protocol reference]+* [MPD protocol reference] * [Using GitHub]-* \#libmpd-haskell @ irc.freenode.net (defunct)+* \#vimus @ irc.freenode.net -[libmpd-haskell mailing list]: http://groups.google.com/group/libmpd-haskell-[bug tracker]: http://github.com/joachifm/libmpd-haskell/issues+[bug tracker]: http://github.com/vimus/libmpd-haskell/issues [GitHub]: http://www.github.com-[repository]: http://www.github.com/joachifm/libmpd-haskell-[API documentation]: http://hackage.haskell.org/packages/archive/libmpd/0.7.2/doc/html/Network-MPD.html-[Protocol reference]: http://www.musicpd.org/doc/protocol/+[GitHub repository]: http://www.github.com/vimus/libmpd-haskell+[API documentation]: http://hackage.haskell.org/packages/archive/libmpd/latest/doc/html/Network-MPD.html+[MPD protocol reference]: http://www.musicpd.org/doc/protocol/ [Using GitHub]: http://help.github.com ## License-LGPL version 2.1 (see LICENSE)--## Contributors (in order of appearance)-Ben Sinclair \<ben.d.sinclair@gmail.com\>--Joachim Fasting \<joachim.fasting@gmail.com\>--gwern0 \<gwern0@gmail.com\>--Daniel Schoepe \<daniel.schoepe@googlemail.com\>--Andrzej Rybczak \<electricityispower@gmail.com\>--Simon Hengel \<sol@typeful.net\>+libmpd-haskell is distributed under the [MIT license]. -Daniel Wagner \<daniel@wagner-home.com\>+[MIT license]: http://www.opensource.org/licenses/MIT -nandykins+## Contributors+See `CONTRIBUTORS` in the source distribution.+Feel free to add yourself to this list if you deem it appropriate+to do so.
+ changelog.md view
@@ -0,0 +1,118 @@+* v0.9.0, 2014-09-21+ - New commands: `deleteRange`, `moveRange`, `playlistInfoRange`,+ `searchAdd`, `searchAddpl`.+ - Fix `playlistId` and `list`+ - Add Mixramp commands+ - Support for MPD 0.17+ - Support for missing metadata keys.+ - Sticker idle events+ - Subscription and message events+ - New applicative interface which allows combining arbitrary commands+ into command lists (sol).+ - Consistent typing for song positions (sol).+ - Command definitions closer to the MPD spec; compound commands+ have been moved to `N.M.C.Extensions`.+ - `Status.{stUpdatingDb,stTime,stBitrate,stVolume` are now `Maybe`+ - `MonadMPD.getHandle` has been removed+ - Re-connect and retry on `ResourceVanished` (e.g., when the+ connection times out).++* v0.8.0, 2012-04-21+ - Use bytestring for wire communication (sol)+ - Increased type safety (sol)+ - Improved memory usage (sol)+ - `lsinfo` supports playlists (nandykins)+ - `idle` now takes a list of subsystems (sol)+ - `currentSong` works when playback is stopped (sol)+ - Fixes failure on songs without associated paths (sol)+ - `LsResult` replaces `EntryType` (nandykins)+ - hspec based testing added to the test-suite+ - More extensive parser testing+ - 'MPDError' now has an 'Exception' instance+ - Lower bound on Cabal bumped to 1.10++* v0.7.2, 2012-02-13+ - Release connections. Reported by Kanisterschleife on GitHub.+ - Some minor internal changes (sol)++* v0.7.1, 2012-02-07+ - Compatible with GHC 7.4.1++* v0.7.0, 2011-11-22+ - Several fixes to the test harness (Simon Hengel)+ - Fixed issue with the (<$>) operator (Simon Hengel)+ - Type safe handling of song IDs (Simon Hengel)+ - Check MPD version on connect (now depends on MPD >= 0.15) (Simon Hengel)+ - Compatibility with GHC 7.2 (Daniel Wagner)++* v0.6.0, 2011-04-01+ - Reverted some changes from 0.5.0 that caused problems,+ most notably the parser improvements have been removed for now.+ - Support for GHC 7+ - Removed support for building against the deprecated base 3 package+ - Added an `Enum' instance for `Metadata'+ - Removed the `old_base' flag++* v0.5.0, 2010-09-08+ - Moved extensions to Network.MPD.Commands.Extensions+ These might be removed in a later version+ - Non-blocking `idle'+ - The API is closer to the MPD spec, by untangling functionality+ - Better MPD API coverage+ - Improved parser implementation, now runs in constant space+ - Constructors of the `Subsystem' type have been renamed+ - Passwords can be changed using `setPassword'+ - The connection handle can be accessed via `getHandle'+ - The version of the MPD server is available via `getVersion'+ - Added support for connecting via unix sockets++* v0.4.2, 2010-08-31+ - Only depend on QuickCheck when building the test target++* v0.4.1, 2010-03-26+ - Fix building test and coverage targets++* v0.4.0, 2010-03-26+ - New maintainer: Joachim Fasting <joachim.fasting@gmail.com>+ - Support QuickCheck 2+ - Better MPD api support+ Should be mostly compatible with mpd 0.16+ - Separated operations on current playlist from those on specific+ playlists+ - Fixed password sending+ - Several minor fixes and cleanups++* v0.3.1, 2008-09-14+ - Now reconnects if MPD closes the connection.++* v0.3.0, 2008-05-06+ - UTF-8 support (now depends on utf8-string package).+ - Fixed corruption by `show' of command parameters.+ - Tidied up `Query' interface.+ - Moved StringConn out of Network.MPD to the tests directory.++* v0.2.1, 2008-04-14+ - Cleaned up libmpd.cabal.++* v0.2.0, 2008-04-14+ - A connection stub for testing purposes.+ - QuickCheck tests for parsing.+ - Partial unit test coverage.+ - Many bug fixes.+ - Precise error handling.+ - Parsing improvements.+ - Code coverage generation.+ - Cabal 1.2 support.+ - Uniform command names.++* v0.1.3, 2007-10-02+ - Bugfixes.++* v0.1.2, 2007-09-29+ - Changed name to libmpd.++* v0.1.1, 2007-09-28+ - Missing files added to the source distribution.++* v0.1, 2007-09-28+ - Initial public release.
libmpd.cabal view
@@ -1,31 +1,29 @@ Name: libmpd-Version: 0.8.0.5+Version: 0.9.0 Synopsis: An MPD client library.-Description: A client library for MPD, the Music Player Daemon- (<http://www.musicpd.org/>).+Description: A client library for MPD, the Music Player Daemon. Category: Network, Sound -License: LGPL+License: MIT License-file: LICENSE-Copyright: Ben Sinclair 2005-2009, Joachim Fasting 2014+Copyright: Ben Sinclair 2005-2009, Joachim Fasting 2012-2014 Author: Ben Sinclair Maintainer: Joachim Fasting <joachim.fasting@gmail.com> Stability: beta-Homepage: http://github.com/joachifm/libmpd-haskell#readme-Bug-reports: http://github.com/joachifm/libmpd-haskell/issues+Homepage: http://github.com/vimus/libmpd-haskell#readme+Bug-reports: http://github.com/vimus/libmpd-haskell/issues -Tested-With: GHC == 7.0.2+Tested-With: GHC ==7.6.3, GHC ==7.8.3 Build-Type: Simple Cabal-Version: >= 1.10 Extra-Source-Files: README.md- NEWS+ changelog.md tests/Arbitrary.hs tests/CommandSpec.hs tests/EnvSpec.hs- tests/Defaults.hs tests/ParserSpec.hs tests/StringConn.hs tests/TestUtil.hs@@ -35,27 +33,43 @@ Source-Repository head type: git- location: https://github.com/joachifm/libmpd-haskell+ location: https://github.com/vimus/libmpd-haskell Library Default-Language: Haskell2010 + Hs-Source-Dirs: src+ Build-Depends:+ -- Platform dependencies base >= 4 && < 5- , mtl >= 2.0 && < 3- , network >= 2.1 && < 2.6- , filepath >= 1.0 && < 1.4- , utf8-string >= 0.3.1 && < 0.4- , old-locale >= 1.0 && < 2.0- , time >= 1.1 && < 2.0- , containers >= 0.3 && < 0.6- , data-default >= 0.4.0 && < 0.6+ , attoparsec >= 0.10.1 && < 1 , bytestring >= 0.9 && < 1- , text >= 0.11- , attoparsec >= 0.10.1 && < 0.13+ , containers >= 0.3 && < 1+ , filepath >= 1 && < 2+ , mtl >= 2.0 && < 3+ , old-locale >= 1 && < 2+ , text >= 0.11 && < 2+ , time >= 1.1 && < 2 + -- Additional dependencies+ , data-default >= 0.4 && < 1+ , network >= 2.1 && < 3+ , utf8-string >= 0.3.1 && < 1+ Exposed-Modules: Network.MPD+ , Network.MPD.Applicative+ , Network.MPD.Applicative.Connection+ , Network.MPD.Applicative.CurrentPlaylist+ , Network.MPD.Applicative.Database+ , Network.MPD.Applicative.Output+ , Network.MPD.Applicative.PlaybackControl+ , Network.MPD.Applicative.PlaybackOptions+ , Network.MPD.Applicative.Reflection+ , Network.MPD.Applicative.Status+ , Network.MPD.Applicative.Stickers+ , Network.MPD.Applicative.StoredPlaylists , Network.MPD.Commands.Extensions , Network.MPD.Core @@ -67,7 +81,18 @@ , Network.MPD.Commands.Parse , Network.MPD.Commands.Query , Network.MPD.Commands.Types- , Network.MPD.Commands.Util+ , Network.MPD.Commands.Status+ , Network.MPD.Commands.PlaybackOptions+ , Network.MPD.Commands.PlaybackControl+ , Network.MPD.Commands.CurrentPlaylist+ , Network.MPD.Commands.StoredPlaylists+ , Network.MPD.Commands.Database+ , Network.MPD.Commands.Stickers+ , Network.MPD.Commands.Connection+ , Network.MPD.Commands.Output+ , Network.MPD.Commands.Reflection+ , Network.MPD.Applicative.Util+ , Network.MPD.Applicative.Internal , Network.MPD.Util ghc-prof-options: -auto-all -prof@@ -76,23 +101,29 @@ Test-Suite specs type: exitcode-stdio-1.0 Default-Language: Haskell2010- Main-Is: tests/Main.hs- Hs-Source-Dirs: . tests- cpp-options: -DTEST+ Main-Is: Main.hs+ Hs-Source-Dirs: src tests+ cpp-options: -DTEST -Wall -Werror ghc-options: -fno-warn-missing-signatures Build-Depends:+ -- Platform dependencies base- , utf8-string+ , attoparsec , bytestring- , old-locale- , network+ , containers+ , filepath , mtl+ , old-locale , text- , containers+ , time++ -- Additional dependencies , data-default+ , network+ , utf8-string++ -- Test dependencies , unix- , time , QuickCheck >= 2.1- , hspec- , HUnit+ , hspec >= 1.3
+ src/Network/MPD.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE CPP #-}++{- |+Module : Network.MPD+Copyright : (c) Joachim Fasting, Simon Hengel 2012+License : MIT++Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+Stability : unstable+Portability : unportable++An MPD client library. MPD is a daemon for playing music that is+controlled over a network socket.++To use the library, do:++> {-# LANGUAGE OverloadedStrings #-}+> import qualified Network.MPD as MPD+-}++module Network.MPD (+ -- * Basic data types+ MonadMPD, MPD, MPDError(..), ACKType(..), Response,+ Host, Port, Password,+ -- * Connections+ withMPD, withMPD_, withMPDEx,+ module Network.MPD.Commands,+#ifdef TEST+ getConnectionSettings, getEnvDefault+#endif+ ) where++import Prelude+import qualified Control.Exception as E+import Network.MPD.Commands+import Network.MPD.Core++import System.Environment (getEnv)+import System.IO.Error (isDoesNotExistError)+import Data.Maybe (listToMaybe)++-- | A wrapper for 'withMPDEx' that uses localhost:6600 as the default+-- host:port, or whatever is found in the environment variables MPD_HOST and+-- MPD_PORT. If MPD_HOST is of the form \"password\@host\" the password+-- will be supplied as well.+--+-- Examples:+--+-- > withMPD $ play Nothing+-- > withMPD $ add_ "tool" >> play Nothing >> currentSong+withMPD :: MPD a -> IO (Response a)+withMPD = withMPD_ Nothing Nothing++-- | Same as `withMPD`, but takes optional arguments that override MPD_HOST and+-- MPD_PORT.+--+-- This is e.g. useful for clients that optionally take @--port@ and @--host@+-- as command line arguments, and fall back to `withMPD`'s defaults if those+-- arguments are not given.+withMPD_ :: Maybe String -- ^ optional override for MPD_HOST+ -> Maybe String -- ^ optional override for MPD_PORT+ -> MPD a -> IO (Response a)+withMPD_ mHost mPort action = do+ settings <- getConnectionSettings mHost mPort+ case settings of+ Right (host, port, pw) -> withMPDEx host port pw action+ Left err -> (return . Left . Custom) err++getConnectionSettings :: Maybe String -> Maybe String -> IO (Either String (Host, Port, Password))+getConnectionSettings mHost mPort = do+ (host, pw) <- parseHost `fmap`+ maybe (getEnvDefault "MPD_HOST" "localhost") return mHost+ port <- maybe (getEnvDefault "MPD_PORT" "6600") return mPort+ case maybeRead port of+ Just p -> (return . Right) (host, p, pw)+ Nothing -> (return . Left) (show port ++ " is not a valid port!")+ where+ parseHost s = case breakChar '@' s of+ (host, "") -> (host, "")+ (pw, host) -> (host, pw)++getEnvDefault :: String -> String -> IO String+getEnvDefault x dflt =+ E.catch (getEnv x) (\e -> if isDoesNotExistError e+ then return dflt else ioError e)++-- Break a string by character, removing the separator.+breakChar :: Char -> String -> (String, String)+breakChar c s = let (x, y) = break (== c) s in (x, drop 1 y)++maybeRead :: Read a => String -> Maybe a+maybeRead = fmap fst . listToMaybe . reads
+ src/Network/MPD/Applicative.hs view
@@ -0,0 +1,47 @@+module Network.MPD.Applicative (+ Command+, runCommand++-- * Querying MPD's status+, module Network.MPD.Applicative.Status++-- * Playback options+, module Network.MPD.Applicative.PlaybackOptions++-- * Controlling playback+, module Network.MPD.Applicative.PlaybackControl++-- * The current playlist+, module Network.MPD.Applicative.CurrentPlaylist++-- * Stored playlists+, module Network.MPD.Applicative.StoredPlaylists++-- * The music database+, module Network.MPD.Applicative.Database++-- * Stickers+, module Network.MPD.Applicative.Stickers++-- * Connection settings+, module Network.MPD.Applicative.Connection++-- * Audio output devices+, module Network.MPD.Applicative.Output++-- * Reflection+, module Network.MPD.Applicative.Reflection+) where++import Network.MPD.Applicative.Internal++import Network.MPD.Applicative.Connection+import Network.MPD.Applicative.CurrentPlaylist+import Network.MPD.Applicative.Database+import Network.MPD.Applicative.Output+import Network.MPD.Applicative.PlaybackControl+import Network.MPD.Applicative.PlaybackOptions+import Network.MPD.Applicative.Reflection+import Network.MPD.Applicative.Status+import Network.MPD.Applicative.Stickers+import Network.MPD.Applicative.StoredPlaylists
+ src/Network/MPD/Applicative/Connection.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Connection+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Connection settings.+-}++module Network.MPD.Applicative.Connection+ ( password+ , ping+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Core++-- | Authenticate session. The password is sent in plain text.+password :: Password -> Command ()+password pwd = Command emptyResponse ["password " ++ pwd]++-- | Ping daemon.+ping :: Command ()+ping = Command emptyResponse ["ping"]
+ src/Network/MPD/Applicative/CurrentPlaylist.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.CurrentPlaylist+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++The current playlist.+-}++module Network.MPD.Applicative.CurrentPlaylist+ ( add+ , addId+ , clear+ , delete+ , deleteRange+ , deleteId+ , move+ , moveId+ , moveRange+ , playlistFind+ , playlistInfo+ , playlistInfoRange+ , playlistId+ , playlistSearch+ , plChanges+ , plChangesPosId+ , shuffle+ , swap+ , swapId+ ) where++import Network.MPD.Commands.Arg hiding (Command)+import qualified Network.MPD.Commands.Arg as Arg+import Network.MPD.Util+import Network.MPD.Commands.Query+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Types+import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util++-- | Add a song (or a whole directory) to the current playlist.+add :: Path -> Command ()+add path = Command emptyResponse ["add" <@> path]++-- | Add a song (non-recursively) and return its id.+addId :: Path -> Maybe Position -> Command Id+addId path pos = Command p c+ where+ c = ["addid" <@> path <++> pos]+ p = do+ r <- getResponse+ case toAssocList r of+ [("Id", n)] -> maybe (unexpected r) (return . Id) (parseNum n)+ _ -> unexpected r++-- | Clear the current playlist.+clear :: Command ()+clear = Command emptyResponse ["clear"]++-- | Delete song at the given playlist position.+delete :: Position -> Command ()+delete pos = Command emptyResponse ["delete" <@> pos]++-- XXX: this does not exist in the monadic version+-- | Delete a range of songs from the playlist.+deleteRange :: (Position, Position) -> Command ()+deleteRange range = Command emptyResponse ["delete" <@> range]++-- | Delete song by id.+deleteId :: Id -> Command ()+deleteId i = Command emptyResponse ["deleteid" <@> i]++-- | Move song from one position to another.+move :: Position -> Position -> Command ()+move pos to = Command emptyResponse ["move" <@> pos <++> to]++-- | Move a range of songs.+moveRange :: (Position, Position) -> Position -> Command ()+moveRange range to = Command emptyResponse ["move" <@> range <++> to]++-- | Move song id to position.+-- If the position is negative, it is relative to the current song.+moveId :: Id -> Position -> Command ()+moveId i to = Command emptyResponse ["moveid" <@> i <++> to]++-- Note: 'playlist' deliberately not defined here++-- Internal helper for playlist* commands+playlist' :: MPDArg a => Arg.Command -> a -> Command [Song]+playlist' cmd q = Command (liftParser takeSongs) [cmd <@> q]++-- | Find songs in current playlist with strict matching.+playlistFind :: Query -> Command [Song]+playlistFind = playlist' "playlistfind"++-- | Get song metadata for all items in the current playlist.+-- Optionally restrict listing the song at the given position.+playlistInfo :: Maybe Position -> Command [Song]+playlistInfo = playlist' "playlistinfo"++-- | Like 'playlistInfo' but can restrict listing to a range+-- of songs.+playlistInfoRange :: Maybe (Position, Position) -> Command [Song]+playlistInfoRange = playlist' "playlistinfo"++-- | Get song metadata for all items in the current playlist.+-- Optionally restrict selection to a single song id.+playlistId :: Maybe Id -> Command [Song]+playlistId = playlist' "playlistid"++-- | Search case-insensitively for partial matches in current playlist.+playlistSearch :: Query -> Command [Song]+playlistSearch = playlist' "playlistsearch"++-- | Get song metadata for items that have changed in the playlist since+-- the given playlist version.+plChanges :: Integer -> Command [Song]+plChanges = playlist' "plchanges"++-- | Get positions and ids of songs that have changed in the playlist+-- since the given playlist version.+plChangesPosId :: Integer -> Command [(Position, Id)]+plChangesPosId ver = Command p ["plchangesposid" <@> ver]+ where+ -- XXX: possibly suboptimal definition+ p :: Parser [(Position, Id)]+ p = liftParser $ mapM f . splitGroups ["cpos"] . toAssocList+ f xs | [("cpos", x), ("Id", y)] <- xs+ , Just (x', y') <- pair parseNum (x, y)+ = Right (x', Id y')+ | otherwise = Left ""++-- | Shuffle the current playlist.+-- Optionally restrict to a range of songs.+shuffle :: Maybe (Position, Position) -> Command ()+shuffle mbRange = Command emptyResponse ["shuffle" <@> mbRange]++-- | Swap songs by position.+swap :: Position -> Position -> Command ()+swap pos1 pos2 = Command emptyResponse ["swap" <@> pos1 <++> pos2]++-- | Swap songs by id.+swapId :: Id -> Id -> Command ()+swapId id1 id2 = Command emptyResponse ["swapid" <@> id1 <++> id2]
+ src/Network/MPD/Applicative/Database.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Database+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++The music database.+-}++module Network.MPD.Applicative.Database where++import Control.Applicative++import qualified Network.MPD.Commands.Arg as Arg+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Query+import Network.MPD.Util+import Network.MPD.Commands.Types+import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util++-- | Get a count of songs and their total playtime that exactly match the+-- query.+count :: Query -> Command Count+count q = Command (liftParser parseCount) ["count" <@> q]++-- | Find songs matching the query exactly.+find :: Query -> Command [Song]+find q = Command p ["find" <@> q]+ where+ p :: Parser [Song]+ p = liftParser takeSongs++-- | Like 'find' but adds the results to the current playlist.+findAdd :: Query -> Command ()+findAdd q = Command emptyResponse ["findadd" <@> q]++-- | Lists all tags of the specified type.+--+-- Note that the optional artist value is only ever used if the+-- metadata type is 'Album', and is then taken to mean that the albums+-- by that artist be listed.+list :: Metadata -> Maybe Artist -> Command [Value]+list m q = Command p c+ where+ p = map Value . takeValues <$> getResponse+ c = case m of+ Album -> ["list Album" <@> q]+ _ -> ["list" <@> m]++-- | List all songs and directories in a database path.+listAll :: Path -> Command [Path]+listAll path = Command p ["listall" <@> path]+ where+ p :: Parser [Path]+ p = map (Path . snd) . filter ((== "file") . fst)+ . toAssocList <$> getResponse++-- Internal helper+lsInfo' :: Arg.Command -> Path -> Command [LsResult]+lsInfo' cmd path = Command p [cmd <@> path]+ where+ p :: Parser [LsResult]+ p = liftParser takeEntries++-- | Same as 'listAll' but also returns metadata.+listAllInfo :: Path -> Command [LsResult]+listAllInfo = lsInfo' "listallinfo"++-- | List the contents of a database directory.+lsInfo :: Path -> Command [LsResult]+lsInfo = lsInfo' "lsinfo"++-- | Like 'find' but with inexact matching.+search :: Query -> Command [Song]+search q = Command p ["search" <@> q]+ where+ p :: Parser [Song]+ p = liftParser takeSongs++-- | Like 'search' but adds the results to the current playlist.+--+-- Since MPD 0.17.+searchAdd :: Query -> Command ()+searchAdd q = Command emptyResponse ["searchadd" <@> q]++-- | Like 'searchAdd' but adds results to the named playlist.+--+-- Since MPD 0.17.+searchAddPl :: PlaylistName -> Query -> Command ()+searchAddPl pl q = Command emptyResponse ["searchaddpl" <@> pl <++> q]++-- | Update the music database.+-- If no path is supplied, the entire database is updated.+update :: Maybe Path -> Command Integer+update = update_ "update"++-- | Like 'update' but also rescan unmodified files.+rescan :: Maybe Path -> Command Integer+rescan = update_ "rescan"++-- A helper for 'update' and 'rescan.+update_ :: Arg.Command -> Maybe Path -> Command Integer+update_ cmd mPath = Command p [cmd <@> mPath]+ where+ p :: Parser Integer+ p = do+ r <- getResponse+ case toAssocList r of+ [("updating_db", id_)] -> maybe (unexpected r)+ return+ (parseNum id_)+ _ -> unexpected r
+ src/Network/MPD/Applicative/Internal.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++{- |+Module : Network.MPD.Applicative.Internal+Copyright : (c) Simon Hengel 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Applicative MPD command interface.++This allows us to combine commands into command lists, as in++> (,,) <$> currentSong <*> stats <*> status++where the requests are automatically combined into a command list and+the result of each command passed to the consumer.+-}++module Network.MPD.Applicative.Internal+ ( Parser(..)+ , liftParser+ , getResponse+ , emptyResponse+ , unexpected+ , Command(..)+ , runCommand+ ) where++import Control.Applicative+import Control.Monad+import Data.ByteString.Char8 (ByteString)++import Network.MPD.Core hiding (getResponse)+import qualified Network.MPD.Core as Core+import Control.Monad.Error++-- | A line-oriented parser that returns a value along with any remaining input.+newtype Parser a+ = Parser { runParser :: [ByteString] -> Either String (a, [ByteString]) }+ deriving Functor++instance Monad Parser where+ fail = Parser . const . Left+ return a = Parser $ \input -> Right (a, input)+ p1 >>= p2 = Parser $ \input -> runParser p1 input >>= uncurry (runParser . p2)++instance Applicative Parser where+ pure = return+ (<*>) = ap++-- | Convert a regular parser.+liftParser :: ([ByteString] -> Either String a) -> Parser a+liftParser p = Parser $ \input -> case break (== "list_OK") input of+ (xs, ys) -> fmap (, drop 1 ys) (p xs)++-- | Return everything until the next "list_OK".+getResponse :: Parser [ByteString]+getResponse = Parser $ \input -> case break (== "list_OK") input of+ (xs, ys) -> Right (xs, drop 1 ys)++-- | For commands returning an empty response.+emptyResponse :: Parser ()+emptyResponse = do+ r <- getResponse+ unless (null r) $+ unexpected r++-- | Fail with unexpected response.+unexpected :: [ByteString] -> Parser a+unexpected = fail . ("unexpected Response: " ++) . show++-- | A compound command, comprising a parser for the responses and a+-- combined request of an arbitrary number of commands.+data Command a = Command {+ commandParser :: Parser a+ , commandRequest :: [String]+ } deriving Functor++instance Applicative Command where+ pure a = Command (pure a) []+ (Command p1 c1) <*> (Command p2 c2) = Command (p1 <*> p2) (c1 ++ c2)++-- | Execute a 'Command'.+runCommand :: MonadMPD m => Command a -> m a+runCommand (Command p c) = do+ r <- Core.getResponse command+ case runParser p r of+ Left err -> throwError (Unexpected err)+ Right (a, []) -> return a+ Right (_, xs) -> throwError (Unexpected $ "superfluous input: " ++ show xs)+ where+ command = case c of+ [x] -> x+ xs -> unlines ("command_list_ok_begin" : xs)+ ++ "command_list_end"
+ src/Network/MPD/Applicative/Output.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Output+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Audio output devices.+-}++module Network.MPD.Applicative.Output+ ( disableOutput+ , enableOutput+ , outputs+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Types++-- | Turn off output.+disableOutput :: Int -> Command ()+disableOutput n = Command emptyResponse ["disableoutput" <@> n]++-- | Turn on output.+enableOutput :: Int -> Command ()+enableOutput n = Command emptyResponse ["enableoutput" <@> n]++-- | Get information about all available output devices.+outputs :: Command [Device]+outputs = Command (liftParser parseOutputs) ["outputs"]
+ src/Network/MPD/Applicative/PlaybackControl.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.PlaybackControl+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Controlling playback.+-}++module Network.MPD.Applicative.PlaybackControl+ ( next+ , pause+ , play+ , playId+ , previous+ , seek+ , seekId+ , stop+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Types++-- | Play next song in the playlist.+next :: Command ()+next = Command emptyResponse ["next"]++-- | Toggle pause.+pause :: Bool -> Command ()+pause f = Command emptyResponse ["pause" <@> f]++-- | Begin playback (optionally at a specific position).+play :: Maybe Position -> Command ()+play mbPos = Command emptyResponse c+ where+ c = return $ maybe "play" ("play" <@>) mbPos++-- | Begin playback at the specified song id.+playId :: Id -> Command ()+playId id' = Command emptyResponse ["playid" <@> id']++-- | Play previous song.+previous :: Command ()+previous = Command emptyResponse ["previous"]++-- | Seek to time in the song at the given position.+seek :: Position -> Seconds -> Command ()+seek pos time = Command emptyResponse ["seek" <@> pos <++> time]++-- | Seek to time in the song with the given id.+seekId :: Id -> Seconds -> Command ()+seekId id' time = Command emptyResponse ["seekid" <@> id' <++> time]++-- | Stop playback.+stop :: Command ()+stop = Command emptyResponse ["stop"]
+ src/Network/MPD/Applicative/PlaybackOptions.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.PlaybackOptions+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Playback options+-}++module Network.MPD.Applicative.PlaybackOptions+ ( consume+ , crossfade+ , random+ , repeat+ , setVolume+ , single+ , replayGainMode+ , replayGainStatus+ , mixrampDb+ , mixrampDelay+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Types+import Network.MPD.Util (toAssocList)++import Control.Applicative+import Prelude hiding (repeat)++-- | Toggle consume mode.+consume :: Bool -> Command ()+consume f = Command emptyResponse ["consume" <@> f]++-- | Set crossfading between songs.+crossfade :: Seconds -> Command ()+crossfade secs = Command emptyResponse ["crossfade" <@> secs]++-- | Toggle random mode.+random :: Bool -> Command ()+random f = Command emptyResponse ["random" <@> f]++-- | Toggle repeat mode.+repeat :: Bool -> Command ()+repeat f = Command emptyResponse ["repeat" <@> f]++-- | Set volume in percent.+setVolume :: Int -> Command ()+setVolume vol = Command emptyResponse ["setvol" <@> vol]++-- | Toggle single mode.+single :: Bool -> Command ()+single f = Command emptyResponse ["single" <@> f]++-- | Set replay gain mode.+replayGainMode :: ReplayGainMode -> Command ()+replayGainMode f = Command emptyResponse ["replay_gain_mode" <@> f]++-- | Get replay gain status: option name and its value.+replayGainStatus :: Command [(String, String)]+replayGainStatus = Command p ["replay_gain_status"]+ where+ p = map decodePair . toAssocList <$> getResponse++-- | Set MixRamp overlap threshold.+-- 0dB is the normalized maximum value; use negative values to adjust it.+--+-- Songs must have MixRamp tags set by an external tool for this to+-- work; crossfading is used if no tags are present.+mixrampDb :: Decibels -> Command ()+mixrampDb db = Command emptyResponse ["mixrampdb" <@> db]++-- | Additional time subtracted from the overlap calculated by+-- 'mixrampDb'.+-- "NaN" disables MixRamp overlapping and reverts to crossfading.+mixrampDelay :: Seconds -> Command ()+mixrampDelay sec = Command emptyResponse ["mixrampdelay" <@> sec]
+ src/Network/MPD/Applicative/Reflection.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Reflection+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Reflection.+-}++module Network.MPD.Applicative.Reflection+ ( commands+ , notCommands+ , tagTypes+ , urlHandlers+ , decoders+ , config+ ) where++import Network.MPD.Util+import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util++import Control.Applicative+import Prelude hiding (repeat, read)++import qualified Data.ByteString.UTF8 as UTF8++-- | Get a list of available commands.+commands :: Command [String]+commands = Command p ["commands"]+ where+ p = map UTF8.toString . takeValues <$> getResponse++-- | Get a list of unavailable commands (i.e., commands that require+-- an authenticated session).+notCommands :: Command [String]+notCommands = Command p ["notcommands"]+ where+ p = map UTF8.toString . takeValues <$> getResponse++-- | Get a list of available song metadata.+tagTypes :: Command [String]+tagTypes = Command p ["tagtypes"]+ where+ p = map UTF8.toString . takeValues <$> getResponse++-- | Get a list of available URL handlers.+urlHandlers :: Command [String]+urlHandlers = Command p ["urlhandlers"]+ where+ p = map UTF8.toString . takeValues <$> getResponse++-- | Get a list of available decoder plugins, with their supported+-- suffixes and MIME types.+decoders :: Command [(String, [(String, String)])]+decoders = Command p ["decoders"]+ where+ p = takeDecoders . toAssocList <$> getResponse++ takeDecoders [] = []+ takeDecoders ((_, m):xs) =+ let (info, rest) = break ((==) "plugin" . fst) xs+ in (UTF8.toString m, map decodePair info) : takeDecoders rest++-- | Get configuration values of interest to a client.+--+-- Note: only permitted for clients connected via a unix domain+-- socket (aka \"local clients\").+config :: Command [(String, String)]+config = Command p ["config"]+ where+ p = map (\(k, v) -> (UTF8.toString k, UTF8.toString v)) . toAssocList <$> getResponse
+ src/Network/MPD/Applicative/Status.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Status+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Querying MPD's status.+-}++module Network.MPD.Applicative.Status+ ( clearError+ , currentSong+ , idle+ , noidle+ , status+ , stats+ ) where+import Control.Monad+import Control.Arrow ((***))++import Network.MPD.Util+import Network.MPD.Applicative.Internal+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Types++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.UTF8 as UTF8++-- | Clear current error message in status.+clearError :: Command ()+clearError = Command emptyResponse ["clearerror"]++-- | Song metadata for currently playing song, if any.+currentSong :: Command (Maybe Song)+currentSong = Command (liftParser parseMaybeSong) ["currentsong"]++takeSubsystems :: [ByteString] -> Either String [Subsystem]+takeSubsystems = mapM f . toAssocList+ where+ f :: (ByteString, ByteString) -> Either String Subsystem+ f ("changed", system) =+ case system of+ "database" -> Right DatabaseS+ "update" -> Right UpdateS+ "stored_playlist" -> Right StoredPlaylistS+ "playlist" -> Right PlaylistS+ "player" -> Right PlayerS+ "mixer" -> Right MixerS+ "output" -> Right OutputS+ "options" -> Right OptionsS+ k -> Left ("Unknown subsystem: " ++ UTF8.toString k)+ f x = Left ("idle: Unexpected " ++ show x)++-- | Wait until there is noteworthy change in one or more of MPD's+-- subsystems.+-- When active, only 'noidle' commands are allowed.+idle :: [Subsystem] -> Command [Subsystem]+idle ss = Command (liftParser takeSubsystems) c+ where+ c = ["idle" <@> foldr (<++>) (Args []) ss]++-- | Cancel an 'idle' request.+noidle :: Command ()+noidle = Command emptyResponse ["noidle"]++-- | Get database statistics.+stats :: Command Stats+stats = Command (liftParser parseStats) ["stats"]++-- | Get the current status of the player.+status :: Command Status+status = Command (liftParser parseStatus) ["status"]+ where+ -- Builds a 'Status' instance from an assoc. list.+ parseStatus :: [ByteString] -> Either String Status+ parseStatus = foldM go def . toAssocList+ where+ go a p@(k, v) = case k of+ "volume" -> vol $ \x -> a { stVolume = x }+ "repeat" -> bool $ \x -> a { stRepeat = x }+ "random" -> bool $ \x -> a { stRandom = x }+ "single" -> bool $ \x -> a { stSingle = x }+ "consume" -> bool $ \x -> a { stConsume = x }+ "playlist" -> num $ \x -> a { stPlaylistVersion = x }+ "playlistlength" -> num $ \x -> a { stPlaylistLength = x }+ "state" -> state $ \x -> a { stState = x }+ "song" -> int $ \x -> a { stSongPos = Just x }+ "songid" -> int $ \x -> a { stSongID = Just $ Id x }+ "nextsong" -> int $ \x -> a { stNextSongPos = Just x }+ "nextsongid" -> int $ \x -> a { stNextSongID = Just $ Id x }+ "time" -> time $ \x -> a { stTime = Just x }+ "elapsed" -> frac $ \x -> a { stTime = fmap ((,) x . snd) (stTime a) }+ "bitrate" -> int $ \x -> a { stBitrate = Just x }+ "xfade" -> num $ \x -> a { stXFadeWidth = x }+ "mixrampdb" -> frac $ \x -> a { stMixRampdB = x }+ "mixrampdelay" -> frac $ \x -> a { stMixRampDelay = x }+ "audio" -> audio $ \x -> a { stAudio = x }+ "updating_db" -> num $ \x -> a { stUpdatingDb = Just x }+ "error" -> Right a { stError = Just (UTF8.toString v) }+ _ -> unexpectedPair+ where+ unexpectedPair = Left ("unexpected key-value pair: " ++ show p)+ int f = maybe unexpectedPair (Right . f) (parseNum v :: Maybe Int)+ num f = maybe unexpectedPair (Right . f) (parseNum v)+ bool f = maybe unexpectedPair (Right . f) (parseBool v)+ frac f = maybe unexpectedPair (Right . f) (parseFrac v)++ -- This is sometimes "audio: 0:?:0", so we ignore any parse+ -- errors.+ audio f = Right $ maybe a f (parseTriple ':' parseNum v)++ time f = case parseFrac *** parseNum $ breakChar ':' v of+ (Just a_, Just b) -> (Right . f) (a_, b)+ _ -> unexpectedPair++ state f = case v of+ "play" -> (Right . f) Playing+ "pause" -> (Right . f) Paused+ "stop" -> (Right . f) Stopped+ _ -> unexpectedPair++ -- A volume of -1 indicates an audio backend w/o a mixer+ vol f = case parseNum v of+ Nothing -> unexpectedPair -- does it really make sense to fail here? when does this occur?+ Just v' -> (Right . f) (g v')+ where g n | n < 0 = Nothing+ | otherwise = Just (n::Int)
+ src/Network/MPD/Applicative/Stickers.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.Stickers+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Stickers.+-}++module Network.MPD.Applicative.Stickers+ ( stickerGet+ , stickerSet+ , stickerDelete+ , stickerList+ , stickerFind+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Types+import Network.MPD.Util++import Control.Applicative++import qualified Data.ByteString.UTF8 as UTF8++-- | Read sticker value for the object specified.+stickerGet :: ObjectType -> String -> String -> Command [String]+stickerGet typ uri name = Command p c+ where+ p :: Parser [String]+ p = map UTF8.toString . takeValues <$> getResponse++ c = ["sticker get" <@> typ <++> uri <++> name]++-- | Add sticker value to the object. Will overwrite existing stickers+-- with the same name.+stickerSet :: ObjectType -> String -> String -> String -> Command ()+stickerSet typ uri name value = Command emptyResponse c+ where+ c = ["sticker set" <@> typ <++> uri <++> name <++> value]++-- | Delete a sticker value from the object. If no sticker name is+-- given, all sticker values attached to the object are deleted.+stickerDelete :: ObjectType -> String -> String -> Command ()+stickerDelete typ uri name = Command emptyResponse c+ where+ c = ["sticker delete" <@> typ <++> uri <++> name]++-- | List stickers for the object.+stickerList :: ObjectType -> String -> Command [(String, String)]+stickerList typ uri = Command p c+ where+ p = map decodePair . toAssocList <$> getResponse++ c = ["sticker list" <@> typ <++> uri]++-- | Search the sticker database for stickers with the specified name,+-- below the specified directory.+stickerFind :: ObjectType -> String -> String -> Command [(String, String)]+stickerFind typ uri name = Command p c+ where+ p = map decodePair . toAssocList <$> getResponse++ c = ["sticker find" <@> typ <++> uri <++> name]
+ src/Network/MPD/Applicative/StoredPlaylists.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Applicative.StoredPlaylists+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Stored playlists.+-}++module Network.MPD.Applicative.StoredPlaylists+ ( listPlaylist+ , listPlaylistInfo+ , listPlaylists+ , load+ , playlistAdd+ , playlistClear+ , playlistDelete+ , playlistMove+ , rename+ , rm+ , save+ ) where++import Network.MPD.Applicative.Internal+import Network.MPD.Applicative.Util+import Network.MPD.Commands.Arg hiding (Command)+import Network.MPD.Commands.Types+import Network.MPD.Util++import Control.Applicative++-- | List song items in the playlist.+listPlaylist :: PlaylistName -> Command [Path]+listPlaylist plName = Command p ["listplaylist" <@> plName]+ where+ p = map Path . takeValues <$> getResponse++-- | List song items in the playlist with metadata.+listPlaylistInfo :: PlaylistName -> Command [Song]+listPlaylistInfo plName =+ Command (liftParser takeSongs) ["listplaylistinfo" <@> plName]++-- | Get a list of stored playlists.+listPlaylists :: Command [PlaylistName]+listPlaylists = Command p ["listplaylists"]+ where+ p = map PlaylistName . go [] . toAssocList <$> getResponse++ -- XXX: need to fail gracefully here+ -- After each playlist name we get a timestamp+ go acc [] = acc+ go acc ((_, b):_:xs) = go (b : acc) xs+ go _ _ = error "listPlaylists: bug"++-- | Load playlist into the current queue.+load :: PlaylistName -> Command ()+load plName = Command emptyResponse ["load" <@> plName]++-- | Add a database path to the named playlist.+playlistAdd :: PlaylistName -> Path -> Command ()+playlistAdd plName path =+ Command emptyResponse ["playlistadd" <@> plName <++> path]++-- | Clear the playlist.+playlistClear :: PlaylistName -> Command ()+playlistClear plName = Command emptyResponse ["playlistclear" <@> plName]++-- | Delete the item at the given position from the playlist.+playlistDelete :: PlaylistName -> Position -> Command ()+playlistDelete name pos =+ Command emptyResponse ["playlistdelete" <@> name <++> pos]++-- | Move a song to a new position within the playlist.+playlistMove :: PlaylistName -> Id -> Position -> Command ()+playlistMove name from to =+ Command emptyResponse ["playlistmove" <@> name <++> from <++> to]++-- | Rename the playlist.+rename :: PlaylistName -> PlaylistName -> Command ()+rename plName new = Command emptyResponse ["rename" <@> plName <++> new]++-- | Remove the playlist.+rm :: PlaylistName -> Command ()+rm plName = Command emptyResponse ["rm" <@> plName]++-- | Save current queue to the named playlist.+save :: PlaylistName -> Command ()+save plName = Command emptyResponse ["save" <@> plName]
+ src/Network/MPD/Applicative/Util.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.MPD.Applicative.Util where++import Network.MPD.Commands.Parse+import Network.MPD.Commands.Types+import Network.MPD.Util++import Control.Monad (liftM)++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.UTF8 as UTF8++-- Separate the result of an lsinfo\/listallinfo call into directories,+-- playlists, and songs.+takeEntries :: [ByteString] -> Either String [LsResult]+takeEntries = mapM toEntry . splitGroups groupHeads . toAssocList+ where+ toEntry xs@(("file",_):_) = LsSong `liftM` parseSong xs+ toEntry (("directory",d):_) = (return . LsDirectory . Path) d+ toEntry (("playlist",pl):_) = (return . LsPlaylist . PlaylistName) pl+ toEntry _ = error "takeEntries: splitGroups is broken"+ groupHeads = ["file", "directory", "playlist"]++takeSongs :: [ByteString] -> Either String [Song]+takeSongs = mapM parseSong . splitGroups ["file"] . toAssocList++-- Run 'toAssocList' and return only the values.+takeValues :: [ByteString] -> [ByteString]+takeValues = snd . unzip . toAssocList++-- an internal helper function+decodePair :: (ByteString, ByteString) -> (String, String)+decodePair (x, y) = (UTF8.toString x, UTF8.toString y)
+ src/Network/MPD/Commands.hs view
@@ -0,0 +1,68 @@+{- |+Module : Network.MPD.Commands+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Interface to the user commands supported by MPD.+-}++module Network.MPD.Commands (+ module Network.MPD.Commands.Query++ , ToString(..)+ , Artist+ , Album+ , Title+ , PlaylistName(..)+ , Path+ , Metadata(..)+ , Value+ , ObjectType(..)+ , Seconds+ , Decibels+ , State(..)+ , Subsystem(..)+ , ReplayGainMode(..)+ , Count(..)+ , LsResult(..)+ , Device(..)+ , Song(..)+ , Position+ , Id(..)+ , sgGetTag+ , sgAddTag+ , Stats(..)+ , Status(..)+ , def++ , module Network.MPD.Commands.Status+ , module Network.MPD.Commands.PlaybackOptions+ , module Network.MPD.Commands.PlaybackControl+ , module Network.MPD.Commands.CurrentPlaylist+ , module Network.MPD.Commands.StoredPlaylists+ , module Network.MPD.Commands.Database+ , module Network.MPD.Commands.Stickers+ , module Network.MPD.Commands.Connection+ , module Network.MPD.Commands.Output+ , module Network.MPD.Commands.Reflection+ , module Network.MPD.Commands.ClientToClient+ ) where++import Network.MPD.Commands.Query+import Network.MPD.Commands.Types++import Network.MPD.Commands.Status+import Network.MPD.Commands.PlaybackOptions+import Network.MPD.Commands.PlaybackControl+import Network.MPD.Commands.CurrentPlaylist+import Network.MPD.Commands.StoredPlaylists+import Network.MPD.Commands.Database+import Network.MPD.Commands.Stickers+import Network.MPD.Commands.Connection+import Network.MPD.Commands.Output+import Network.MPD.Commands.Reflection+import Network.MPD.Commands.ClientToClient
+ src/Network/MPD/Commands/Arg.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, GeneralizedNewtypeDeriving #-}++{- |+Module : Network.MPD.Commands.Arg+Copyright : (c) Joachim Fasting, Simon Hengel 2012+License : MIT++Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+Stability : alpha+Portability : unportable++Prepare command arguments.+-}++module Network.MPD.Commands.Arg (Command, Args(..), MPDArg(..), (<++>), (<@>)) where++import Network.MPD.Util (showBool)++import Data.ByteString (ByteString)+import qualified Data.ByteString.UTF8 as UTF8+import Data.String++-- | Arguments for getResponse are accumulated as strings in values of+-- this type after being converted from whatever type (an instance of+-- MPDArg) they were to begin with.+newtype Args = Args [String]+ deriving Show++-- | A uniform interface for argument preparation+-- The basic idea is that one should be able+-- to magically prepare an argument for use with+-- an MPD command, without necessarily knowing/\caring+-- how it needs to be represented internally.+class Show a => MPDArg a where+ prep :: a -> Args+ -- Note that because of this, we almost+ -- never have to actually provide+ -- an implementation of 'prep'+ prep = Args . return . show++-- | Groups together arguments to getResponse.+infixl 3 <++>+(<++>) :: (MPDArg a, MPDArg b) => a -> b -> Args+x <++> y = Args $ xs ++ ys+ where Args xs = prep x+ Args ys = prep y++newtype Command = Command String+ deriving IsString++-- | Converts a command name and a string of arguments into the string+-- to hand to getResponse.+infix 2 <@>+(<@>) :: (MPDArg a) => Command -> a -> String+Command x <@> y = unwords $ x : filter (not . null) y'+ where Args y' = prep y++instance MPDArg Args where prep = id++instance MPDArg String where+ -- We do this to avoid mangling+ -- non-ascii characters with 'show'+ prep x = Args ['"' : x ++ "\""]++instance MPDArg ByteString where+ prep = prep . UTF8.toString++instance (MPDArg a) => MPDArg (Maybe a) where+ prep Nothing = Args []+ prep (Just x) = prep x++instance (MPDArg a, MPDArg b) => MPDArg (a, b) where+ prep (x, y) = Args [show x ++ ":" ++ show y]++instance MPDArg Int+instance MPDArg Integer+instance MPDArg Bool where prep = Args . return . showBool
+ src/Network/MPD/Commands/Connection.hs view
@@ -0,0 +1,31 @@+{- |+Module : Network.MPD.Commands.Connection+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Connection settings.+-}++module Network.MPD.Commands.Connection+ ( password+ , ping+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Connection as A+import Network.MPD.Core++-- XXX should the password be quoted? Change "++" to "<@>" if so. If+-- it should, it also needs to be fixed in N.M.Core.+-- | Send password to server to authenticate session.+-- Password is sent as plain text.+password :: MonadMPD m => String -> m ()+password = A.runCommand . A.password++-- | Check that the server is still responding.+ping :: MonadMPD m => m ()+ping = A.runCommand A.ping
+ src/Network/MPD/Commands/CurrentPlaylist.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.CurrentPlaylist+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++The current playlist.+-}++module Network.MPD.Commands.CurrentPlaylist+ ( addId+ , add+ , clear+ , delete+ , deleteId+ , move+ , moveId+ , playlist+ , playlistFind+ , playlistInfo+ , playlistInfoRange+ , playlistId+ , playlistSearch+ , plChanges+ , plChangesPosId+ , shuffle+ , swap+ , swapId+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.CurrentPlaylist as A+import Network.MPD.Commands.Query+import Network.MPD.Commands.Types+import Network.MPD.Core+import Network.MPD.Util++import Control.Monad.Error (throwError)++-- | Like 'add', but returns a playlist id.+addId :: MonadMPD m => Path -> Maybe Position -> m Id+addId path = A.runCommand . A.addId path++-- | Add a song (or a whole directory) to the current playlist.+add :: MonadMPD m => Path -> m ()+add = A.runCommand . A.add++-- | Clear the current playlist.+clear :: MonadMPD m => m ()+clear = A.runCommand A.clear++-- | Remove a song from the current playlist.+delete :: MonadMPD m => Position -> m ()+delete = A.runCommand . A.delete++-- | Remove a song from the current playlist.+deleteId :: MonadMPD m => Id -> m ()+deleteId = A.runCommand . A.deleteId++-- | Move a song to a given position in the current playlist.+move :: MonadMPD m => Position -> Position -> m ()+move pos = A.runCommand . A.move pos++-- | Move a song from (songid) to (playlist index) in the playlist. If to is+-- negative, it is relative to the current song in the playlist (if there is one).+moveId :: MonadMPD m => Id -> Position -> m ()+moveId i = A.runCommand . A.moveId i++-- | Retrieve file paths and positions of songs in the current playlist.+-- Note that this command is only included for completeness sake; it's+-- deprecated and likely to disappear at any time, please use 'playlistInfo'+-- instead.+playlist :: MonadMPD m => m [(Position, Path)]+playlist = mapM f =<< getResponse "playlist"+ where f s | (pos, name) <- breakChar ':' s+ , Just pos' <- parseNum pos+ = return (pos', Path name)+ | otherwise = throwError . Unexpected $ show s+{-# WARNING playlist "this is deprecated; please use 'playlistInfo' instead." #-}++-- | Search for songs in the current playlist with strict matching.+playlistFind :: MonadMPD m => Query -> m [Song]+playlistFind = A.runCommand . A.playlistFind++-- | Retrieve metadata for songs in the current playlist.+playlistInfo :: MonadMPD m => Maybe Position -> m [Song]+playlistInfo = A.runCommand . A.playlistInfo++-- | Like 'playlistInfo' but can restrict to a range of songs.+playlistInfoRange :: MonadMPD m => Maybe (Position, Position) -> m [Song]+playlistInfoRange = A.runCommand . A.playlistInfoRange++-- | Displays a list of songs in the playlist.+-- If id is specified, only its info is returned.+playlistId :: MonadMPD m => Maybe Id -> m [Song]+playlistId = A.runCommand . A.playlistId++-- | Search case-insensitively with partial matches for songs in the+-- current playlist.+playlistSearch :: MonadMPD m => Query -> m [Song]+playlistSearch = A.runCommand . A.playlistSearch++-- | Retrieve a list of changed songs currently in the playlist since+-- a given playlist version.+plChanges :: MonadMPD m => Integer -> m [Song]+plChanges = A.runCommand . A.plChanges++-- | Like 'plChanges' but only returns positions and ids.+plChangesPosId :: MonadMPD m => Integer -> m [(Position, Id)]+plChangesPosId = A.runCommand . A.plChangesPosId++-- | Shuffle the playlist.+shuffle :: MonadMPD m => Maybe (Position, Position) -- ^ Optional range (start, end)+ -> m ()+shuffle = A.runCommand . A.shuffle++-- | Swap the positions of two songs.+swap :: MonadMPD m => Position -> Position -> m ()+swap pos1 = A.runCommand . A.swap pos1++-- | Swap the positions of two songs (Id version)+swapId :: MonadMPD m => Id -> Id -> m ()+swapId id1 = A.runCommand . A.swapId id1
+ src/Network/MPD/Commands/Database.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Database+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++The music database.+-}++module Network.MPD.Commands.Database+ ( count+ , find+ , findAdd+ , list+ , listAll+ , listAllInfo+ , lsInfo+ , search+ , update+ , rescan+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Database as A+import Network.MPD.Commands.Query+import Network.MPD.Commands.Types+import Network.MPD.Core++-- | Count the number of entries matching a query.+count :: MonadMPD m => Query -> m Count+count = A.runCommand . A.count++-- | Search the database for entries exactly matching a query.+find :: MonadMPD m => Query -> m [Song]+find = A.runCommand . A.find++-- | Adds songs matching a query to the current playlist.+findAdd :: MonadMPD m => Query -> m ()+findAdd = A.runCommand . A.findAdd++-- | List all tags of the specified type.+list :: MonadMPD m+ => Metadata -- ^ Metadata to list+ -> Maybe Artist -> m [Value]+list m = A.runCommand . A.list m++-- | List the songs (without metadata) in a database directory recursively.+listAll :: MonadMPD m => Path -> m [Path]+listAll = A.runCommand . A.listAll++-- | Recursive 'lsInfo'.+listAllInfo :: MonadMPD m => Path -> m [LsResult]+listAllInfo = A.runCommand . A.listAllInfo++-- | Non-recursively list the contents of a database directory.+lsInfo :: MonadMPD m => Path -> m [LsResult]+lsInfo = A.runCommand . A.lsInfo++-- | Search the database using case insensitive matching.+search :: MonadMPD m => Query -> m [Song]+search = A.runCommand . A.search++-- | Update the server's database.+--+-- If no path is given, the whole library will be scanned. Unreadable or+-- non-existent paths are silently ignored.+--+-- The update job id is returned.+update :: MonadMPD m => Maybe Path -> m Integer+update = A.runCommand . A.update++-- | Like 'update' but also rescans unmodified files.+rescan :: MonadMPD m => Maybe Path -> m Integer+rescan = A.runCommand . A.rescan
+ src/Network/MPD/Commands/Extensions.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Extensions+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+Stability : unstable+Portability : unportable++Extensions and shortcuts to the standard MPD command set.+-}++module Network.MPD.Commands.Extensions where++import Network.MPD.Core+import Network.MPD.Commands+import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.CurrentPlaylist as A+import qualified Network.MPD.Applicative.StoredPlaylists as A++import Control.Monad (liftM)+import Data.Traversable (for)+import Data.Foldable (for_)++-- | This is exactly the same as `update`.+updateId :: MonadMPD m => Maybe Path -> m Integer+updateId = update+{-# DEPRECATED updateId "use `update` instead" #-}++-- | Toggles play\/pause. Plays if stopped.+toggle :: MonadMPD m => m ()+toggle = status >>= \st -> case stState st of Playing -> pause True+ _ -> play Nothing++-- | Add a list of songs\/folders to a playlist.+-- Should be more efficient than running 'add' many times.+addMany :: MonadMPD m => PlaylistName -> [Path] -> m ()+addMany plname xs = A.runCommand (for_ xs cmd)+ where cmd | plname == "" = A.add+ | otherwise = A.playlistAdd plname++-- | Recursive 'addId'. For directories, it will use the given position+-- for the first file in the directory and use the successor for the remaining+-- files. It returns a list of playlist ids for the songs added.+addIdMany :: MonadMPD m => Path -> Maybe Position -> m [Id]+addIdMany x (Just p) = do+ fs <- listAll x+ let fs' = map (\(a, b) -> (a, Just b)) $ zip fs [p ..]+ A.runCommand $ for fs' (uncurry A.addId)+addIdMany x Nothing = do+ fs <- listAll x+ A.runCommand $ for fs (`A.addId` Nothing)++-- | Like 'add' but returns a list of the files added.+addList :: MonadMPD m => Path -> m [Path]+addList x = add x >> listAll x+{-# DEPRECATED addList "will be removed in a future version" #-}++-- | Like 'playlistAdd' but returns a list of the files added.+playlistAddList :: MonadMPD m => PlaylistName -> Path -> m [Path]+playlistAddList plname path = playlistAdd plname path >> listAll path+{-# DEPRECATED playlistAddList "will be removed in a future version" #-}++{-+-- | Returns all songs and directories that match the given partial+-- path name.+complete :: MonadMPD m => String -> m [Either Path Song]+complete path = do+ xs <- liftM matches . lsInfo $ dropFileName path+ case xs of+ [Left dir] -> complete $ dir ++ "/"+ _ -> return xs+ where+ matches = filter (isPrefixOf path . takePath)+ takePath = either id sgFilePath+-}++-- | List the artists in the database.+listArtists :: MonadMPD m => m [Artist]+listArtists = list Artist Nothing++-- | List the albums in the database, optionally matching a given+-- artist.+listAlbums :: MonadMPD m => Maybe Artist -> m [Album]+listAlbums = list Album++-- | List the songs in an album of some artist.+listAlbum :: MonadMPD m => Artist -> Album -> m [Song]+listAlbum artist album = find (Artist =? artist <&> Album =? album)++-- | Retrieve the current playlist.+-- Equivalent to @playlistinfo Nothing@.+getPlaylist :: MonadMPD m => m [Song]+getPlaylist = playlistInfo Nothing++-- | Increase or decrease volume by a given percent, e.g.+-- 'volume 10' will increase the volume by 10 percent, while+-- 'volume (-10)' will decrease it by the same amount.+volume :: MonadMPD m => Int -> m ()+volume n = do+ cur <- stVolume `liftM` status+ case cur of+ Nothing -> return ()+ Just v -> setVolume (adjust v)+ where+ adjust x = round $ (fromIntegral n / (100 :: Double)) * x' + x'+ where x' = fromIntegral x
+ src/Network/MPD/Commands/Output.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Output+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Audio output devices.+-}++module Network.MPD.Commands.Output+ ( disableOutput+ , enableOutput+ , outputs+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Output as A+import Network.MPD.Core+import Network.MPD.Commands.Types++-- | Turn off an output device.+disableOutput :: MonadMPD m => Int -> m ()+disableOutput = A.runCommand . A.disableOutput++-- | Turn on an output device.+enableOutput :: MonadMPD m => Int -> m ()+enableOutput = A.runCommand . A.enableOutput++-- | Retrieve information for all output devices.+outputs :: MonadMPD m => m [Device]+outputs = A.runCommand A.outputs
+ src/Network/MPD/Commands/Parse.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}++-- | Module : Network.MPD.Commands.Parse+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : MIT (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Parsers for MPD data types.++module Network.MPD.Commands.Parse where++import Network.MPD.Commands.Types++import Control.Applicative+import Control.Monad.Error+import Data.Maybe (fromMaybe)++import Network.MPD.Util++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.UTF8 as UTF8++-- | Builds a 'Count' instance from an assoc. list.+parseCount :: [ByteString] -> Either String Count+parseCount = foldM f def . toAssocList+ where f :: Count -> (ByteString, ByteString) -> Either String Count+ f a ("songs", x) = return $ parse parseNum+ (\x' -> a { cSongs = x'}) a x+ f a ("playtime", x) = return $ parse parseNum+ (\x' -> a { cPlaytime = x' }) a x+ f _ x = Left $ show x++-- | Builds a list of 'Device' instances from an assoc. list+parseOutputs :: [ByteString] -> Either String [Device]+parseOutputs = mapM (foldM f def)+ . splitGroups ["outputid"]+ . toAssocList+ where f a ("outputid", x) = return $ parse parseNum+ (\x' -> a { dOutputID = x' }) a x+ f a ("outputname", x) = return a { dOutputName = UTF8.toString x }+ f a ("outputenabled", x) = return $ parse parseBool+ (\x' -> a { dOutputEnabled = x'}) a x+ f _ x = Left $ show x++-- | Builds a 'Stats' instance from an assoc. list.+parseStats :: [ByteString] -> Either String Stats+parseStats = foldM f def . toAssocList+ where+ f a ("artists", x) = return $ parse parseNum+ (\x' -> a { stsArtists = x' }) a x+ f a ("albums", x) = return $ parse parseNum+ (\x' -> a { stsAlbums = x' }) a x+ f a ("songs", x) = return $ parse parseNum+ (\x' -> a { stsSongs = x' }) a x+ f a ("uptime", x) = return $ parse parseNum+ (\x' -> a { stsUptime = x' }) a x+ f a ("playtime", x) = return $ parse parseNum+ (\x' -> a { stsPlaytime = x' }) a x+ f a ("db_playtime", x) = return $ parse parseNum+ (\x' -> a { stsDbPlaytime = x' }) a x+ f a ("db_update", x) = return $ parse parseNum+ (\x' -> a { stsDbUpdate = x' }) a x+ f _ x = Left $ show x++parseMaybeSong :: [ByteString] -> Either String (Maybe Song)+parseMaybeSong xs | null xs = Right Nothing+ | otherwise = Just <$> (parseSong . toAssocList) xs++-- | Builds a 'Song' instance from an assoc. list.+parseSong :: [(ByteString, ByteString)] -> Either String Song+parseSong xs = case xs of+ ("file", path):ys -> foldM f (defaultSong (Path path)) ys+ _ -> Left "Got a song without a file path! This indicates a bug in either libmpd-haskell or MPD itself!"++ where+ f :: Song -> (ByteString, ByteString) -> Either String Song+ f s ("Last-Modified", v) =+ return s { sgLastModified = parseIso8601 v }+ f s ("Time", v) =+ return s { sgLength = fromMaybe 0 $ parseNum v }+ f s ("Id", v) =+ return $ parse parseNum (\v' -> s { sgId = Just $ Id v' }) s v+ f s ("Pos", v) =+ maybe (return $ parse parseNum+ (\v' -> s { sgIndex = Just v' }) s v)+ (const $ return s)+ (sgIndex s)+ f s (k, v) = return . maybe s (\m -> sgAddTag m (Value v) s) $+ readMeta k++ -- Custom-made Read instance+ readMeta "ArtistSort" = Just ArtistSort+ readMeta "Artist" = Just Artist+ readMeta "Album" = Just Album+ readMeta "AlbumArtist" = Just AlbumArtist+ readMeta "AlbumArtistSort" = Just AlbumArtistSort+ readMeta "Title" = Just Title+ readMeta "Genre" = Just Genre+ readMeta "Name" = Just Name+ readMeta "Composer" = Just Composer+ readMeta "Performer" = Just Performer+ readMeta "Comment" = Just Comment+ readMeta "Date" = Just Date+ readMeta "Track" = Just Track+ readMeta "Disc" = Just Disc+ readMeta "MUSICBRAINZ_ARTISTID" = Just MUSICBRAINZ_ARTISTID+ readMeta "MUSICBRAINZ_ALBUMID" = Just MUSICBRAINZ_ALBUMID+ readMeta "MUSICBRAINZ_ALBUMARTISTID" = Just MUSICBRAINZ_ALBUMARTISTID+ readMeta "MUSICBRAINZ_TRACKID" = Just MUSICBRAINZ_TRACKID+ readMeta _ = Nothing++-- | A helper that runs a parser on a string and, depending on the+-- outcome, either returns the result of some command applied to the+-- result, or a default value. Used when building structures.+parse :: (ByteString -> Maybe a) -> (a -> b) -> b -> ByteString -> b+parse parser f x = maybe x f . parser++-- | A helper for running a parser returning Maybe on a pair of strings.+-- Returns Just if both strings where parsed successfully, Nothing otherwise.+pair :: (ByteString -> Maybe a) -> (ByteString, ByteString) -> Maybe (a, a)+pair p (x, y) = case (p x, p y) of+ (Just a, Just b) -> Just (a, b)+ _ -> Nothing
+ src/Network/MPD/Commands/PlaybackControl.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.PlaybackControl+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Controlling playback.+-}++module Network.MPD.Commands.PlaybackControl+ ( next+ , pause+ , play+ , playId+ , previous+ , seek+ , seekId+ , stop+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.PlaybackControl as A+import Network.MPD.Commands.Types+import Network.MPD.Core++-- | Play the next song.+next :: MonadMPD m => m ()+next = A.runCommand A.next++-- | Pause playing.+pause :: MonadMPD m => Bool -> m ()+pause = A.runCommand . A.pause++-- | Begin\/continue playing.+play :: MonadMPD m => Maybe Position -> m ()+play = A.runCommand . A.play++-- | Play a file with given id.+playId :: MonadMPD m => Id -> m ()+playId = A.runCommand . A.playId++-- | Play the previous song.+previous :: MonadMPD m => m ()+previous = A.runCommand A.previous++-- | Seek to some point in a song.+seek :: MonadMPD m => Position -> Seconds -> m ()+seek pos = A.runCommand . A.seek pos++-- | Seek to some point in a song (id version)+seekId :: MonadMPD m => Id -> Seconds -> m ()+seekId id' = A.runCommand . A.seekId id'++-- | Stop playing.+stop :: MonadMPD m => m ()+stop = A.runCommand A.stop
+ src/Network/MPD/Commands/PlaybackOptions.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.PlaybackOptions+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Playback options.+-}++module Network.MPD.Commands.PlaybackOptions+ ( consume+ , crossfade+ , random+ , repeat+ , setVolume+ , single+ , replayGainMode+ , replayGainStatus+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.PlaybackOptions as A+import Network.MPD.Commands.Types+import Network.MPD.Core++import Prelude hiding (repeat)++-- | Set consume mode+consume :: MonadMPD m => Bool -> m ()+consume = A.runCommand . A.consume++-- | Set crossfading between songs.+crossfade :: MonadMPD m => Seconds -> m ()+crossfade = A.runCommand . A.crossfade++-- | Set random playing.+random :: MonadMPD m => Bool -> m ()+random = A.runCommand . A.random++-- | Set repeating.+repeat :: MonadMPD m => Bool -> m ()+repeat = A.runCommand . A.repeat++-- | Set the volume (0-100 percent).+setVolume :: MonadMPD m => Int -> m ()+setVolume = A.runCommand . A.setVolume++-- | Set single mode+single :: MonadMPD m => Bool -> m ()+single = A.runCommand . A.single++-- | Set the replay gain mode.+replayGainMode :: MonadMPD m => ReplayGainMode -> m ()+replayGainMode = A.runCommand . A.replayGainMode++-- | Get the replay gain options.+replayGainStatus :: MonadMPD m => m [(String, String)]+replayGainStatus = A.runCommand A.replayGainStatus
+ src/Network/MPD/Commands/Query.hs view
@@ -0,0 +1,60 @@+{- |+Module : Network.MPD.Commands.Query+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+Stability : unstable+Portability : unportable++Query interface.+-}++module Network.MPD.Commands.Query (Query, (=?), (<&>), anything) where++import Network.MPD.Commands.Arg+import Network.MPD.Commands.Types++import Data.Monoid++-- | An interface for creating MPD queries.+--+-- For example, to match any song where the value of artist is \"Foo\", we+-- use:+--+-- > Artist =? "Foo"+--+-- We can also compose queries, thus narrowing the search. For example, to+-- match any song where the value of artist is \"Foo\" and the value of album+-- is \"Bar\", we use:+--+-- > Artist =? "Foo" <&> Album =? "Bar"+newtype Query = Query [Match] deriving Show++-- A single query clause, comprising a metadata key and a desired value.+data Match = Match Metadata Value++instance Show Match where+ show (Match meta query) = show meta ++ " \"" ++ toString query ++ "\""+ showList xs _ = unwords $ map show xs++instance Monoid Query where+ mempty = Query []+ Query a `mappend` Query b = Query (a ++ b)++instance MPDArg Query where+ prep = foldl (<++>) (Args []) . f+ where f (Query ms) = map (\(Match m q) -> Args [show m] <++> q) ms++-- | An empty query. Matches anything.+anything :: Query+anything = mempty++-- | Create a query.+(=?) :: Metadata -> Value -> Query+m =? s = Query [Match m s]++-- | Combine queries.+infixr 6 <&>+(<&>) :: Query -> Query -> Query+(<&>) = mappend
+ src/Network/MPD/Commands/Reflection.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Reflection+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Reflection.+-}++module Network.MPD.Commands.Reflection+ ( commands+ , notCommands+ , tagTypes+ , urlHandlers+ , decoders+ , config+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Reflection as A+import Network.MPD.Core++-- | Retrieve a list of available commands.+commands :: MonadMPD m => m [String]+commands = A.runCommand A.commands++-- | Retrieve a list of unavailable (due to access restrictions) commands.+notCommands :: MonadMPD m => m [String]+notCommands = A.runCommand A.notCommands++-- | Retrieve a list of available song metadata.+tagTypes :: MonadMPD m => m [String]+tagTypes = A.runCommand A.tagTypes++-- | Retrieve a list of supported urlhandlers.+urlHandlers :: MonadMPD m => m [String]+urlHandlers = A.runCommand A.urlHandlers++-- | Retreive a list of decoder plugins with associated suffix and mime types.+decoders :: MonadMPD m => m [(String, [(String, String)])]+decoders = A.runCommand A.decoders++-- | Retrieve configuration keys and values.+config :: MonadMPD m => m [(String, String)]+config = A.runCommand A.config
+ src/Network/MPD/Commands/Status.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Status+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Querying MPD's status.+-}++module Network.MPD.Commands.Status+ ( clearError+ , currentSong+ , idle+ , noidle+ , stats+ , status+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Status as A+import Network.MPD.Commands.Types+import Network.MPD.Core++-- | Clear the current error message in status.+clearError :: MonadMPD m => m ()+clearError = A.runCommand A.clearError++-- | Get the currently playing song.+currentSong :: MonadMPD m => m (Maybe Song)+currentSong = A.runCommand A.currentSong++-- | Wait until there is a noteworthy change in one or more of MPD's+-- susbystems.+--+-- The first argument is a list of subsystems that should be considered. An+-- empty list specifies that all subsystems should be considered.+--+-- A list of subsystems that have noteworthy changes is returned.+--+-- Note that running this command will block until either 'idle' returns or is+-- cancelled by 'noidle'.+idle :: MonadMPD m => [Subsystem] -> m [Subsystem]+idle = A.runCommand . A.idle++-- | Cancel 'idle'.+noidle :: MonadMPD m => m ()+noidle = A.runCommand A.noidle++-- | Get server statistics.+stats :: MonadMPD m => m Stats+stats = A.runCommand A.stats++-- | Get the server's status.+status :: MonadMPD m => m Status+status = A.runCommand A.status
+ src/Network/MPD/Commands/Stickers.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.Stickers+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Stickers.+-}++module Network.MPD.Commands.Stickers+ ( stickerGet+ , stickerSet+ , stickerDelete+ , stickerList+ , stickerFind+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.Stickers as A+import Network.MPD.Commands.Types+import Network.MPD.Core++-- | Reads a sticker value for the specified object.+stickerGet :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> m [String]+stickerGet typ uri = A.runCommand . A.stickerGet typ uri++-- | Adds a sticker value to the specified object.+stickerSet :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> String -- ^ Sticker value+ -> m ()+stickerSet typ uri name = A.runCommand . A.stickerSet typ uri name++-- | Delete a sticker value from the specified object.+stickerDelete :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> m ()+stickerDelete typ uri = A.runCommand . A.stickerDelete typ uri++-- | Lists the stickers for the specified object.+stickerList :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> m [(String, String)] -- ^ Sticker name\/sticker value+stickerList typ = A.runCommand . A.stickerList typ++-- | Searches the sticker database for stickers with the specified name, below+-- the specified path.+stickerFind :: MonadMPD m => ObjectType+ -> String -- ^ Path+ -> String -- ^ Sticker name+ -> m [(String, String)] -- ^ URI\/sticker value+stickerFind typ uri = A.runCommand . A.stickerFind typ uri
+ src/Network/MPD/Commands/StoredPlaylists.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Network.MPD.Commands.StoredPlaylists+Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2012+License : MIT (see LICENSE)++Maintainer : joachim.fasting@gmail.com+Stability : stable+Portability : unportable++Stored playlists.+-}++module Network.MPD.Commands.StoredPlaylists+ ( listPlaylist+ , listPlaylistInfo+ , listPlaylists+ , load+ , playlistAdd+ , playlistClear+ , playlistDelete+ , playlistMove+ , rename+ , rm+ , save+ ) where++import qualified Network.MPD.Applicative.Internal as A+import qualified Network.MPD.Applicative.StoredPlaylists as A+import Network.MPD.Commands.Types+import Network.MPD.Core++-- | Retrieve a list of files in a given playlist.+listPlaylist :: MonadMPD m => PlaylistName -> m [Path]+listPlaylist = A.runCommand . A.listPlaylist++-- | Retrieve metadata for files in a given playlist.+listPlaylistInfo :: MonadMPD m => PlaylistName -> m [Song]+listPlaylistInfo = A.runCommand . A.listPlaylistInfo++-- | Retreive a list of stored playlists.+listPlaylists :: MonadMPD m => m [PlaylistName]+listPlaylists = A.runCommand A.listPlaylists++-- | Load an existing playlist.+load :: MonadMPD m => PlaylistName -> m ()+load = A.runCommand . A.load++-- | Add a song (or a whole directory) to a stored playlist.+-- Will create a new playlist if the one specified does not already exist.+playlistAdd :: MonadMPD m => PlaylistName -> Path -> m ()+playlistAdd plname = A.runCommand . A.playlistAdd plname++-- | Clear a playlist. If the specified playlist does not exist, it will be+-- created.+playlistClear :: MonadMPD m => PlaylistName -> m ()+playlistClear = A.runCommand . A.playlistClear++-- | Remove a song from a playlist.+playlistDelete :: MonadMPD m => PlaylistName -> Position -> m ()+playlistDelete name = A.runCommand . A.playlistDelete name++-- | Move a song to a given position in the playlist specified.+playlistMove :: MonadMPD m => PlaylistName -> Id -> Position -> m ()+playlistMove name from = A.runCommand . A.playlistMove name from++-- | Rename an existing playlist.+rename :: MonadMPD m+ => PlaylistName -- ^ Original playlist+ -> PlaylistName -- ^ New playlist name+ -> m ()+rename plname = A.runCommand . A.rename plname++-- | Delete existing playlist.+rm :: MonadMPD m => PlaylistName -> m ()+rm = A.runCommand . A.rm++-- | Save the current playlist.+save :: MonadMPD m => PlaylistName -> m ()+save = A.runCommand . A.save
+ src/Network/MPD/Commands/Types.hs view
@@ -0,0 +1,337 @@+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, GeneralizedNewtypeDeriving #-}+-- | Module : Network.MPD.Commands.Types+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : MIT (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Various MPD data structures and types++module Network.MPD.Commands.Types+ ( ToString(..)+ , Artist+ , Album+ , Title+ , PlaylistName(..)+ , Path(..)+ , Metadata(..)+ , Value(..)+ , ObjectType(..)+ , Seconds+ , Decibels+ , State(..)+ , Subsystem(..)+ , ReplayGainMode(..)+ , Count(..)+ , LsResult(..)+ , Device(..)+ , Song(..)+ , Position+ , Id(..)+ , sgGetTag+ , sgAddTag+ , Stats(..)+ , Status(..)+ , def+ , defaultSong+ ) where++import Network.MPD.Commands.Arg (MPDArg(prep), Args(Args))++import Data.Default++import qualified Data.Map as M+import Data.Time.Clock (UTCTime)+import Data.String++import Data.Text (Text)+import qualified Data.Text.Encoding as Text+import Data.ByteString (ByteString)+import qualified Data.ByteString.UTF8 as UTF8++-- The purpose of this class is to allow users to choose the optimal+-- representation of response values.+-- | A type class for values that can be converted to `String`s.+class ToString a where++ -- | Convert given value to `String`.+ toString :: a -> String++ -- | Convert given value to `Text`.+ toText :: a -> Text++ -- | Convert given value to a UTF-8 encoded `ByteString`.+ toUtf8 :: a -> ByteString++type Artist = Value+type Album = Value+type Title = Value++-- | Used for commands which require a playlist name.+-- If empty, the current playlist is used.+newtype PlaylistName = PlaylistName ByteString+ deriving (Eq, Show, MPDArg)++instance ToString PlaylistName where+ toString (PlaylistName x) = UTF8.toString x+ toText (PlaylistName x) = Text.decodeUtf8 x+ toUtf8 (PlaylistName x) = x++instance IsString PlaylistName where+ fromString = PlaylistName . UTF8.fromString++-- | Used for commands which require a path within the database.+-- If empty, the root path is used.+newtype Path = Path ByteString+ deriving (Eq, Show, MPDArg)++instance ToString Path where+ toString (Path x) = UTF8.toString x+ toText (Path x) = Text.decodeUtf8 x+ toUtf8 (Path x) = x++instance IsString Path where+ fromString = Path . UTF8.fromString++-- | Available metadata types\/scope modifiers, used for searching the+-- database for entries with certain metadata values.+data Metadata = Artist+ | ArtistSort+ | Album+ | AlbumArtist+ | AlbumArtistSort+ | Title+ | Track+ | Name+ | Genre+ | Date+ | Composer+ | Performer+ | Comment+ | Disc+ | MUSICBRAINZ_ARTISTID+ | MUSICBRAINZ_ALBUMID+ | MUSICBRAINZ_ALBUMARTISTID+ | MUSICBRAINZ_TRACKID+ deriving (Eq, Enum, Ord, Bounded, Show)++instance MPDArg Metadata++-- | A metadata value.+newtype Value = Value ByteString+ deriving (Eq, Show, MPDArg)++instance ToString Value where+ toString (Value x) = UTF8.toString x+ toText (Value x) = Text.decodeUtf8 x+ toUtf8 (Value x) = x++instance IsString Value where+ fromString = Value . UTF8.fromString++-- | Object types.+data ObjectType = SongObj+ deriving (Eq, Show)++instance MPDArg ObjectType where+ prep SongObj = Args ["song"]++type Seconds = Integer++type Decibels = Integer++-- | Represents the different playback states.+data State = Playing+ | Stopped+ | Paused+ deriving (Show, Eq)++-- | Represents the various MPD subsystems.+data Subsystem+ = DatabaseS -- ^ The song database+ | UpdateS -- ^ Database updates+ | StoredPlaylistS -- ^ Stored playlists+ | PlaylistS -- ^ The current playlist+ | PlayerS -- ^ The player+ | MixerS -- ^ The volume mixer+ | OutputS -- ^ Audio outputs+ | OptionsS -- ^ Playback options+ | StickerS -- ^ Sticker database+ | SubscriptionS -- ^ Subscription+ | MessageS -- ^ Message on subscribed channel+ deriving (Eq, Show)++instance MPDArg Subsystem where+ prep DatabaseS = Args ["database"]+ prep UpdateS = Args ["update"]+ prep StoredPlaylistS = Args ["stored_playlist"]+ prep PlaylistS = Args ["playlist"]+ prep PlayerS = Args ["player"]+ prep MixerS = Args ["mixer"]+ prep OutputS = Args ["output"]+ prep OptionsS = Args ["options"]+ prep StickerS = Args ["sticker"]+ prep SubscriptionS = Args ["subscription"]+ prep MessageS = Args ["message"]++data ReplayGainMode+ = Off -- ^ Disable replay gain+ | TrackMode -- ^ Per track mode+ | AlbumMode -- ^ Per album mode+ deriving (Eq, Show)++instance MPDArg ReplayGainMode where+ prep Off = Args ["off"]+ prep TrackMode = Args ["track"]+ prep AlbumMode = Args ["album"]++-- | Represents the result of running 'count'.+data Count =+ Count { cSongs :: Integer -- ^ Number of songs matching the query+ , cPlaytime :: Seconds -- ^ Total play time of matching songs+ }+ deriving (Eq, Show)++defaultCount :: Count+defaultCount = Count { cSongs = 0, cPlaytime = 0 }++instance Default Count where+ def = defaultCount++-- | Result of the lsInfo operation+data LsResult+ = LsDirectory Path -- ^ Directory+ | LsSong Song -- ^ Song+ | LsPlaylist PlaylistName -- ^ Playlist+ deriving (Eq, Show)++-- | Represents an output device.+data Device =+ Device { dOutputID :: Int -- ^ Output's ID number+ , dOutputName :: String -- ^ Output's name as defined in the MPD+ -- configuration file+ , dOutputEnabled :: Bool }+ deriving (Eq, Show)++defaultDevice :: Device+defaultDevice =+ Device { dOutputID = 0, dOutputName = "", dOutputEnabled = False }++instance Default Device where+ def = defaultDevice++-- | Represents a single song item.+data Song = Song+ { sgFilePath :: Path+ -- | Map of available tags (multiple occurrences of one tag type allowed)+ , sgTags :: M.Map Metadata [Value]+ -- | Last modification date+ , sgLastModified :: Maybe UTCTime+ -- | Length of the song in seconds+ , sgLength :: Seconds+ -- | Id in playlist+ , sgId :: Maybe Id+ -- | Position in playlist+ , sgIndex :: Maybe Position+ } deriving (Eq, Show)++-- | The position of a song in a playlist.+type Position = Int++newtype Id = Id Int+ deriving (Eq, Show)++instance (MPDArg Id) where+ prep (Id x) = prep x++-- | Get list of specific tag type+sgGetTag :: Metadata -> Song -> Maybe [Value]+sgGetTag meta s = M.lookup meta $ sgTags s++-- | Add metadata tag value.+sgAddTag :: Metadata -> Value -> Song -> Song+sgAddTag meta value s = s { sgTags = M.insertWith' (++) meta [value] (sgTags s) }++defaultSong :: Path -> Song+defaultSong path =+ Song { sgFilePath = path, sgTags = M.empty, sgLastModified = Nothing+ , sgLength = 0, sgId = Nothing, sgIndex = Nothing }++-- | Container for database statistics.+data Stats =+ Stats { stsArtists :: Integer -- ^ Number of artists.+ , stsAlbums :: Integer -- ^ Number of albums.+ , stsSongs :: Integer -- ^ Number of songs.+ , stsUptime :: Seconds -- ^ Daemon uptime in seconds.+ , stsPlaytime :: Seconds -- ^ Total playing time.+ , stsDbPlaytime :: Seconds -- ^ Total play time of all the songs in+ -- the database.+ , stsDbUpdate :: Integer -- ^ Last database update in UNIX time.+ }+ deriving (Eq, Show)++defaultStats :: Stats+defaultStats =+ Stats { stsArtists = 0, stsAlbums = 0, stsSongs = 0, stsUptime = 0+ , stsPlaytime = 0, stsDbPlaytime = 0, stsDbUpdate = 0 }++instance Default Stats where+ def = defaultStats++-- | Container for MPD status.+data Status =+ Status { stState :: State+ -- | A percentage (0-100).+ --+ -- 'Nothing' indicates that the output lacks mixer support.+ , stVolume :: Maybe Int+ , stRepeat :: Bool+ , stRandom :: Bool+ -- | A value that is incremented by the server every time the+ -- playlist changes.+ , stPlaylistVersion :: Integer+ -- | The number of items in the current playlist.+ , stPlaylistLength :: Integer+ -- | Current song's position in the playlist.+ , stSongPos :: Maybe Position+ -- | Current song's playlist ID.+ , stSongID :: Maybe Id+ -- | Next song's position in the playlist.+ , stNextSongPos :: Maybe Position+ -- | Next song's playlist ID.+ , stNextSongID :: Maybe Id+ -- | Time elapsed\/total time of playing song (if any).+ , stTime :: Maybe (Double, Seconds)+ -- | Bitrate (in kilobytes per second) of playing song (if any).+ , stBitrate :: Maybe Int+ -- | Crossfade time.+ , stXFadeWidth :: Seconds+ -- | MixRamp threshold in dB+ , stMixRampdB :: Double+ -- | MixRamp extra delay in seconds+ , stMixRampDelay :: Double+ -- | Samplerate\/bits\/channels for the chosen output device+ -- (see mpd.conf).+ , stAudio :: (Int, Int, Int)+ -- | Job ID of currently running update (if any).+ , stUpdatingDb :: Maybe Integer+ -- | If True, MPD will play only one song and stop after finishing it.+ , stSingle :: Bool+ -- | If True, a song will be removed after it has been played.+ , stConsume :: Bool+ -- | Last error message (if any).+ , stError :: Maybe String }+ deriving (Eq, Show)++defaultStatus :: Status+defaultStatus =+ Status { stState = Stopped, stVolume = Just 0, stRepeat = False+ , stRandom = False, stPlaylistVersion = 0, stPlaylistLength = 0+ , stSongPos = Nothing, stSongID = Nothing, stTime = Nothing+ , stNextSongPos = Nothing, stNextSongID = Nothing+ , stBitrate = Nothing, stXFadeWidth = 0, stMixRampdB = 0+ , stMixRampDelay = 0, stAudio = (0,0,0), stUpdatingDb = Nothing+ , stSingle = False, stConsume = False, stError = Nothing }++instance Default Status where+ def = defaultStatus
+ src/Network/MPD/Core.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving, OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module : Network.MPD.Core+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : MIT (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- The core datatypes and operations are defined here, including the+-- primary instance of the 'MonadMPD' class, 'MPD'.++module Network.MPD.Core (+ -- * Classes+ MonadMPD(..),+ -- * Data types+ MPD, MPDError(..), ACKType(..), Response, Host, Port, Password,+ -- * Running+ withMPDEx,+ -- * Interacting+ getResponse, kill,+ ) where++import Network.MPD.Util+import Network.MPD.Core.Class+import Network.MPD.Core.Error++import Data.Char (isDigit)+import Control.Applicative (Applicative(..), (<$>), (<*))+import qualified Control.Exception as E+import Control.Monad (ap, unless)+import Control.Monad.Error (ErrorT(..), MonadError(..))+import Control.Monad.Reader (ReaderT(..), ask)+import Control.Monad.State (StateT, MonadIO(..), modify, gets, evalStateT)+import qualified Data.Foldable as F+import Network (PortID(..), withSocketsDo, connectTo)+import System.IO (Handle, hPutStrLn, hReady, hClose, hFlush)+import System.IO.Error (isEOFError, tryIOError, ioeGetErrorType)+import qualified System.IO.UTF8 as U+import Text.Printf (printf)+import qualified GHC.IO.Exception as GE++import qualified Prelude+import Prelude hiding (break, drop, dropWhile, read)+import Data.ByteString.Char8 (ByteString, isPrefixOf, break, drop, dropWhile)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.UTF8 as UTF8++--+-- Data types.+--++type Host = String+type Port = Integer++--+-- IO based MPD client implementation.+--++-- | The main implementation of an MPD client. It actually connects+-- to a server and interacts with it.+--+-- To use the error throwing\/catching capabilities:+--+-- > import Control.Monad.Error (throwError, catchError)+--+-- To run IO actions within the MPD monad:+--+-- > import Control.Monad.Trans (liftIO)++newtype MPD a =+ MPD { runMPD :: ErrorT MPDError+ (StateT MPDState+ (ReaderT (Host, Port) IO)) a+ } deriving (Functor, Monad, MonadIO, MonadError MPDError)++instance Applicative MPD where+ (<*>) = ap+ pure = return++instance MonadMPD MPD where+ open = mpdOpen+ close = mpdClose+ send = mpdSend+ getPassword = MPD $ gets stPassword+ setPassword pw = MPD $ modify (\st -> st { stPassword = pw })+ getVersion = MPD $ gets stVersion++-- | Inner state for MPD+data MPDState =+ MPDState { stHandle :: Maybe Handle+ , stPassword :: String+ , stVersion :: (Int, Int, Int)+ }++-- | A response is either an 'MPDError' or some result.+type Response = Either MPDError++-- | The most configurable API for running an MPD action.+withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)+withMPDEx host port pw x = withSocketsDo $+ runReaderT (evalStateT (runErrorT . runMPD $ open >> (x <* close)) initState)+ (host, port)+ where initState = MPDState Nothing pw (0, 0, 0)++mpdOpen :: MPD ()+mpdOpen = MPD $ do+ (host, port) <- ask+ runMPD close+ mHandle <- liftIO (safeConnectTo host port)+ modify (\st -> st { stHandle = mHandle })+ F.forM_ mHandle $ \_ -> runMPD checkConn >>= (`unless` runMPD close)+ where+ safeConnectTo host@('/':_) _ =+ (Just <$> connectTo "" (UnixSocket host))+ `E.catch` (\(_ :: E.SomeException) -> return Nothing)+ safeConnectTo host port =+ (Just <$> connectTo host (PortNumber $ fromInteger port))+ `E.catch` (\(_ :: E.SomeException) -> return Nothing)+ checkConn = do+ [msg] <- send ""+ if "OK MPD" `isPrefixOf` msg+ then MPD $ checkVersion $ parseVersion msg+ else return False++ checkVersion Nothing = throwError $ Custom "Couldn't determine MPD version"+ checkVersion (Just version)+ | version < requiredVersion =+ throwError $ Custom $ printf+ "MPD %s is not supported, upgrade to MPD %s or above!"+ (formatVersion version) (formatVersion requiredVersion)+ | otherwise = do+ modify (\st -> st { stVersion = version })+ return True+ where+ requiredVersion = (0, 15, 0)++ parseVersion = parseTriple '.' parseNum . dropWhile (not . isDigit)++ formatVersion :: (Int, Int, Int) -> String+ formatVersion (x, y, z) = printf "%d.%d.%d" x y z+++mpdClose :: MPD ()+mpdClose =+ MPD $ do+ mHandle <- gets stHandle+ F.forM_ mHandle $ \h -> do+ modify $ \st -> st{stHandle = Nothing}+ r <- liftIO $ sendClose h+ F.forM_ r throwError+ where+ sendClose handle =+ (hPutStrLn handle "close" >> hReady handle >> hClose handle >> return Nothing)+ `E.catch` handler++ handler err+ | isEOFError err = return Nothing+ | otherwise = (return . Just . ConnectionError) err++mpdSend :: String -> MPD [ByteString]+mpdSend str = send' `catchError` handler+ where+ handler err+ | ConnectionError e <- err, isRetryable e = mpdOpen >> send'+ | otherwise = throwError err++ send' :: MPD [ByteString]+ send' = MPD $ gets stHandle >>= maybe (throwError NoMPD) go++ go handle = (liftIO . tryIOError $ do+ unless (null str) $ U.hPutStrLn handle str >> hFlush handle+ getLines handle [])+ >>= either (\err -> modify (\st -> st { stHandle = Nothing })+ >> throwError (ConnectionError err)) return++ getLines :: Handle -> [ByteString] -> IO [ByteString]+ getLines handle acc = do+ l <- B.hGetLine handle+ if "OK" `isPrefixOf` l || "ACK" `isPrefixOf` l+ then (return . reverse) (l:acc)+ else getLines handle (l:acc)++-- | Re-connect and retry for these Exceptions.+isRetryable :: E.IOException -> Bool+isRetryable e = or [ isEOFError e, isResourceVanished e ]++-- | Predicate to identify ResourceVanished exceptions.+-- Note: these are GHC only!+isResourceVanished :: GE.IOException -> Bool+isResourceVanished e = ioeGetErrorType e == GE.ResourceVanished++--+-- Other operations.+--++-- | Kill the server. Obviously, the connection is then invalid.+kill :: (MonadMPD m) => m ()+kill = send "kill" >> return ()++-- | Send a command to the MPD server and return the result.+getResponse :: (MonadMPD m) => String -> m [ByteString]+getResponse cmd = (send cmd >>= parseResponse) `catchError` sendpw+ where+ sendpw e@(ACK Auth _) = do+ pw <- getPassword+ if null pw then throwError e+ else send ("password " ++ pw) >>= parseResponse+ >> send cmd >>= parseResponse+ sendpw e =+ throwError e++-- Consume response and return a Response.+parseResponse :: (MonadError MPDError m) => [ByteString] -> m [ByteString]+parseResponse xs+ | null xs = throwError $ NoMPD+ | "ACK" `isPrefixOf` x = throwError $ parseAck x+ | otherwise = return $ Prelude.takeWhile ("OK" /=) xs+ where+ x = head xs++-- Turn MPD ACK into the corresponding 'MPDError'+parseAck :: ByteString -> MPDError+parseAck s = ACK ack (UTF8.toString msg)+ where+ ack = case code of+ 2 -> InvalidArgument+ 3 -> InvalidPassword+ 4 -> Auth+ 5 -> UnknownCommand+ 50 -> FileNotFound+ 51 -> PlaylistMax+ 52 -> System+ 53 -> PlaylistLoad+ 54 -> Busy+ 55 -> NotPlaying+ 56 -> FileExists+ _ -> UnknownACK+ (code, _, msg) = splitAck s++-- Break an ACK into (error code, current command, message).+-- ACKs are of the form:+-- ACK [error@command_listNum] {current_command} message_text\n+splitAck :: ByteString -> (Int, ByteString, ByteString)+splitAck s = (read code, cmd, msg)+ where+ (code, notCode) = between '[' '@' s+ (cmd, notCmd) = between '{' '}' notCode+ msg = drop 1 $ dropWhile (' ' ==) notCmd++ -- take whatever is between 'f' and 'g'.+ between a b xs = let (_, y) = break (== a) xs+ in break (== b) (drop 1 y)
+ src/Network/MPD/Core/Class.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE FlexibleContexts #-}++-- | Module : Network.MPD.Core.Class+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : MIT (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- The MPD typeclass.++module Network.MPD.Core.Class where++import Data.ByteString (ByteString)++import Network.MPD.Core.Error (MPDError)++import Control.Monad.Error (MonadError)++type Password = String++-- | A typeclass to allow for multiple implementations of a connection+-- to an MPD server.+class (Monad m, MonadError MPDError m) => MonadMPD m where+ -- | Open (or re-open) a connection to the MPD server.+ open :: m ()+ -- | Close the connection.+ close :: m ()+ -- | Send a string to the server and return its response.+ send :: String -> m [ByteString]+ -- | Produce a password to send to the server should it ask for+ -- one.+ getPassword :: m Password+ -- | Alters password to be sent to the server.+ setPassword :: Password -> m ()+ -- | Get MPD protocol version+ getVersion :: m (Int, Int, Int)
+ src/Network/MPD/Core/Error.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE DeriveDataTypeable #-}++{- |+Module : Network.MPD.Core.Error+Copyright : (c) Joachim Fasting 2012+License : MIT++Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+Stability : unstable+Portability : unportable++MPD errors.+-}++module Network.MPD.Core.Error where++import qualified Control.Exception as E+import Control.Monad.Error (Error(..))+import Data.Typeable++-- | The MPDError type is used to signal errors, both from the MPD and+-- otherwise.+data MPDError = NoMPD -- ^ MPD not responding+ | ConnectionError E.IOException -- ^ An error occurred while talking to MPD.+ | Unexpected String -- ^ MPD returned an unexpected response.+ -- This is a bug, either in the library or+ -- in MPD itself.+ | Custom String -- ^ Used for misc. errors+ | ACK ACKType String -- ^ ACK type and a message from the+ -- server+ deriving (Eq, Typeable)++instance E.Exception MPDError++instance Show MPDError where+ show NoMPD = "Could not connect to MPD"+ show (ConnectionError e) = "Connection error (" ++ show e ++ ")"+ show (Unexpected s) = "MPD returned an unexpected response: " ++ unlines [+ s+ , ""+ , "This is most likely a bug in libmpd! Please report it here:"+ , ""+ , "https://github.com/vimus/libmpd-haskell/issues/new"+ ]+ show (Custom s) = s+ show (ACK _ s) = s++instance Error MPDError where+ noMsg = Custom "An error occurred"+ strMsg = Custom++-- | Represents various MPD errors (aka. ACKs).+data ACKType = InvalidArgument -- ^ Invalid argument passed (ACK 2)+ | InvalidPassword -- ^ Invalid password supplied (ACK 3)+ | Auth -- ^ Authentication required (ACK 4)+ | UnknownCommand -- ^ Unknown command (ACK 5)+ | FileNotFound -- ^ File or directory not found ACK 50)+ | PlaylistMax -- ^ Playlist at maximum size (ACK 51)+ | System -- ^ A system error (ACK 52)+ | PlaylistLoad -- ^ Playlist loading failed (ACK 53)+ | Busy -- ^ Update already running (ACK 54)+ | NotPlaying -- ^ An operation requiring playback+ -- got interrupted (ACK 55)+ | FileExists -- ^ File already exists (ACK 56)+ | UnknownACK -- ^ An unknown ACK (aka. bug)+ deriving Eq
+ src/Network/MPD/Util.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Module : Network.MPD.Util+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : MIT (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Utilities.++module Network.MPD.Util (+ parseDate, parseIso8601, formatIso8601, parseNum, parseFrac,+ parseBool, showBool, breakChar, parseTriple,+ toAssoc, toAssocList, splitGroups, read+ ) where++import Control.Arrow++import Data.Time.Format (ParseTime, parseTime, FormatTime, formatTime)+import System.Locale (defaultTimeLocale)++import qualified Prelude+import Prelude hiding (break, take, drop, dropWhile, read)+import Data.ByteString.Char8 (break, drop, dropWhile, ByteString)+import qualified Data.ByteString.UTF8 as UTF8+import Data.String++import Control.Applicative+import qualified Data.Attoparsec.ByteString.Char8 as A++-- | Like Prelude.read, but works with ByteString.+read :: Read a => ByteString -> a+read = Prelude.read . UTF8.toString++-- Break a string by character, removing the separator.+breakChar :: Char -> ByteString -> (ByteString, ByteString)+breakChar c = second (drop 1) . break (== c)++-- Parse a date value.+-- > parseDate "2008" = Just 2008+-- > parseDate "2008-03-01" = Just 2008+parseDate :: ByteString -> Maybe Int+parseDate = parseMaybe p+ where+ p = A.decimal <* A.skipMany (A.char '-' <|> A.digit)++-- Parse date in iso 8601 format+parseIso8601 :: (ParseTime t) => ByteString -> Maybe t+parseIso8601 = parseTime defaultTimeLocale iso8601Format . UTF8.toString++formatIso8601 :: FormatTime t => t -> String+formatIso8601 = formatTime defaultTimeLocale iso8601Format++iso8601Format :: String+iso8601Format = "%FT%TZ"++-- Parse a positive or negative integer value, returning 'Nothing' on failure.+parseNum :: (Read a, Integral a) => ByteString -> Maybe a+parseNum = parseMaybe (A.signed A.decimal)++-- Parse C style floating point value, returning 'Nothing' on failure.+parseFrac :: (Fractional a, Read a) => ByteString -> Maybe a+parseFrac = parseMaybe p+ where+ p = A.string "nan" *> pure (Prelude.read "NaN")+ <|> A.string "inf" *> pure (Prelude.read "Infinity")+ <|> A.string "-inf" *> pure (Prelude.read "-Infinity")+ <|> A.rational++-- Inverts 'parseBool'.+showBool :: IsString a => Bool -> a+-- FIXME: can we change the type to (Bool -> ByteString)?+-- not without also changing Arg to use bytestrings rather than plain String.+showBool x = if x then "1" else "0"++-- Parse a boolean response value.+parseBool :: ByteString -> Maybe Bool+parseBool = parseMaybe p+ where+ p = A.char '1' *> pure True <|> A.char '0' *> pure False++-- Break a string into triple.+parseTriple :: Char -> (ByteString -> Maybe a) -> ByteString -> Maybe (a, a, a)+parseTriple c f s = let (u, u') = breakChar c s+ (v, w) = breakChar c u' in+ case (f u, f v, f w) of+ (Just a, Just b, Just c') -> Just (a, b, c')+ _ -> Nothing++-- Break a string into a key-value pair, separating at the first ':'.+toAssoc :: ByteString -> (ByteString, ByteString)+toAssoc = second (dropWhile (== ' ') . drop 1) . break (== ':')++toAssocList :: [ByteString] -> [(ByteString, ByteString)]+toAssocList = map toAssoc++-- Takes an association list with recurring keys and groups each cycle of keys+-- with their values together. There can be several keys that begin cycles,+-- (the elements of the first parameter).+splitGroups :: [ByteString] -> [(ByteString, ByteString)] -> [[(ByteString, ByteString)]]+splitGroups groupHeads = go+ where+ go [] = []+ go (x:xs) =+ let+ (ys, zs) = Prelude.break isGroupHead xs+ in+ (x:ys) : go zs++ isGroupHead = (`elem` groupHeads) . fst++-- A helper for running a Parser, turning errors into Nothing.+parseMaybe :: A.Parser a -> ByteString -> Maybe a+parseMaybe p s = either (const Nothing) Just $ A.parseOnly (p <* A.endOfInput) s
tests/Arbitrary.hs view
@@ -29,8 +29,8 @@ arbitrary = UTF8.fromString <$> arbitrary -- No longer provided by QuickCheck 2-two :: Monad m => m a -> m (a, a)-two m = liftM2 (,) m m+-- two :: Monad m => m a -> m (a, a)+-- two m = liftM2 (,) m m three :: Monad m => m a -> m (a, a, a) three m = liftM3 (,,) m m m
tests/CommandSpec.hs view
@@ -12,30 +12,26 @@ module CommandSpec (main, spec) where import Arbitrary ()-import Defaults () import StringConn import TestUtil import Unparse -import Test.Hspec.Monadic-import Test.Hspec.HUnit ()-+import Network.MPD.Core import Network.MPD.Commands+import Network.MPD.Commands.Types import Network.MPD.Commands.Extensions-import Network.MPD.Core (MPDError(..), ACKType(..)) import Prelude hiding (repeat)-import Data.Default (Default(def)) main :: IO ()-main = hspecX spec+main = hspec spec -spec :: Specs+spec :: Spec spec = do -- * Admin commands describe "enableOutput" $ do it "sends an enableoutput command" $ testEnableOutput- + describe "disableOutput" $ do it "sends an disableoutput command" $ testDisableOutput @@ -43,15 +39,30 @@ it "lists available outputs" $ testOutputs describe "update" $ do- it "updates entire collection by default" $ testUpdate0- it "can update single paths" $ testUpdate1- it "can update multiple paths" $ testUpdateMany+ it "updates entire collection by default" $ do+ cmd [("update", Right "updating_db: 23\nOK")] (Right 23)+ (update Nothing) + it "can update a specific path" $ do+ cmd [("update \"foo\"", Right "updating_db: 23\nOK")] (Right 23)+ (update $ Just "foo")++ describe "rescan" $ do+ it "returns entire collection by default" $ do+ cmd [("rescan", Right "updating_db: 23\nOK")] (Right 23)+ (rescan Nothing)++ it "can rescan a specific path" $ do+ cmd [("rescan \"foo\"", Right "updating_db: 23\nOK")] (Right 23)+ (rescan $ Just "foo")+ -- * Database commands +{- XXX: incorrect; fixed in the applicative version describe "list" $ do it "returns a list of values for a given metadata type" $ testListAny it "can constrain listing to entries matching a query" $ testListQuery+-} describe "listAll" $ do it "lists everything" $ testListAll@@ -66,28 +77,17 @@ it "returns a count of items matching a query" $ testCount -- * Playlist commands-- describe "add" $ do- it "adds a url to the current playlist" $ testAdd-- describe "add_" $ do- it "adds a url to current playlist, returning nothing" $ testAdd_- describe "playlistAdd" $ do it "adds a url to a stored playlist" $ testPlaylistAdd - describe "addId" $ do- it "adds a url to a stored playlist, returning the pl index" $ testAddId- describe "playlistClear" $ do it "clears a stored playlist" $ testPlaylistClear - describe "clear" $ do- it "clears current play list" $ testClear- describe "plChangesPosid" $ do it "does something ..." $ testPlChangesPosId+{- XXX: doesn't work it "fails on weird input" $ testPlChangesPosIdWeird+-} -- XXX: this is ill-defined {-@@ -160,11 +160,6 @@ it "gets database stats" $ testStats -- * Extensions- - describe "updateId" $ do- it "returns the job id" $ do- testUpdateId0- testUpdateId1 describe "toggle" $ do it "starts playback if paused" $ testTogglePlay it "stops playback if playing" $ testToggleStop@@ -173,10 +168,14 @@ it "can also add to stored playlists" $ testAddMany1 describe "volume" $ do it "adjusts volume relative to current volume" $ testVolume- -cmd_ expect f = cmd expect (Right ()) f -cmd expect resp f = testMPD expect resp "" f `shouldBe` Ok ++cmd_ :: [(Expect, Response String)] -> StringMPD () -> Expectation+cmd_ expect f = cmd expect (Right ()) f++cmd :: (Eq a, Show a) => [(Expect, Response String)] -> Response a -> StringMPD a -> Expectation+cmd expect resp f = testMPD expect f `shouldBe` resp+ -- -- Admin commands --@@ -191,18 +190,13 @@ resp = concatMap unparse [obj1, obj2] ++ "OK" cmd [("outputs", Right resp)] (Right [obj1, obj2]) outputs -testUpdate0 = cmd_ [("update", Right "updating_db: 1\nOK")] (update [])-testUpdate1 =- cmd_ [("update \"foo\"", Right "updating_db: 1\nOK")] (update ["foo"]) -testUpdateMany =- cmd_ [("command_list_begin\nupdate \"foo\"\nupdate \"bar\"\ncommand_list_end", Right "updating_db: 1\nOK")]- (update ["foo","bar"]) -- -- Database commands -- +{- XXX: this is incorrect -- XXX: generalize to arbitrary Metadata values testListAny = cmd [("list Title", Right "Title: Foo\nTitle: Bar\nOK")] (Right ["Foo", "Bar"])@@ -211,6 +205,7 @@ testListQuery = cmd [("list Title Artist \"Muzz\"", Right "Title: Foo\nOK")] (Right ["Foo"]) (list Title (Artist =? "Muzz"))+-} testListAll = cmd [("listall \"\"", Right "directory: FooBand\n\@@ -243,41 +238,25 @@ -- -- Playlist commands ----testAdd =- cmd [("add \"foo\"", Right "OK"),- ("listall \"foo\"", Right "file: Foo\nfile: Bar\nOK")]- (Right ["Foo", "Bar"])- (add "foo")--testAdd_ =- cmd_ [("add \"foo\"", Right "OK")] (add_ "foo")- testPlaylistAdd = cmd_ [("playlistadd \"foo\" \"bar\"", Right "OK")]- (playlistAdd_ "foo" "bar")--testAddId =- cmd [("addid \"dir/Foo-Bar.ogg\"", Right "Id: 20\nOK")]- (Right $ Id 20)- (addId "dir/Foo-Bar.ogg" Nothing)+ (playlistAdd "foo" "bar") testPlaylistClear = cmd_ [("playlistclear \"foo\"", Right "OK")] (playlistClear "foo") -testClear =- cmd_ [("clear", Right "OK")] clear- testPlChangesPosId = cmd [("plchangesposid 10", Right "OK")] (Right []) (plChangesPosId 10) +{- XXX: testPlChangesPosIdWeird = cmd [("plchangesposid 10", Right "cpos: foo\nId: bar\nOK")] (Left $ Unexpected "[(\"cpos\",\"foo\"),(\"Id\",\"bar\")]") (plChangesPosId 10)+-} -- XXX: this is ill-defined {-@@ -293,7 +272,7 @@ testLoad = cmd_ [("load \"foo\"", Right "OK")] (load "foo") -testMove2 = cmd_ [("playlistmove \"foo\" 1 2", Right "OK")] (playlistMove "foo" 1 2)+testMove2 = cmd_ [("playlistmove \"foo\" 23 2", Right "OK")] (playlistMove "foo" (Id 23) 2) testRm = cmd_ [("rm \"foo\"", Right "OK")] (rm "foo") @@ -365,7 +344,7 @@ testPassword = cmd_ [("password foo", Right "OK")] (password "foo") testPasswordSucceeds =- testMPD convo expected_resp "foo" cmd_in `shouldBe` Ok+ testMPDWithPassword convo "foo" cmd_in `shouldBe` expected_resp where convo = [("lsinfo \"/\"", Right "ACK [4@0] {play} you don't have \ \permission for \"play\"")@@ -375,7 +354,7 @@ cmd_in = lsInfo "/" testPasswordFails =- testMPD convo expected_resp "foo" cmd_in `shouldBe` Ok+ testMPDWithPassword convo "foo" cmd_in `shouldBe` expected_resp where convo = [("play", Right "ACK [4@0] {play} you don't have \ \permission for \"play\"")@@ -397,14 +376,6 @@ -- Extensions\/shortcuts -- -testUpdateId0 = cmd [("update", Right "updating_db: 1")]- (Right 1)- (updateId [])--testUpdateId1 = cmd [("update \"foo\"", Right "updating_db: 1")]- (Right 1)- (updateId ["foo"])- testTogglePlay = cmd_ [("status", Right resp) ,("pause 1", Right "OK")]@@ -432,4 +403,4 @@ (addMany "foo" ["bar"]) testVolume = cmd_ [("status", Right st), ("setvol 90", Right "OK")] (volume (-10))- where st = unparse def { stVolume = 100 }+ where st = unparse def { stVolume = Just 100 }
− tests/Defaults.hs
@@ -1,21 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}--module Defaults where--import Data.Default-import Network.MPD.Commands.Types--instance Default Count where- def = defaultCount--instance Default Device where- def = defaultDevice---- XXX: note Song has no sensible default value--- XXX: or does it?--instance Default Stats where- def = defaultStats--instance Default Status where- def = defaultStatus
tests/EnvSpec.hs view
@@ -1,15 +1,14 @@ module EnvSpec (main, spec) where import TestUtil-import Test.Hspec.Monadic import Network.MPD import System.Posix.Env hiding (getEnvDefault) main :: IO ()-main = hspecX spec+main = hspec spec -spec :: Specs+spec :: Spec spec = do describe "getEnvDefault" $ do it "returns the value of an environment variable" $ do
tests/Main.hs view
@@ -1,14 +1,1 @@-module Main (main) where--import qualified CommandSpec-import qualified EnvSpec-import qualified ParserSpec-import qualified UtilSpec-import Test.Hspec.Monadic (describe, hspecX)--main :: IO ()-main = hspecX $ do- describe "CommandSpec" CommandSpec.spec- describe "EnvSpec" EnvSpec.spec- describe "ParserSpec" ParserSpec.spec- describe "UtilSpec" UtilSpec.spec+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
tests/ParserSpec.hs view
@@ -4,11 +4,9 @@ module ParserSpec (main, spec) where import Arbitrary ()-import Defaults () import Unparse -import Test.Hspec.Monadic-import Test.Hspec.HUnit ()+import Test.Hspec import Test.Hspec.QuickCheck (prop) import Network.MPD.Commands.Parse@@ -21,9 +19,9 @@ import Data.Time main :: IO ()-main = hspecX spec+main = hspec spec -spec :: Specs+spec :: Spec spec = do describe "parseIso8601" $ do prop "parses dates in ISO8601 format" prop_parseIso8601
tests/StringConn.hs view
@@ -3,7 +3,7 @@ -- | Module : StringConn -- Copyright : (c) Ben Sinclair 2005-2009--- License : LGPL (see LICENSE)+-- License : MIT (see LICENSE) -- Maintainer : bsinclai@turing.une.edu.au -- Stability : alpha --@@ -35,26 +35,14 @@ | BadRequest StringMPDError deriving (Show, Eq) -newtype MatchError = MErr (Either StringMPDError MPDError)-instance Error MatchError where- strMsg = MErr . Right . strMsg- newtype StringMPD a =- SMPD { runSMPD :: ErrorT MatchError+ SMPD { runSMPD :: ErrorT MPDError (StateT [(Expect, Response String)] (ReaderT Password Identity)) a- } deriving (Functor, Monad)--instance MonadError MPDError StringMPD where- throwError = SMPD . throwError . MErr . Right- catchError m f = SMPD $ catchError (runSMPD m) handler- where- handler err@(MErr (Left _)) = throwError err- handler (MErr (Right err)) = runSMPD (f err)+ } deriving (Functor, Monad, MonadError MPDError) instance MonadMPD StringMPD where getVersion = error "StringConn.getVersion: undefined"- getHandle = error "StringConn.getHandle: undefined" setPassword = error "StringConn.setPassword: undefined" open = return ()@@ -64,33 +52,24 @@ SMPD $ do ~pairs@((expected_request,response):rest) <- get when (null pairs)- (throwError . MErr $ Left TooManyRequests)+ (throwError $ Custom "too many requests") when (expected_request /= request)- (throwError . MErr . Left- $ UnexpectedRequest expected_request request)+ (throwError . Custom $ "unexpected request: " ++ show request ++ ", expected: " ++ show expected_request) put rest- either (throwError . MErr . Right) (return . B.lines . UTF8.fromString) response+ either throwError (return . B.lines . UTF8.fromString) response +testMPD :: (Eq a) => [(Expect, Response String)] -> StringMPD a -> Response a+testMPD pairs m = testMPDWithPassword pairs "" m+ -- | Run an action against a set of expected requests and responses, -- and an expected result. The result is Nothing if everything matched -- what was expected. If anything differed the result of the -- computation is returned along with pairs of expected and received -- requests.-testMPD :: (Eq a)+testMPDWithPassword :: (Eq a) => [(Expect, Response String)] -- ^ The expected requests and their -- ^ corresponding responses.- -> Response a -- ^ The expected final result. -> Password -- ^ A password to be supplied. -> StringMPD a -- ^ The MPD action to run.- -> Result a-testMPD pairs expected passwd m =- let result = runIdentity- $ runReaderT (evalStateT (runErrorT $ runSMPD m) pairs) passwd- in case result of- Right r- | Right r == expected -> Ok- | otherwise -> BadResult expected (Right r)- Left (MErr (Right r))- | Left r == expected -> Ok- | otherwise -> BadResult expected (Left r)- Left (MErr (Left e)) -> BadRequest e+ -> Response a+testMPDWithPassword pairs passwd m = runIdentity $ runReaderT (evalStateT (runErrorT $ runSMPD m) pairs) passwd
tests/TestUtil.hs view
@@ -1,7 +1,21 @@-module TestUtil (shouldBe) where+module TestUtil (+ with+, withPassword+, module StringConn+, module Test.Hspec+) where -import Test.Hspec.HUnit ()-import Test.HUnit+import Network.MPD.Core+import Network.MPD.Applicative -shouldBe :: (Eq a, Show a) => a -> a -> Assertion-shouldBe = (@?=)+import Test.Hspec+import StringConn++with :: Eq a => Command a -> [(Expect, Response String)] -> Response a+with = flip testMPD . runCommand++withPassword :: Eq a => Password+ -> [(Expect, Response String)]+ -> Command a+ -> Response a+withPassword pwd ps m = testMPDWithPassword ps pwd (runCommand m)
tests/Unparse.hs view
@@ -9,6 +9,10 @@ class Unparse parsed where unparse :: parsed -> String +instance Unparse a => Unparse (Maybe a) where+ unparse Nothing = ""+ unparse (Just x) = unparse x+ instance Unparse Count where unparse x = unlines [ "songs: " ++ show (cSongs x)@@ -54,22 +58,21 @@ [ "state: " ++ (case stState s of Playing -> "play" Paused -> "pause" _ -> "stop")- , "volume: " ++ show (stVolume s)+ , "volume: " ++ maybe "-1" show (stVolume s) , "repeat: " ++ showBool (stRepeat s) , "random: " ++ showBool (stRandom s) , "playlist: " ++ show (stPlaylistVersion s) , "playlistlength: " ++ show (stPlaylistLength s) , "xfade: " ++ show (stXFadeWidth s)- , "time: " ++ (let (x, y) = stTime s in show x ++ ":" ++ show y)- , "bitrate: " ++ show (stBitrate s) , "xfade: " ++ show (stXFadeWidth s)- , "audio: " ++ (let (x, y, z) = stAudio s in show x ++ ":" ++ show y ++ ":" ++ show z)- , "updating_db: " ++ show (stUpdatingDb s)- , "error: " ++ show (stError s) , "single: " ++ showBool (stSingle s) , "consume: " ++ showBool (stConsume s) ]+ ++ maybe [] (\n -> ["updating_db: " ++ show n]) (stUpdatingDb s) ++ maybe [] (\n -> ["song: " ++ show n]) (stSongPos s) ++ maybe [] (\n -> ["songid: " ++ show n]) (stSongID s)+ ++ maybe [] (\n -> ["error: " ++ show n]) (stError s)+ ++ maybe [] (\(x, y) -> ["time: " ++ show x ++ ":" ++ show y]) (stTime s)+ ++ maybe [] (\n -> ["bitrate: " ++ show n]) (stBitrate s)
tests/UtilSpec.hs view
@@ -6,8 +6,6 @@ import Arbitrary import TestUtil -import Test.Hspec.Monadic-import Test.Hspec.HUnit () import Test.Hspec.QuickCheck (prop) import Test.QuickCheck @@ -19,9 +17,9 @@ import Network.MPD.Util main :: IO ()-main = hspecX spec+main = hspec spec -spec :: Specs+spec :: Spec spec = do describe "splitGroups" $ do it "breaks an association list into sublists" $ do