Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
system:homeautomation:home-assistant
python-python-mpd2
_service:obs_scm:python-mpd2-3.0.1.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File _service:obs_scm:python-mpd2-3.0.1.obscpio of Package python-python-mpd2
07070100000000000041ED0000000000000000000000035FE435FD00000000000000000000000000000000000000000000001A00000000python-mpd2-3.0.1/.github07070100000001000081A40000000000000000000000015FE435FD00000076000000000000000000000000000000000000002900000000python-mpd2-3.0.1/.github/dependabot.ymlversion: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" 07070100000002000041ED0000000000000000000000025FE435FD00000000000000000000000000000000000000000000002400000000python-mpd2-3.0.1/.github/workflows07070100000003000081A40000000000000000000000015FE435FD00000157000000000000000000000000000000000000002D00000000python-mpd2-3.0.1/.github/workflows/test.ymlname: "Test" on: pull_request: push: jobs: tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: cachix/install-nix-action@v12 with: nix_path: nixpkgs=https://github.com/NixOS/nixpkgs-channels/archive/nixpkgs-unstable.tar.gz - name: run tests run: nix-shell --run 'tox' 07070100000004000081A40000000000000000000000015FE435FD0000013E000000000000000000000000000000000000001D00000000python-mpd2-3.0.1/.gitignore*.py[cod] # Packages *.egg *.egg-info /.eggs/ /dist/ /build/ /eggs/ /parts/ /bin/ /var/ /sdist/ /develop-eggs/ /lib/ /lib64/ /include/ /local/ /.installed.cfg # Installer logs /pip-selfcheck.json /pip-log.txt # Unit test / coverage reports /.coverage /.tox /nosetests.xml /coverage_html/ # Sphinx docs /doc/_build 07070100000005000081A40000000000000000000000015FE435FD0000894B000000000000000000000000000000000000001A00000000python-mpd2-3.0.1/GPL.txt GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright (C) <year> <name of author> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>. 07070100000006000081A40000000000000000000000015FE435FD00000357000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/INSTALL.rstPyPI: ~~~~~ :: $ pip install python-mpd2 Debian ~~~~~~ Drop this line in */etc/apt/sources.list.d/python-mpd2.list*:: deb http://deb.kaliko.me/debian/ testing main deb-src http://deb.kaliko.me/debian/ testing main Import the gpg key as root:: $ wget -O - http://sima.azylum.org/sima.gpg | apt-key add - Key fingerprint:: 2255 310A D1A2 48A0 7B59 7638 065F E539 32DC 551D Controls with *apt-key finger*. Then simply update/install *python-mpd2* or *python3-mpd2* with apt or aptitude: Arch Linux ~~~~~~~~~~ Install `python-mpd2 <http://aur.archlinux.org/packages.php?ID=59276>`__ from AUR. Gentoo Linux ~~~~~~~~~~~~ Replaces the original python-mpd beginning with version 0.4.2:: $ emerge -av python-mpd FreeBSD ~~~~~~~ Install *py-mpd2*:: $ pkg_add -r py-mpd2 Packages for other distributions are welcome! 07070100000007000081A40000000000000000000000015FE435FD00001DE3000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/LICENSE.txt GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. 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 that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. 07070100000008000081A40000000000000000000000015FE435FD0000004A000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/MANIFEST.inexclude setup.cfg include *.txt include *.rst recursive-include doc *.rst 07070100000009000081A40000000000000000000000015FE435FD000001E2000000000000000000000000000000000000001B00000000python-mpd2-3.0.1/MakefilePYTHON ?= python3.8 REMOTE = git@github.com:Mic92/python-mpd2 VERSION = $(shell $(PYTHON) -c "import mpd; print('.'.join(map(str,mpd.VERSION)))") test: $(PYTHON) setup.py test release: test git tag "v$(VERSION)" git push --tags git@github.com:Mic92/python-mpd2 master $(PYTHON) setup.py sdist bdist_wheel $(PYTHON) -m twine upload dist/python-mpd2-$(VERSION).tar.gz dist/python_mpd2-$(VERSION)-py2.py3-none-any.whl clean: $(PYTHON) setup.py clean .PHONY: test release clean 0707010000000A000081A40000000000000000000000015FE435FD00001092000000000000000000000000000000000000001D00000000python-mpd2-3.0.1/README.rstpython-mpd2 =========== .. image:: https://travis-ci.org/Mic92/python-mpd2.png?branch=master :target: http://travis-ci.org/Mic92/python-mpd2 :alt: Build Status *python-mpd2* is a Python library which provides a client interface for the `Music Player Daemon <http://musicpd.org>`__. Difference with python-mpd -------------------------- python-mpd2 is a fork of `python-mpd`_. While 0.4.x was backwards compatible with python-mpd, starting with 0.5 provides enhanced features which are *NOT* backward compatibles with the original `python-mpd`_ package (see `Porting Guide <https://python-mpd2.readthedocs.io/en/latest/topics/porting.html>`__ for more information). The following features were added: - Python 3 support (but you need at least Python 3.6) - asyncio/twisted support - support for the client-to-client protocol - support for new commands from MPD (seekcur, prio, prioid, config, searchadd, searchaddpl, listfiles, rangeid, addtagid, cleartagid, mount, umount, listmounts, listneighbors) - remove deprecated commands (volume) - explicitly declared MPD commands (which is handy when using for example `IPython <http://ipython.org>`__) - a test suite - API documentation to add new commands (see `Future Compatible <https://python-mpd2.readthedocs.io/en/latest/topics/advanced.html#future-compatible>`__) - support for Unicode strings in all commands (optionally in python2, default in python3 - see `Unicode Handling <https://python-mpd2.readthedocs.io/en/latest/topics/advanced.html#unicode-handling>`__) - configureable timeouts - support for `logging <https://python-mpd2.readthedocs.io/en/latest/topics/logging.html>`__ - improved support for sticker - improved support for ranges Getting the latest source code ------------------------------ If you would like to use the latest source code, you can grab a copy of the development version from Git by running the command:: $ git clone https://github.com/Mic92/python-mpd2.git Getting the latest release -------------------------- The latest stable release of *python-mpd2* can be found on `PyPI <http://pypi.python.org/pypi?:action=display&name=python-mpd2>`__ PyPI: ~~~~~ :: $ pip install python-mpd2 Installation in Linux/BSD distributions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Until Linux distributions adapt this package, here are some ready to use packages to test your applications: See `INSTALL.rst <INSTALL.rst>`__ Installing from source ---------------------- To install *python-mpd2* from source, simply run the command:: $ python setup.py install You can use the *--help* switch to *setup.py* for a complete list of commands and their options. See the `Installing Python Modules <http://docs.python.org/inst/inst.html>`__ document for more details. Documentation ------------- `Documentation <https://python-mpd2.readthedocs.io/en/latest/>`__ `Getting Started <https://python-mpd2.readthedocs.io/en/latest/topics/getting-started.html>`__ `Command Reference <https://python-mpd2.readthedocs.io/en/latest/topics/commands.html>`__ `Examples <examples>`__ Testing ------- Just run:: $ python setup.py test This will install `Tox <http://tox.testrun.org/>`__. Tox will take care of testing against all the supported Python versions (at least available) on our computer, with the required dependencies If you have nix, you can also use the provided `default.nix` to bring all supported python versions in scope using `nix-shell`. In that case run `tox` directly instead of using `setup.py`:: $ nix-shell --command 'tox' Building Documentation ---------------------- Install Sphinx:: $ easy_install -U Sphinx Change to the source directory and run:: $ python ./setup.py build_sphinx The command reference is generated from the official mpd protocol documentation. In order to update it, install python-lxml and run the following command:: $ python ./doc/generate_command_reference.py > ./doc/topics/commands.rst Contacting the author --------------------- Just contact me (Mic92) on Github or via email (joerg@thalheim.io). .. |Build Status| image:: https://travis-ci.org/Mic92/python-mpd2.png .. _python-mpd: https://pypi.python.org/pypi/python-mpd/ 0707010000000B000041ED0000000000000000000000045FE435FD00000000000000000000000000000000000000000000001600000000python-mpd2-3.0.1/doc0707010000000C000081A40000000000000000000000015FE435FD00001A7E000000000000000000000000000000000000001F00000000python-mpd2-3.0.1/doc/Makefile# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make <target>' where <target> is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-mpd2.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-mpd2.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/python-mpd2" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-mpd2" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 0707010000000D000041ED0000000000000000000000025FE435FD00000000000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/doc/_static0707010000000E000081A40000000000000000000000015FE435FD00000000000000000000000000000000000000000000002700000000python-mpd2-3.0.1/doc/_static/.gitkeep0707010000000F000081A40000000000000000000000015FE435FD00001049000000000000000000000000000000000000002200000000python-mpd2-3.0.1/doc/changes.rstpython-mpd2 Changes List ======================== Changes in v3.0.1 ----------------- * 3.0.0 accidentially introduced typing annotation that were not meant to be published yet. Changes in v3.0.0 ----------------- * Breaking changes: albumart now returns dictionary :code:`{"size": "...", "binary": b"..."}` instead of just a string * add readpicture command * add partition, newpartition and delpartition commands * add moveoutput command * removed deprecated `send_` and `fetch_` commands. Use the asyncio or twisted API instead for asynchronous mpd commands. Changes in v2.0.0 ----------------- * Minimum python version was increased to python3.6, python2.7 support was dropped * asyncio: fix parsing delimiters * add support for albumart command Changes in v1.1.0 ----------------- * Fix list command to work with grouping. Always returns list of dictionaries now. Make sure to adopt your code since this is an API change. * fix compatibility with python3.9 * fix connecting to unix socket in asyncio version * close asyncio transports on disconnect * create TCP socket with TCP_NODELAY for better responsiveness Changes in v1.0.0 ----------------- * Add support for twisted * Add support for asyncio * Use @property and @property.setter for MPDClient.timeout * Deprecate send_* and fetch_* variants of MPD commands: Consider using asyncio/twisted instead * Port argument is optional when connecting via unix sockets. * python-mpd will now raise mpd.ConnectionError instead of socket.error, when connection is lost * Add command outputvolume for forked-daapd Changes in v0.5.5 ----------------- * fix sended newlines on windows systems * include tests in source distribution Changes in v0.5.4 ----------------- * support for listfiles, rangeid, addtagid, cleartagid, mount, umount, listmounts, listneighbors Changes in v0.5.3 ----------------- * noidle command does returns pending changes now Changes in v0.5.2 ----------------- * add support for readcomments and toggleoutput Changes in v0.5.1 ----------------- * add support for ranges Changes in 0.5.0 ---------------- * improved support for sticker Changes in 0.4.6 ---------------- * enforce utf8 for encoding/decoding in python3 Changes in 0.4.5 ---------------- * support for logging Changes in 0.4.4 ---------------- * fix cleanup after broken connection * deprecate timeout parameter added in v0.4.2 * add timeout and idletimeout property Changes in 0.4.3 ---------------- * add searchadd and searchaddpl command * fix commands without a callback function * transform MPDClient to new style class Changes in 0.4.2 ---------------- * backward compatible unicode handling * added optional socket timeout parameter Changes in 0.4.1 ---------------- * prio and prioid was spelled wrong * added config command * remove deprecated volume command Changes in 0.4.0 ---------------- * python3 support (python2.6 is minimum python version required) * support for the upcoming client-to-client protocol * added new commands of mpd (seekcur, prior, priorid) * methods are explicit declared now, so they are shown in ipython * added unit tests * documented API to add new commands (see Future Compatible) Changes in 0.3.0 ---------------- * added replay_gain_mode and replay_gain_status commands * added mixrampdb and mixrampdelay commands * added findadd and rescan commands * added decoders command * changed license to LGPL * added sticker commands * correctly handle errors in command lists (fixes a longstanding bug) * raise IteratingError instead of breaking horribly when called wrong * added fileno() to export socket FD (for polling with select et al.) * asynchronous API (use send_<cmd> to queue, fetch_<cmd> to retrieve) * support for connecting to unix domain sockets * added consume and single commands * added idle and noidle commands * added listplaylists command Changes in 0.2.1 ---------------- * connect() no longer broken on Windows Changes in 0.2.0 ---------------- * support for IPv6 and multi-homed hostnames * connect() will fail if already connected * commands may now raise ConnectionError * addid and update may now return None 07070100000010000081A40000000000000000000000015FE435FD000000E8000000000000000000000000000000000000002A00000000python-mpd2-3.0.1/doc/commands_header.txt======== Commands ======== .. note:: Each command have a *send_* and a *fetch_* variant, which allows to send a MPD command and then fetch the result later. See :ref:`getting-started` for examples and more information. 07070100000011000081A40000000000000000000000015FE435FD00001F87000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/doc/conf.py# -*- coding: utf-8 -*- # # python-mpd2 documentation build configuration file, created by # sphinx-quickstart on Thu Apr 4 09:22:21 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import mpd # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-mpd2' copyright = u'2013, Jörg Thalheim' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = ".".join(map(str, mpd.VERSION)) # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a <link> tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-mpd2doc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'python-mpd2.tex', u'python-mpd2 Documentation', u'Jörg Thalheim', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-mpd2', u'python-mpd2 Documentation', [u'Jörg Thalheim'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'python-mpd2', u'python-mpd2 Documentation', u'Jörg Thalheim', 'python-mpd2', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False 07070100000012000081A40000000000000000000000015FE435FD00000FE1000000000000000000000000000000000000003400000000python-mpd2-3.0.1/doc/generate_command_reference.py#!/usr/bin/env python import re import sys import os.path from textwrap import TextWrapper import urllib.request try: from lxml import etree except ImportError: sys.stderr.write("Please install lxml to run this script.") sys.exit(1) DEPRECATED_COMMANDS = [] SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) def get_text(elements, itemize=False): paragraphs = [] highlight_elements = ['varname', 'parameter'] strip_elements = [ 'returnvalue', 'command', 'link', 'footnote', 'simpara', 'footnoteref', 'function' ] + highlight_elements for element in elements: # put "Since MPD version..." in paranthese etree.strip_tags(element, "application") for e in element.xpath("footnote/simpara"): e.text = "(" + e.text.strip() + ")" for e in element.xpath("|".join(highlight_elements)): e.text = "*" + e.text.strip() + "*" etree.strip_tags(element, *strip_elements) if itemize: initial_indent = " * " subsequent_indent = " " else: initial_indent = " " subsequent_indent = " " wrapper = TextWrapper(subsequent_indent=subsequent_indent, initial_indent=initial_indent) text = element.text.replace("\n", " ").strip() text = re.subn(r'\s+', ' ', text)[0] paragraphs.append(wrapper.fill(text)) return "\n\n".join(paragraphs) def main(url): header_file = os.path.join(SCRIPT_PATH, "commands_header.txt") with open(header_file, 'r') as f: print(f.read()) r = urllib.request.urlopen(url) tree = etree.parse(r) chapter = tree.xpath('/book/chapter[@id="command_reference"]')[0] for section in chapter.xpath("section"): title = section.xpath("title")[0].text print(title) print(len(title) * "-") print(get_text(section.xpath("para"))) print("") for entry in section.xpath("variablelist/varlistentry"): cmd = entry.xpath("term/cmdsynopsis/command")[0].text if cmd in DEPRECATED_COMMANDS: continue subcommand = "" args = "" begin_optional = False first_argument = True for arg in entry.xpath("term/cmdsynopsis/arg"): choice = arg.attrib.get("choice", None) if choice == "opt" and not begin_optional: begin_optional = True args += "[" if args != "" and args != "[": args += ", " replaceables = arg.xpath("replaceable") if len(replaceables) > 0: for replaceable in replaceables: args += replaceable.text.lower() elif first_argument: subcommand = arg.text else: args += '"{}"'.format(arg.text) first_argument = False if begin_optional: args += "]" if subcommand != "": cmd += "_" + subcommand print(".. function:: MPDClient." + cmd + "(" + args + ")") description = get_text(entry.xpath("listitem/para")) description = re.sub(r':$', r'::', description, flags=re.MULTILINE) print("\n") print(description) print("\n") for screen in entry.xpath("listitem/screen | listitem/programlisting"): for line in screen.text.split("\n"): print(" " + line) for item in entry.xpath("listitem/itemizedlist/listitem"): print(get_text(item.xpath("para"), itemize=True)) print("\n") if __name__ == "__main__": url = "https://raw.githubusercontent.com/MusicPlayerDaemon/MPD/master/doc/protocol.xml" if len(sys.argv) > 1: url += "?id=release-" + sys.argv[1] main(url) 07070100000013000081A40000000000000000000000015FE435FD000006B7000000000000000000000000000000000000002000000000python-mpd2-3.0.1/doc/index.rstpython-mpd2 Documentation ========================= *python-mpd2* is a Python library which provides a client interface for the `Music Player Daemon <http://musicpd.org>`__. Difference with python-mpd -------------------------- python-mpd2 is a fork of `python-mpd`_. While 0.4.x was backwards compatible with python-mpd, starting with 0.5 provides enhanced features which are *NOT* backward compatibles with the original `python-mpd`_ package. See :doc:`Porting <topics/porting>` for more information. The following features were added: - Python 3 support (but you need at least Python 2.7 or 3.4) - asyncio/twisted support - support for the client-to-client protocol - support for new commands from MPD v0.17 (seekcur, prio, prioid, config, searchadd, searchaddpl) and MPD v0.18 (readcomments, toggleoutput) - remove deprecated commands (volume) - explicitly declared MPD commands (which is handy when using for example `IPython <http://ipython.org>`__) - a test suite - API documentation to add new commands (see :doc:`Future Compatible <topics/advanced>`) - support for Unicode strings in all commands (optionally in python2, default in python3 - see :doc:`Unicode Handling <topics/advanced>`) - configurable timeouts - support for :doc:`logging <topics/logging>` - improved support for sticker - improved support for ranges Getting Started =============== A quick guide for getting started python-mpd2: * :doc:`Getting Started <topics/getting-started>` .. _python-mpd: https://pypi.python.org/pypi/python-mpd/ Command Reference ================= A complete list of all available commands: * :doc:`Commands <topics/commands>` Changelog ========= * :doc:`Change log <changes>` 07070100000014000041ED0000000000000000000000025FE435FD00000000000000000000000000000000000000000000001D00000000python-mpd2-3.0.1/doc/topics07070100000015000081A40000000000000000000000015FE435FD0000055E000000000000000000000000000000000000002A00000000python-mpd2-3.0.1/doc/topics/advanced.rstFuture Compatible ----------------- New commands or special handling of commands can be easily implemented. Use ``add_command()`` or ``remove_command()`` to modify the commands of the *MPDClient* class and all its instances.:: def fetch_cover(client): """"Take a MPDClient instance as its arguments and return mimetype and image""" # this command may come in the future. pass client.add_command("get_cover", fetch_cover) # you can then use: client.get_cover() # remove the command, because it doesn't exist already. client.remove_command("get_cover") Thread-Safety ------------- Currently ``MPDClient`` is **NOT** thread-safe. As it use a socket internaly, only one thread can send or receive at the time. But ``MPDClient`` can be easily extended to be thread-safe using `locks <http://docs.python.org/library/threading.html#lock-objects>`__. Take a look at ``examples/locking.py`` for further informations. Unicode Handling ---------------- To quote the `mpd protocol documentation <https://www.musicpd.org/doc/protocol/request_syntax.html>`_: > All data between the client and the server is encoded in UTF-8. With Python 3: ~~~~~~~~~~~~~~ In Python 3, Unicode string is the default string type. So just pass these strings as arguments for MPD commands and *python-mpd2* will also return such Unicode string. 07070100000016000081A40000000000000000000000015FE435FD00006A0A000000000000000000000000000000000000002A00000000python-mpd2-3.0.1/doc/topics/commands.rst======== Commands ======== .. note:: Each command have a *send_* and a *fetch_* variant, which allows to send a MPD command and then fetch the result later. See :ref:`getting-started` for examples and more information. Querying --------- .. function:: MPDClient.clearerror() Clears the current error message in status (this is also accomplished by any command that starts playback). .. function:: MPDClient.currentsong() Returns the song info of the current song (same song that is identified in status). .. function:: MPDClient.idle([subsystems]) (Introduced with MPD 0.14) Waits until there is a noteworthy change in one or more of MPD's subsystems. As soon as there is one, it lists all changed systems in a line in the format changed:: SUBSYSTEM, where SUBSYSTEM is one of the following:: While a client is waiting for idle results, the server disables timeouts, allowing a client to wait for events as long as mpd runs. The idle command can be canceled by sending the command noidle (no other commands are allowed). MPD will then leave idle mode and print results immediately; might be empty at this time. If the optional *SUBSYSTEMS* argument is used, MPD will only send notifications when something changed in one of the specified subsytems. * database: the song database has been modified after update. * update: a database update has started or finished. If the database was modified during the update, the database event is also emitted. * stored_playlist: a stored playlist has been modified, renamed, created or deleted * playlist: the current playlist has been modified * player: the player has been started, stopped or seeked * mixer: the volume has been changed * output: an audio output has been enabled or disabled * options: options like * partition: a partition was added, removed or changed * sticker: the sticker database has been modified. * subscription: a client has subscribed or unsubscribed to a channel * message: a message was received on a channel this client is subscribed to; this event is only emitted when the queue is empty .. function:: MPDClient.status() Returns the current status of the player and the volume level. * *partition*: the name of the current partition * *volume*: 0-100 * *repeat*: 0 or 1 * *random*: 0 or 1 * *single*: (Introduced with MPD 0.15) 0 or 1 * *consume*: 0 or 1 * *playlist*: 31-bit unsigned integer, the playlist version number * *playlistlength*: integer, the length of the playlist * *state*: play, stop, or pause * *song*: playlist song number of the current song stopped on or playing * *songid*: playlist songid of the current song stopped on or playing * *nextsong*: playlist song number of the next song to be played * *nextsongid*: playlist songid of the next song to be played * *time*: total time elapsed (of current playing/paused song) * *elapsed*: (Introduced with MPD 0.16) Total time elapsed within the current song, but with higher resolution. * *duration*: (Introduced with MPD 0.20) Duration of the current song in seconds. * *bitrate*: instantaneous bitrate in kbps * *xfade*: crossfade in seconds * *mixrampdb*: mixramp threshold in dB * *mixrampdelay*: mixrampdelay in seconds * *audio*: sampleRate:bits:channels * *updating_db*: job id * *error*: if there is an error, returns message here .. function:: MPDClient.stats() Displays statistics. * *artists*: number of artists * *albums*: number of albums * *songs*: number of songs * *uptime*: daemon uptime in seconds * *db_playtime*: sum of all song times in the db * *db_update*: last db update in UNIX time * *playtime*: time length of music played Playback options ---------------- .. function:: MPDClient.consume(state) Sets consume state to *STATE*, *STATE* should be 0 or 1. When consume is activated, each song played is removed from playlist. .. function:: MPDClient.crossfade(seconds) Sets crossfading between songs. .. function:: MPDClient.mixrampdb(decibels) Sets the threshold at which songs will be overlapped. Like crossfading but doesn't fade the track volume, just overlaps. The songs need to have MixRamp tags added by an external tool. 0dB is the normalized maximum volume so use negative values, I prefer -17dB. In the absence of mixramp tags crossfading will be used. See http://sourceforge.net/projects/mixramp .. function:: MPDClient.mixrampdelay(seconds) Additional time subtracted from the overlap calculated by mixrampdb. A value of "nan" disables MixRamp overlapping and falls back to crossfading. .. function:: MPDClient.random(state) Sets random state to *STATE*, *STATE* should be 0 or 1. .. function:: MPDClient.repeat(state) Sets repeat state to *STATE*, *STATE* should be 0 or 1. .. function:: MPDClient.setvol(vol) Sets volume to *VOL*, the range of volume is 0-100. .. function:: MPDClient.volume(vol_change) Changes volume by amount *VOL_CHANGE*, the range is -100 to +100. A negative value decreases volume, positive value increases volume. .. function:: MPDClient.single(state) Sets single state to *STATE*, *STATE* should be 0 or 1. When single is activated, playback is stopped after current song, or song is repeated if the 'repeat' mode is enabled. .. function:: MPDClient.replay_gain_mode(mode) Sets the replay gain mode. One of *off*, *track*, *album*, *auto* (added in MPD 0.16) . Changing the mode during playback may take several seconds, because the new settings does not affect the buffered data. This command triggers the options idle event. .. function:: MPDClient.replay_gain_status() Returns replay gain options. Currently, only the variable *replay_gain_mode* is returned. Controlling playback -------------------- .. function:: MPDClient.next() Plays next song in the playlist. .. function:: MPDClient.pause(pause) Toggles pause/resumes playing, *PAUSE* is 0 or 1. .. function:: MPDClient.play(songpos) Begins playing the playlist at song number *SONGPOS*. .. function:: MPDClient.playid(songid) Begins playing the playlist at song *SONGID*. .. function:: MPDClient.previous() Plays previous song in the playlist. .. function:: MPDClient.seek(songpos, time) Seeks to the position *TIME* (in seconds; fractions allowed) of entry *SONGPOS* in the playlist. .. function:: MPDClient.seekid(songid, time) Seeks to the position *TIME* (in seconds; fractions allowed) of song *SONGID*. .. function:: MPDClient.seekcur(time) Seeks to the position *TIME* (in seconds; fractions allowed) within the current song. If prefixed by '+' or '-', then the time is relative to the current playing position. .. function:: MPDClient.stop() Stops playing. The current playlist -------------------- .. function:: MPDClient.add(uri) Adds the file *URI* to the playlist (directories add recursively). *URI* can also be a single file. .. function:: MPDClient.addid(uri, position) Adds a song to the playlist (non-recursive) and returns the song id. *URI* is always a single file or URL. For example:: addid "foo.mp3" Id: 999 OK .. function:: MPDClient.clear() Clears the current playlist. .. function:: MPDClient.delete(index_or_range) Deletes a song from the playlist based on the song's position in the playlist. .. function:: MPDClient.deleteid(songid) Deletes the song *SONGID* from the playlist .. function:: MPDClient.move(to) Moves the song at *FROM* or range of songs at *START:END* to *TO* in the playlist. (Ranges are supported since MPD 0.15) .. function:: MPDClient.moveid(from, to) Moves the song with *FROM* (songid) to *TO* (playlist index) in the playlist. If *TO* is negative, it is relative to the current song in the playlist (if there is one). .. function:: MPDClient.playlist() Displays the current playlist. .. function:: MPDClient.playlistfind(tag, needle) Finds songs in the current playlist with strict matching. .. function:: MPDClient.playlistid(songid) Returns a list of songs in the playlist. *SONGID* is optional and specifies a single song to display info for. .. function:: MPDClient.playlistinfo() Returns a list of all songs in the playlist, or if the optional argument is given, displays information only for the song *SONGPOS* or the range of songs *START:END* .. function:: MPDClient.playlistsearch(tag, needle) Returns case-insensitive search results for partial matches in the current playlist. .. function:: MPDClient.plchanges(version, start:end) Returns changed songs currently in the playlist since *VERSION*. Start and end positions may be given to limit the output to changes in the given range. To detect songs that were deleted at the end of the playlist, use playlistlength returned by status command. .. function:: MPDClient.plchangesposid(version, start:end) Returns changed songs currently in the playlist since *VERSION*. This function only returns the position and the id of the changed song, not the complete metadata. This is more bandwidth efficient. To detect songs that were deleted at the end of the playlist, use playlistlength returned by status command. .. function:: MPDClient.prio(priority, start:end) Set the priority of the specified songs. A higher priority means that it will be played first when "random" mode is enabled. A priority is an integer between 0 and 255. The default priority of new songs is 0. .. function:: MPDClient.prioid(priority, id) Same as prio, but address the songs with their id. .. function:: MPDClient.rangeid(id, start:end) (Since MPD 0.19) Specifies the portion of the song that shall be played. *START* and *END* are offsets in seconds (fractional seconds allowed); both are optional. Omitting both (i.e. sending just ":") means "remove the range, play everything". A song that is currently playing cannot be manipulated this way. .. function:: MPDClient.shuffle(start:end) Shuffles the current playlist. *START:END* is optional and specifies a range of songs. .. function:: MPDClient.swap(song1, song2) Swaps the positions of *SONG1* and *SONG2*. .. function:: MPDClient.swapid(song1, song2) Swaps the positions of *SONG1* and *SONG2* (both song ids). .. function:: MPDClient.addtagid(songid, tag, value) Adds a tag to the specified song. Editing song tags is only possible for remote songs. This change is volatile: it may be overwritten by tags received from the server, and the data is gone when the song gets removed from the queue. .. function:: MPDClient.cleartagid(songid[, tag]) Removes tags from the specified song. If *TAG* is not specified, then all tag values will be removed. Editing song tags is only possible for remote songs. Stored playlists ---------------- Playlists are stored inside the configured playlist directory. They are addressed with their file name (without the directory and without the Some of the commands described in this section can be used to run playlist plugins instead of the hard-coded simple .. function:: MPDClient.listplaylist(name) Returns a list of the songs in the playlist. Playlist plugins are supported. .. function:: MPDClient.listplaylistinfo(name) Returns a list of the songs with metadata in the playlist. Playlist plugins are supported. .. function:: MPDClient.listplaylists() Returns a list of the playlist in the playlist directory. After each playlist name the server sends its last modification time as attribute "Last-Modified" in ISO 8601 format. To avoid problems due to clock differences between clients and the server, clients should not compare this value with their local clock. .. function:: MPDClient.load(name[, start:end]) Loads the playlist into the current queue. Playlist plugins are supported. A range may be specified to load only a part of the playlist. .. function:: MPDClient.playlistadd(name, uri) Adds *URI* to the playlist .. function:: MPDClient.playlistclear(name) Clears the playlist .. function:: MPDClient.playlistdelete(name, songpos) Deletes *SONGPOS* from the playlist .. function:: MPDClient.playlistmove(name, from, to) Moves the song at position *FROM* in the playlist .. function:: MPDClient.rename(name, new_name) Renames the playlist .. function:: MPDClient.rm(name) Removes the playlist .. function:: MPDClient.save(name) Saves the current playlist to The music database ------------------ .. function:: MPDClient.albumart(uri) Returns the album art image for the given song. *URI* is always a single file or URL. The returned value is a dictionary containing the album art image in its ``'binary'`` entry. If the given URI is invalid, or the song does not have an album cover art file that MPD recognizes, a CommandError is thrown. .. function:: MPDClient.count(tag, needle[, ..., "group", grouptype]) Returns the counts of the number of songs and their total playtime in the db matching *TAG* exactly. The *group* keyword may be used to group the results by a tag. The following prints per-artist counts:: count group artist .. function:: MPDClient.find(type, what[, ..., startend]) Returns songs in the db that are exactly *WHAT*. *TYPE* can be any tag supported by MPD, or one of the special parameters:: *WHAT* is what to find. *window* can be used to query only a portion of the real response. The parameter is two zero-based record numbers; a start number and an end number. * *any* checks all tag values * *file* checks the full path (relative to the music directory) * *base* restricts the search to songs in the given directory (also relative to the music directory) * *modified-since* compares the file's time stamp with the given value (ISO 8601 or UNIX time stamp) .. function:: MPDClient.findadd(type, what[, ...]) Returns songs in the db that are exactly *WHAT* and adds them to current playlist. Parameters have the same meaning as for find. .. function:: MPDClient.list(type[, filtertype, filterwhat, ..., "group", grouptype, ...]) Returns a list of unique tag values of the specified type. *TYPE* can be any tag supported by MPD or *file*. Additional arguments may specify a filter like the one in the find command. The *group* keyword may be used (repeatedly) to group the results by one or more tags. The following example lists all album names, grouped by their respective (album) artist:: list album group albumartist .. function:: MPDClient.listall(uri) Returns a lists of all songs and directories in *URI*. Do not use this command. Do not manage a client-side copy of MPD's database. That is fragile and adds huge overhead. It will break with large databases. Instead, query MPD whenever you need something. .. function:: MPDClient.listallinfo(uri) Returns a lists of all songs and directories with their metadata info in *URI*. Same as listall, except it also returns metadata info in the same format as lsinfo. Do not use this command. Do not manage a client-side copy of MPD's database. That is fragile and adds huge overhead. It will break with large databases. Instead, query MPD whenever you need something. .. function:: MPDClient.listfiles(uri) Returns a list of the contents of the directory *URI*, including files are not recognized by MPD. *URI* can be a path relative to the music directory or an URI understood by one of the storage plugins. The response contains at least one line for each directory entry with the prefix "file: " or "directory: ", and may be followed by file attributes such as "Last-Modified" and "size". For example, "smb://SERVER" returns a list of all shares on the given SMB/CIFS server; "nfs://servername/path" obtains a directory listing from the NFS server. .. function:: MPDClient.lsinfo(uri) Returns a list of the contents of the directory *URI*. When listing the root directory, this currently returns the list of stored playlists. This behavior is deprecated; use "listplaylists" instead. This command may be used to list metadata of remote files (e.g. URI beginning with "http://" or "smb://"). Clients that are connected via UNIX domain socket may use this command to read the tags of an arbitrary local file (URI is an absolute path). .. function:: MPDClient.readcomments(uri) Returns "comments" (i.e. key-value pairs) from the file specified by "URI". This "URI" can be a path relative to the music directory or an absolute path. This command may be used to list metadata of remote files (e.g. URI beginning with "http://" or "smb://"). The response consists of lines in the form "KEY: VALUE". Comments with suspicious characters (e.g. newlines) are ignored silently. The meaning of these depends on the codec, and not all decoder plugins support it. For example, on Ogg files, this lists the Vorbis comments. .. function:: MPDClient.readpicture(uri) Returns the embedded cover image for the given song. *URI* is always a single file or URL. The returned value is a dictionary containing the embedded cover image in its ``'binary'`` entry, and potentially the picture's MIME type in its ``'type'`` entry. If the given URI is invalid, a CommandError is thrown. If the given song URI exists, but the song does not have an embedded cover image that MPD recognizes, an empty dictionary is returned. .. function:: MPDClient.search(type, what[, ..., startend]) Returns results of a search for any song that contains *WHAT*. Parameters have the same meaning as for find, except that search is not case sensitive. .. function:: MPDClient.searchadd(type, what[, ...]) Searches for any song that contains *WHAT* in tag *TYPE* and adds them to current playlist. Parameters have the same meaning as for find, except that search is not case sensitive. .. function:: MPDClient.searchaddpl(name, type, what[, ...]) Searches for any song that contains *WHAT* in tag *TYPE* and adds them to the playlist named *NAME*. If a playlist by that name doesn't exist it is created. Parameters have the same meaning as for find, except that search is not case sensitive. .. function:: MPDClient.update([uri]) Updates the music database: find new files, remove deleted files, update modified files. *URI* is a particular directory or song/file to update. If you do not specify it, everything is updated. Prints "updating_db: JOBID" where *JOBID* is a positive number identifying the update job. You can read the current job id in the status response. .. function:: MPDClient.rescan([uri]) Same as update, but also rescans unmodified files. Mounts and neighbors -------------------- A "storage" provides access to files in a directory tree. The most basic storage plugin is the "local" storage plugin which accesses the local file system, and there are plugins to access NFS and SMB servers. Multiple storages can be "mounted" together, similar to the mount command on many operating systems, but without cooperation from the kernel. No superuser privileges are necessary, beause this mapping exists only inside the MPD process .. function:: MPDClient.mount(path, uri) Mount the specified remote storage URI at the given path. Example:: mount foo nfs://192.168.1.4/export/mp3 .. function:: MPDClient.unmount(path) Unmounts the specified path. Example:: unmount foo .. function:: MPDClient.listmounts() Returns a list of all mounts. By default, this contains just the configured *music_directory*. Example:: listmounts mount: storage: /home/foo/music mount: foo storage: nfs://192.168.1.4/export/mp3 OK .. function:: MPDClient.listneighbors() Returns a list of "neighbors" (e.g. accessible file servers on the local net). Items on that list may be used with the mount command. Example:: listneighbors neighbor: smb://FOO name: FOO (Samba 4.1.11-Debian) OK Stickers -------- "Stickers" are pieces of information attached to existing MPD objects (e.g. song files, directories, albums). Clients can create arbitrary name/value pairs. MPD itself does not assume any special meaning in them. The goal is to allow clients to share additional (possibly dynamic) information about songs, which is neither stored on the client (not available to other clients), nor stored in the song files (MPD has no write access). Client developers should create a standard for common sticker names, to ensure interoperability. Objects which may have stickers are addressed by their object type ("song" for song objects) and their URI (the path within the database for songs). .. function:: MPDClient.sticker_get(type, uri, name) Reads and returns a sticker value for the specified object. .. function:: MPDClient.sticker_set(type, uri, name, value) Adds a sticker value to the specified object. If a sticker item with that name already exists, it is replaced. .. function:: MPDClient.sticker_delete(type, uri[, name]) Deletes a sticker value from the specified object. If you do not specify a sticker name, all sticker values are deleted. .. function:: MPDClient.sticker_list(type, uri) Lists the stickers for the specified object. .. function:: MPDClient.sticker_find(type, uri, name) Searches the sticker database for stickers with the specified name, below the specified directory (URI). For each matching song, it prints the URI and that one sticker's value. .. function:: MPDClient.sticker_find(type, uri, name, "=", value) Returns the results of a search for stickers with the given value. Other supported operators are: "<", ">" Connection settings ------------------- .. function:: MPDClient.close() Closes the connection to MPD. MPD will try to send the remaining output buffer before it actually closes the connection, but that cannot be guaranteed. This command will not generate a response. .. function:: MPDClient.kill() Kills MPD. .. function:: MPDClient.password(password) This is used for authentication with the server. *PASSWORD* is simply the plaintext password. .. function:: MPDClient.ping() Does nothing but return "OK". Partition commands ------------------ These commands allow a client to inspect and manage "partitions". A partition is one frontend of a multi-player MPD process: it has separate queue, player and outputs. A client is assigned to one partition at a time. .. function:: MPDClient.partition(name) Switch the client to a different partition. .. function:: MPDClient.listpartitions() Return a list of partitions. .. function:: MPDClient.newpartition(name) Create a new partition. .. function:: MPDClient.delpartition(name) Delete a partition. The partition must be empty (no connected clients and no outputs). .. function:: MPDClient.moveoutput(output_name) Move an output to the current partition. Audio output devices -------------------- .. function:: MPDClient.disableoutput(id) Turns an output off. .. function:: MPDClient.enableoutput(id) Turns an output on. .. function:: MPDClient.toggleoutput(id) Turns an output on or off, depending on the current state. .. function:: MPDClient.outputs() Returns information about all outputs:: outputid: 0 outputname: My ALSA Device outputenabled: 0 OK * *outputid*: ID of the output. May change between executions * *outputname*: Name of the output. It can be any. * *outputenabled*: Status of the output. 0 if disabled, 1 if enabled. Reflection ---------- .. function:: MPDClient.config() Returns a dump of all configuration values that may be interesting for the client. This command is only permitted to "local" clients (connected via UNIX domain socket). The following response attributes are available:: .. function:: MPDClient.commands() Returns which commands the current user has access to. .. function:: MPDClient.notcommands() Returns which commands the current user does not have access to. .. function:: MPDClient.tagtypes() Returns a list of available song metadata. .. function:: MPDClient.urlhandlers() Returns a list of available URL handlers. .. function:: MPDClient.decoders() Returns a list of decoder plugins, followed by their supported suffixes and MIME types. Example response:: plugin: mad suffix: mp3 suffix: mp2 mime_type: audio/mpeg plugin: mpcdec suffix: mpc Client to client ---------------- Clients can communicate with each others over "channels". A channel is created by a client subscribing to it. More than one client can be subscribed to a channel at a time; all of them will receive the messages which get sent to it. Each time a client subscribes or unsubscribes, the global idle event *subscription* is generated. In conjunction with the channels command, this may be used to auto-detect clients providing additional services. New messages are indicated by the *message* idle event. .. function:: MPDClient.subscribe(name) Subscribe to a channel. The channel is created if it does not exist already. The name may consist of alphanumeric ASCII characters plus underscore, dash, dot and colon. .. function:: MPDClient.unsubscribe(name) Unsubscribe from a channel. .. function:: MPDClient.channels() Obtains and returns a list of all channels. The response is a list of "channel:" lines. .. function:: MPDClient.readmessages() Reads messages for this client. The response is a list of "channel:" and "message:" lines. .. function:: MPDClient.sendmessage(channel, text) Send a message to the specified channel. 07070100000017000081A40000000000000000000000015FE435FD00000BA3000000000000000000000000000000000000003100000000python-mpd2-3.0.1/doc/topics/getting-started.rst.. _getting-started: Using the client library ------------------------ The client library can be used as follows:: >>> from mpd import MPDClient >>> client = MPDClient() # create client object >>> client.timeout = 10 # network timeout in seconds (floats allowed), default: None >>> client.idletimeout = None # timeout for fetching the result of the idle command is handled seperately, default: None >>> client.connect("localhost", 6600) # connect to localhost:6600 >>> print(client.mpd_version) # print the MPD version >>> print(client.find("any", "house")) # print result of the command "find any house" >>> client.close() # send the close command >>> client.disconnect() # disconnect from the server A list of supported commands, their arguments (as MPD currently understands them), and the functions used to parse their responses can be found in :doc:`Commands <commands>`. See the `MPD protocol documentation <http://www.musicpd.org/doc/protocol/>`__ for more details. Command lists are also supported using *command\_list\_ok\_begin()* and *command\_list\_end()*:: >>> client.command_list_ok_begin() # start a command list >>> client.update() # insert the update command into the list >>> client.status() # insert the status command into the list >>> results = client.command_list_end() # results will be a list with the results Commands may also return iterators instead of lists if *iterate* is set to *True*:: client.iterate = True for song in client.playlistinfo(): print song["file"] Each command have a *send\_* and a *fetch\_* variant, which allows to send a MPD command and then fetch the result later. This is useful for the idle command:: >>> client.send_idle() # do something else or use function like select(): http://docs.python.org/howto/sockets.html#non-blocking-sockets # ex. select([client], [], []) or with gobject: http://jatreuman.indefero.net/p/python-mpd/page/ExampleIdle/ >>> events = client.fetch_idle() Some more complex usage examples can be found `here <http://jatreuman.indefero.net/p/python-mpd/doc/>`_ Some commands support integer ranges as argument. This is done in python-mpd2 by using two element tuple:: # move the first three songs # after the last in the playlist >>> client.status() ['file: song1.mp3', 'file: song2.mp3', 'file: song3.mp3', 'file: song4.mp3'] >>> client.move((0,3), 1) >>> client.status() ['file: song4.mp3' 'file: song1.mp3', 'file: song2.mp3', 'file: song3.mp3',] Second element can be omitted. MPD will assumes the biggest possible number then (don't forget the comma!):: NOTE: mpd versions between 0.16.8 and 0.17.3 contains a bug, so ommiting doesn't work. >>> client.delete((1,)) # delete all songs, but the first. 07070100000018000081A40000000000000000000000015FE435FD000001E1000000000000000000000000000000000000002900000000python-mpd2-3.0.1/doc/topics/logging.rstLogging ------- By default messages are sent to the logger named ``mpd``:: >>> import logging, mpd >>> logging.basicConfig(level=logging.DEBUG) >>> client = mpd.MPDClient() >>> client.connect("localhost", 6600) INFO:mpd:Calling MPD connect('localhost', 6600, timeout=None) >>> client.find('any', 'dubstep') DEBUG:mpd:Calling MPD find('any', 'dubstep') For more information about logging configuration, see http://docs.python.org/2/howto/logging.html 07070100000019000081A40000000000000000000000015FE435FD0000059A000000000000000000000000000000000000002900000000python-mpd2-3.0.1/doc/topics/porting.rst============= Porting guide ============= Until the versions 0.4.x, `python-mpd2`_ was a drop-in replacement for application which were using the original `python-mpd`_. That is, you could just replace the package's content of the latter one by the former one, and *things should just work*. However, starting from version 0.5, `python-mpd2`_ provides enhanced features which are *NOT* backward compatibles with the original `python-mpd`_ package. This goal of this document is to explains the differences the releases and if it makes sense, how to migrate from one version to another. Stickers API ============ When fetching stickers, `python-mpd2`_ used to return mostly the raw results MPD was providing:: >>> client.sticker_get('song', 'foo.mp3', 'my-sticker') 'my-sticker=some value' >>> client.sticker_list('song', 'foo.mp3') ['my-sticker=some value', 'foo=bar'] Starting from version 0.5, `python-mpd2`_ provides a higher-level representation of the stickers' content:: >>> client.sticker_get('song', 'foo.mp3', 'my-sticker') 'some value' >>> client.sticker_list('song', 'foo.mp3') {'my-sticker': 'some value', 'foo': 'bar'} This removes the burden from the application to do the interpretation of the stickers' content by itself. .. versionadded:: 0.5 .. _python-mpd: http://jatreuman.indefero.net/p/python-mpd/ .. _python-mpd2: https://github.com/Mic92/python-mpd2/ .. vim:ft=rst 0707010000001A000041ED0000000000000000000000025FE435FD00000000000000000000000000000000000000000000001B00000000python-mpd2-3.0.1/examples0707010000001B000081A40000000000000000000000015FE435FD000005D7000000000000000000000000000000000000002E00000000python-mpd2-3.0.1/examples/asyncio_example.pyimport asyncio from mpd.asyncio import MPDClient async def main(): print("Create MPD client") client = MPDClient() try: await client.connect('localhost', 6600) except Exception as e: print("Connection failed:", e) return print("Connected to MPD version", client.mpd_version) try: status = await client.status() except Exception as e: print("Status error:", e) return else: print("Status success:", status) print(list(await client.commands())) import time start = time.time() for x in await client.listall(): print("sync:", x) print("Time to first sync:", time.time() - start) break start = time.time() async for x in client.listall(): print("async:", x) print("Time to first async:", time.time() - start) break try: await client.addid() except Exception as e: print("An erroneous command, as expected, raised:", e) try: async for x in client.plchangesposid(): print("Why does this work?") except Exception as e: print("An erroneous asynchronously looped command, as expected, raised:", e) i = 0 async for subsystem in client.idle(): print("Idle change in", subsystem) i += 1 if i > 5: print("Enough changes, quitting") break if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main()) 0707010000001C000081A40000000000000000000000015FE435FD00000392000000000000000000000000000000000000002700000000python-mpd2-3.0.1/examples/coverart.py#!/usr/bin/env python # -*- coding: utf-8 -*- # IMPORTS from mpd import (MPDClient, CommandError) from socket import error as SocketError from sys import exit from PIL import Image from io import BytesIO ## SETTINGS ## HOST = 'localhost' PORT = '6600' PASSWORD = False SONG = '' ### client = MPDClient() try: client.connect(host=HOST, port=PORT) except SocketError: exit(1) if PASSWORD: try: client.password(PASSWORD) except CommandError: exit(1) try: cover_art = client.readpicture(SONG) except CommandError: exit(1) if 'binary' not in cover_art: # The song exists but has no embedded cover art print("No embedded art found!") exit(1) if 'type' in cover_art: print("Cover art of type " + cover_art['type']) with Image.open(BytesIO(cover_art['binary'])) as img: img.show() client.disconnect() # VIM MODLINE # vim: ai ts=4 sw=4 sts=4 expandtab 0707010000001D000081A40000000000000000000000015FE435FD00000F8F000000000000000000000000000000000000002C00000000python-mpd2-3.0.1/examples/errorhandling.py#! /usr/bin/env python # #Introduction # #A python program that continuously polls for song info. Demonstrates how and where to handle errors #Details # from mpd import MPDClient, MPDError, CommandError import sys class PollerError(Exception): """Fatal error in poller.""" class MPDPoller(object): def __init__(self, host="localhost", port="6600", password=None): self._host = host self._port = port self._password = password self._client = MPDClient() def connect(self): try: self._client.connect(self._host, self._port) # Catch socket errors except IOError as err: errno, strerror = err raise PollerError("Could not connect to '%s': %s" % (self._host, strerror)) # Catch all other possible errors # ConnectionError and ProtocolError are always fatal. Others may not # be, but we don't know how to handle them here, so treat them as if # they are instead of ignoring them. except MPDError as e: raise PollerError("Could not connect to '%s': %s" % (self._host, e)) if self._password: try: self._client.password(self._password) # Catch errors with the password command (e.g., wrong password) except CommandError as e: raise PollerError("Could not connect to '%s': " "password commmand failed: %s" % (self._host, e)) # Catch all other possible errors except (MPDError, IOError) as e: raise PollerError("Could not connect to '%s': " "error with password command: %s" % (self._host, e)) def disconnect(self): # Try to tell MPD we're closing the connection first try: self._client.close() # If that fails, don't worry, just ignore it and disconnect except (MPDError, IOError): pass try: self._client.disconnect() # Disconnecting failed, so use a new client object instead # This should never happen. If it does, something is seriously broken, # and the client object shouldn't be trusted to be re-used. except (MPDError, IOError): self._client = MPDClient() def poll(self): try: song = self._client.currentsong() # Couldn't get the current song, so try reconnecting and retrying except (MPDError, IOError): # No error handling required here # Our disconnect function catches all exceptions, and therefore # should never raise any. self.disconnect() try: self.connect() # Reconnecting failed except PollerError as e: raise PollerError("Reconnecting failed: %s" % e) try: song = self._client.currentsong() # Failed again, just give up except (MPDError, IOError) as e: raise PollerError("Couldn't retrieve current song: %s" % e) # Hurray! We got the current song without any errors! print(song) def main(): from time import sleep poller = MPDPoller() poller.connect() while True: poller.poll() sleep(3) if __name__ == "__main__": import sys try: main() # Catch fatal poller errors except PollerError as e: print("Fatal poller error: %s" % e, file=sys.stderr) sys.exit(1) # Catch all other non-exit errors except Exception as e: print("Unexpected exception: %s" % e, file=sys.stderr) sys.exit(1) # Catch the remaining exit errors except: sys.exit(0) # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 0707010000001E000081A40000000000000000000000015FE435FD000000E1000000000000000000000000000000000000002900000000python-mpd2-3.0.1/examples/helloworld.py#!/usr/bin/python import mpd client = mpd.MPDClient() client.connect("localhost", 6600) for entry in client.lsinfo("/"): print("%s" % entry) for key, value in client.status().items(): print("%s: %s" % (key, value)) 0707010000001F000081A40000000000000000000000015FE435FD0000050B000000000000000000000000000000000000002600000000python-mpd2-3.0.1/examples/locking.pyfrom threading import Lock, Thread from random import choice from mpd import MPDClient class LockableMPDClient(MPDClient): def __init__(self): super(LockableMPDClient, self).__init__() self._lock = Lock() def acquire(self): self._lock.acquire() def release(self): self._lock.release() def __enter__(self): self.acquire() def __exit__(self, type, value, traceback): self.release() client = LockableMPDClient() client.connect("localhost", 6600) # now whenever you need thread-safe access # use the 'with' statement like this: with client: # acquire lock status = client.status() # if you leave the block, the lock is released # it is recommend to leave it soon, # otherwise your other threads will blocked. # Let's test if it works .... def fetch_playlist(): for i in range(10): if choice([0, 1]) == 0: with client: song = client.currentsong() assert isinstance(song, dict) else: with client: playlist = client.playlist() assert isinstance(playlist, list) threads = [] for i in range(5): t = Thread(target=fetch_playlist) threads.append(t) t.start() for t in threads: t.join() print("Done...") 07070100000020000081A40000000000000000000000015FE435FD00000094000000000000000000000000000000000000002500000000python-mpd2-3.0.1/examples/logger.pyimport logging, mpd logging.basicConfig(level=logging.DEBUG) client = mpd.MPDClient() client.connect("localhost", 6600) client.find("any", "house") 07070100000021000081A40000000000000000000000015FE435FD00000217000000000000000000000000000000000000002800000000python-mpd2-3.0.1/examples/multitags.py#Multi tag files # #Some tag formats (such as ID3v2 and VorbisComment) support defining the same tag multiple times, mostly for when a song has multiple artists. MPD supports this, and sends each occurrence of a tag to the client. # #When python-mpd encounters the same tag more than once on the same song, it uses a list instead of a string. #Function to get a string only song object. def collapse_tags(song): for tag, value in song.iteritems(): if isinstance(value, list): song[tag] = ", ".join(set(value)) 07070100000022000081A40000000000000000000000015FE435FD00000239000000000000000000000000000000000000002A00000000python-mpd2-3.0.1/examples/randomqueue.py#!/usr/bin/env python # -*- coding: utf-8 -*- # IMPORTS from mpd import (MPDClient, CommandError) from random import choice from socket import error as SocketError from sys import exit ## SETTINGS ## HOST = 'localhost' PORT = '6600' PASSWORD = False ### client = MPDClient() try: client.connect(host=HOST, port=PORT) except SocketError: exit(1) if PASSWORD: try: client.password(PASSWORD) except CommandError: exit(1) client.add(choice(client.list('file'))) client.disconnect() # VIM MODLINE # vim: ai ts=4 sw=4 sts=4 expandtab 07070100000023000081A40000000000000000000000015FE435FD0000058E000000000000000000000000000000000000002400000000python-mpd2-3.0.1/examples/stats.py#!/usr/bin/env python # -*- coding: utf-8 -*- # IMPORTS import sys import pprint from mpd import (MPDClient, CommandError) from socket import error as SocketError HOST = 'localhost' PORT = '6600' PASSWORD = False ## CON_ID = {'host':HOST, 'port':PORT} ## ## Some functions def mpdConnect(client, con_id): """ Simple wrapper to connect MPD. """ try: client.connect(**con_id) except SocketError: return False return True def mpdAuth(client, secret): """ Authenticate """ try: client.password(secret) except CommandError: return False return True ## def main(): ## MPD object instance client = MPDClient() if mpdConnect(client, CON_ID): print('Got connected!') else: print('fail to connect MPD server.') sys.exit(1) # Auth if password is set non False if PASSWORD: if mpdAuth(client, PASSWORD): print('Pass auth!') else: print('Error trying to pass auth.') client.disconnect() sys.exit(2) ## Fancy output pp = pprint.PrettyPrinter(indent=4) ## Print out MPD stats & disconnect print('\nCurrent MPD state:') pp.pprint(client.status()) print('\nMusic Library stats:') pp.pprint(client.stats()) client.disconnect() sys.exit(0) # Script starts here if __name__ == "__main__": main() 07070100000024000081A40000000000000000000000015FE435FD00000B84000000000000000000000000000000000000002700000000python-mpd2-3.0.1/examples/stickers.py#Descriptio, file=sys.stderrn # #Using this client, one can manipulate and query stickers. The script is essentially a raw interface to the MPD protocol's sticker command, and is used in exactly the same way. #Examples ## set sticker "foo" to "bar" on "dir/song.mp3" #sticker.py set dir/song.mp3 foo bar # ## get sticker "foo" on "dir/song.mp3" #sticker.py get dir/song.mp3 foo # ## list all stickers on "dir/song.mp3" #sticker.py list dir/song.mp3 # ## find all files with sticker "foo" in "dir" #sticker.py find dir foo # ## find all files with sticker "foo" #sticker.py find / foo # ## delete sticker "foo" from "dir/song.mp3" #sticker.py delete dir/song.mp3 foo # #sticker.py #! /usr/bin/env python # Edit these HOST = "localhost" PORT = 6600 PASS = None from optparse import OptionParser from socket import error as SocketError from sys import stderr from mpd import MPDClient, MPDError ACTIONS = ("get", "set", "delete", "list", "find") def main(action, uri, name, value): client = MPDClient() client.connect(HOST, PORT) if PASS: client.password(PASS) if action == "get": print(client.sticker_get("song", uri, name)) if action == "set": client.sticker_set("song", uri, name, value) if action == "delete": client.sticker_delete("song", uri, name) if action == "list": stickers = client.sticker_list("song", uri) for sticker in stickers: print(sticker) if action == "find": matches = client.sticker_find("song", uri, name) for match in matches: if "file" in match: print(match["file"]) if __name__ == "__main__": parser = OptionParser(usage="%prog action args", version="0.1", description="Manipulate and query " "MPD song stickers.") options, args = parser.parse_args() if len(args) < 1: parser.error("no action specified, must be one of: %s" % " ".join(ACTIONS)) action = args.pop(0) if action not in ACTIONS: parser.error("action must be one of: %s" % " ".join(ACTIONS)) if len(args) < 1: parser.error("no URI specified") uri = args.pop(0) if action in ("get", "set", "delete", "find"): if len(args) < 1: parser.error("no name specified") name = args.pop(0) else: name = None if action == "set": if len(args) < 1: parser.error("no value specified") value = args.pop(0) else: value = None try: main(action, uri, name, value) except SocketError as e: print("%s: error with connection to MPD: %s" % \ (parser.get_prog_name(), e[1]), file=stderr) except MPDError as e: print("%s: error executing action: %s" % \ (parser.get_prog_name(), e), file=stderr) # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 07070100000025000081A40000000000000000000000015FE435FD000004D3000000000000000000000000000000000000002700000000python-mpd2-3.0.1/examples/summary.txt:Python scripts examples Here follows some scripts using python-mpd to connect and play with your MPD server. MPD server used in the script is localhost:6600, please adapt to your own configuration changing the proper var in the script header. Examples Print out general stats: ExampleStats Random queue: ExampleRandomQueue Handling errors: ExampleErrorhandling Deal with mutli-tag files. Some sound files may define the same tag multiple times, here is a function to deal with it in your client: ExampleMultiTags idle command (python-mpd > 0.3 & mpd > 0.14) ExampleIdle Manipulate and query stickers: ExampleStickers ExampleErrorhandling demo of handling errors in long-running client 2010-11-29 ExampleIdle Using idle command 2010-12-14 ExampleMultiTags How to deal with multi tag file 2009-09-15 ExampleRandomQueue Queue song at random 2009-09-24 ExampleStats Get general information of your MPD server 2009-09-12 ExampleStickers A command-line client for manipulating and querying stickers 2010-12-18 Examples Some example scripts to show how to play with python-mpd 2010-12-18 The asyncio_example.py shows how MPD can be used with the asyncio idioms; it requires at least Python 3.5 to run. 07070100000026000081A40000000000000000000000015FE435FD000005FA000000000000000000000000000000000000002E00000000python-mpd2-3.0.1/examples/twisted_example.pyfrom __future__ import print_function from mpd import MPDProtocol from twisted.internet import protocol from twisted.internet import reactor class MPDApp(object): # Example application which deals with MPD def __init__(self, protocol): self.protocol = protocol def __call__(self, result): # idle result callback print('Subsystems: {}'.format(list(result))) def status_success(result): # status query success print('Status success: {}'.format(result)) def status_error(result): # status query failure print('Status error: {}'.format(result)) # query player status self.protocol.status()\ .addCallback(status_success)\ .addErrback(status_error) class MPDClientFactory(protocol.ClientFactory): protocol = MPDProtocol def buildProtocol(self, addr): print('Create MPD protocol') protocol = self.protocol() protocol.factory = self protocol.idle_result = MPDApp(protocol) return protocol def clientConnectionFailed(self, connector, reason): print('Connection failed - goodbye!: {}'.format(reason)) reactor.stop() def clientConnectionLost(self, connector, reason): print('Connection lost - goodbye!: {}'.format(reason)) if reactor.running: reactor.stop() if __name__ == '__main__': factory = MPDClientFactory() reactor.connectTCP('localhost', 6600, factory) reactor.run() 07070100000027000041ED0000000000000000000000025FE435FD00000000000000000000000000000000000000000000001600000000python-mpd2-3.0.1/mpd07070100000028000081A40000000000000000000000015FE435FD0000054C000000000000000000000000000000000000002200000000python-mpd2-3.0.1/mpd/__init__.py# python-mpd2: Python MPD client library # # Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net> # Copyright (C) 2012 J. Thalheim <jthalheim@gmail.com> # Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at> # # python-mpd2 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 3 of the License, or # (at your option) any later version. # # python-mpd2 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 python-mpd2. If not, see <http://www.gnu.org/licenses/>. from mpd.base import CommandError from mpd.base import CommandListError from mpd.base import ConnectionError from mpd.base import IteratingError from mpd.base import MPDClient from mpd.base import MPDError from mpd.base import PendingCommandError from mpd.base import ProtocolError from mpd.base import VERSION try: from mpd.twisted import MPDProtocol except ImportError: class MPDProtocol: def __init__(): raise "No twisted module found" 07070100000029000081A40000000000000000000000015FE435FD00004D21000000000000000000000000000000000000002100000000python-mpd2-3.0.1/mpd/asyncio.py"""Asynchronous access to MPD using the asyncio methods of Python 3. Interaction happens over the mpd.asyncio.MPDClient class, whose connect and command methods are coroutines. Some commands (eg. listall) additionally support the asynchronous iteration (aiter, `async for`) interface; using it allows the library user to obtain items of result as soon as they arrive. The .idle() method works differently here: It is an asynchronous iterator that produces a list of changed subsystems whenever a new one is available. The MPDClient object automatically switches in and out of idle mode depending on which subsystems there is currently interest in. Command lists are currently not supported. This module requires Python 3.5.2 or later to run. """ import asyncio from functools import partial from mpd.base import HELLO_PREFIX, ERROR_PREFIX, SUCCESS from mpd.base import MPDClientBase from mpd.base import MPDClient as SyncMPDClient from mpd.base import ProtocolError, ConnectionError, CommandError from mpd.base import mpd_command_provider class BaseCommandResult(asyncio.Future): """A future that carries its command/args/callback with it for the convenience of passing it around to the command queue.""" def __init__(self, command, args, callback): super().__init__() self._command = command self._args = args self._callback = callback async def _feed_from(self, mpdclient): while True: line = await mpdclient._read_line() self._feed_line(line) if line is None: return class CommandResult(BaseCommandResult): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__spooled_lines = [] def _feed_line(self, line): # FIXME just inline? """Put the given line into the callback machinery, and set the result on a None line.""" if line is None: self.set_result(self._callback(self.__spooled_lines)) else: self.__spooled_lines.append(line) def _feed_error(self, error): if not self.done(): self.set_exception(error) else: # These do occur (especially during the test suite run) when a # disconnect was already initialized, but the run task being # cancelled has not ever yielded at all and thus still needs to run # through to its first await point (which is then in a situation # where properties it'd like to access are already cleaned up, # resulting in an AttributeError) # # Rather than quenching them here, they are made visible (so that # other kinds of double errors raise visibly, even though none are # known right now); instead, the run loop yields initially with a # sleep(0) that ensures it can be cancelled properly at any time. raise error class BinaryCommandResult(asyncio.Future): # Unlike the regular commands that defer to any callback that may be # defined for them, this uses the predefined _read_binary mechanism of the # mpdclient async def _feed_from(self, mpdclient): self.set_result(await mpdclient._read_binary()) _feed_error = CommandResult._feed_error class CommandResultIterable(BaseCommandResult): """Variant of CommandResult where the underlying callback is an asynchronous` generator, and can thus interpret lines as they come along. The result can be used with the aiter interface (`async for`). If it is still used as a future instead, it eventually results in a list. Commands used with this CommandResult must use their passed lines not like an iterable (as in the synchronous implementation), but as a asyncio.Queue. Furthermore, they must check whether the queue elements are exceptions, and raise them. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__spooled_lines = asyncio.Queue() def _feed_line(self, line): self.__spooled_lines.put_nowait(line) _feed_error = _feed_line def __await__(self): asyncio.Task(self.__feed_future()) return super().__await__() __iter__ = __await__ # for 'yield from' style invocation async def __feed_future(self): result = [] try: async for r in self: result.append(r) except Exception as e: self.set_exception(e) else: self.set_result(result) def __aiter__(self): if self.done(): raise RuntimeError("Command result is already being consumed") return self._callback(self.__spooled_lines).__aiter__() @mpd_command_provider class MPDClient(MPDClientBase): __run_task = None # doubles as indicator for being connected #: Indicator of whether there is a pending idle command that was not terminated yet. # When in doubt; this is True, thus erring at the side of caution (because # a "noidle" being sent while racing against an incmoing idle notification # does no harm) __in_idle = False #: Seconds after a command's completion to send idle. Setting this too high # causes "blind spots" in the client's view of the server, setting it too # low sends needless idle/noidle after commands in quick succession. IMMEDIATE_COMMAND_TIMEOUT = 0.1 #: FIFO list of processors that may consume the read stream one after the # other # # As we don't have any other form of backpressure in the sending side # (which is not expected to be limited), its limit of COMMAND_QUEUE_LENGTH # serves as a limit against commands queuing up indefinitely. (It's not # *directly* throttling output, but as the convention is to put the # processor on the queue and then send the command, and commands are of # limited size, this is practically creating backpressure.) __command_queue = None #: Construction size of __command_queue. The default limit is high enough # that a client can easily send off all existing commands simultaneously # without needlessly blocking the TCP flow, but small enough that # freespinning tasks create warnings. COMMAND_QUEUE_LENGTH = 128 async def connect(self, host, port=6600, loop=None): if "/" in host: r, w = await asyncio.open_unix_connection(host, loop=loop) else: r, w = await asyncio.open_connection(host, port, loop=loop) self.__rfile, self.__wfile = r, w self.__command_queue = asyncio.Queue(maxsize=self.COMMAND_QUEUE_LENGTH) self.__idle_consumers = [] #: list of (subsystem-list, callbacks) tuples try: helloline = await asyncio.wait_for(self.__readline(), timeout=5) except asyncio.TimeoutError: self.disconnect() raise ConnectionError("No response from server while reading MPD hello") # FIXME should be reusable w/o reaching in SyncMPDClient._hello(self, helloline) self.__run_task = asyncio.Task(self.__run()) def disconnect(self): if ( self.__run_task is not None ): # is None eg. when connection fails in .connect() self.__run_task.cancel() if self.__wfile is not None: self.__wfile.close() self.__rfile = self.__wfile = None self.__run_task = None self.__command_queue = None self.__idle_consumers = None def _get_idle_interests(self): """Accumulate a set of interests from the current __idle_consumers. Returns the union of their subscribed subjects, [] if at least one of them is the empty catch-all set, or None if there are no interests at all.""" if not self.__idle_consumers: return None if any(len(s) == 0 for (s, c) in self.__idle_consumers): return [] return set.union(*(set(s) for (s, c) in self.__idle_consumers)) def _end_idle(self): """If the main task is currently idling, make it leave idle and process the next command (if one is present) or just restart idle""" if self.__in_idle: self.__write("noidle\n") self.__in_idle = False async def __run(self): # See CommandResult._feed_error documentation await asyncio.sleep(0) try: while True: try: result = await asyncio.wait_for( self.__command_queue.get(), timeout=self.IMMEDIATE_COMMAND_TIMEOUT, ) except asyncio.TimeoutError: # The cancellation of the __command_queue.get() that happens # in this case is intended, and is just what asyncio.Queue # suggests for "get with timeout". subsystems = self._get_idle_interests() if subsystems is None: # The presumably most quiet subsystem -- in this case, # idle is only used to keep the connection alive. subsystems = ["database"] # Careful: There can't be any await points between the # except and here, or the sequence between the idle and the # command processor might be wrong. result = CommandResult("idle", subsystems, lambda result: self.__distribute_idle_result(self._parse_list(result))) self.__in_idle = True self._write_command(result._command, result._args) try: await result._feed_from(self) except CommandError as e: result._feed_error(e) # This kind of error we can tolerate without breaking up # the connection; any other would fly out, be reported # through the result and terminate the connection except Exception as e: # Prevent the destruction of the pending task in the shutdown # function -- it's just shutting down by itself. self.__run_task = None self.disconnect() if result is not None: result._feed_error(e) return else: raise # Typically this is a bug in mpd.asyncio. def __distribute_idle_result(self, result): # An exception flying out of here probably means a connection # interruption during idle. This will just show like any other # unhandled task exception and that's probably the best we can do. idle_changes = list(result) for subsystems, callback in self.__idle_consumers: if not subsystems or any(s in subsystems for s in idle_changes): callback(idle_changes) # helper methods async def __readline(self): """Wrapper around .__rfile.readline that handles encoding""" data = await self.__rfile.readline() try: return data.decode("utf8") except UnicodeDecodeError: self.disconnect() raise ProtocolError("Invalid UTF8 received") async def _read_chunk(self, length): try: return await self.__rfile.readexactly(length) except asyncio.IncompleteReadError: raise ConnectionError("Connection lost while reading binary") def __write(self, text): """Wrapper around .__wfile.write that handles encoding.""" self.__wfile.write(text.encode("utf8")) # copied and subtly modifiedstuff from base # This is just a wrapper for the below. def _write_line(self, text): self.__write(text + "\n") # FIXME This code should be shareable. _write_command = SyncMPDClient._write_command async def _read_line(self): line = await self.__readline() if not line.endswith("\n"): raise ConnectionError("Connection lost while reading line") line = line.rstrip("\n") if line.startswith(ERROR_PREFIX): error = line[len(ERROR_PREFIX) :].strip() raise CommandError(error) if line == SUCCESS: return None return line async def _parse_objects_direct(self, lines, delimiters=[], lookup_delimiter=False): obj = {} while True: line = await lines.get() if isinstance(line, BaseException): raise line if line is None: break key, value = self._parse_pair(line, separator=": ") key = key.lower() if lookup_delimiter and not delimiters: delimiters = [key] if obj: if key in delimiters: yield obj obj = {} elif key in obj: if not isinstance(obj[key], list): obj[key] = [obj[key], value] else: obj[key].append(value) continue obj[key] = value if obj: yield obj async def _execute_binary(self, command, args): # Fun fact: By fetching data in lockstep, this is a bit less efficient # than it could be (which would be "after having received the first # chunk, guess that the other chunks are of equal size and request at # several multiples concurrently, ensuring the TCP connection can stay # full), but at the other hand it leaves the command queue empty so # that more time critical commands can be executed right away data = None args = list(args) assert len(args) == 1 args.append(0) final_metadata = None while True: partial_result = BinaryCommandResult() await self.__command_queue.put(partial_result) self._end_idle() self._write_command(command, args) metadata = await partial_result chunk = metadata.pop('binary', None) if final_metadata is None: data = chunk final_metadata = metadata if not data: break try: size = int(final_metadata['size']) except KeyError: size = len(chunk) except ValueError: raise CommandError("Size data unsuitable for binary transfer") else: if metadata != final_metadata: raise CommandError("Metadata of binary data changed during transfer") if chunk is None: raise CommandError("Binary field vanished changed during transfer") data += chunk args[-1] = len(data) if len(data) > size: raise CommandListError("Binary data announced size exceeded") elif len(data) == size: break if data is not None: final_metadata['binary'] = data final_metadata.pop('size', None) return final_metadata # omits _read_chunk checking because the async version already # raises; otherwise it's just awaits sprinkled in async def _read_binary(self): obj = {} while True: line = await self._read_line() if line is None: break key, value = self._parse_pair(line, ": ") if key == "binary": chunk_size = int(value) value = await self._read_chunk(chunk_size) if await self.__rfile.readexactly(1) != b"\n": # newline after binary content self.disconnect() raise ConnectionError("Connection lost while reading line") obj[key] = value return obj # command provider interface @classmethod def add_command(cls, name, callback): if callback.mpd_commands_binary: async def f(self, *args): result = await self._execute_binary(name, args) # With binary, the callback is applied to the final result # rather than to the iterator over the lines (cf. # MPDClient._execute_binary) return callback(self, result) else: command_class = ( CommandResultIterable if callback.mpd_commands_direct else CommandResult ) if hasattr(cls, name): # Idle and noidle are explicitly implemented, skipping them. return def f(self, *args): result = command_class(name, args, partial(callback, self)) if self.__run_task is None: raise ConnectionError("Can not send command to disconnected client") try: self.__command_queue.put_nowait(result) except asyncio.QueueFull as e: e.args = ("Command queue overflowing; this indicates the" " application sending commands in an uncontrolled" " fashion without awaiting them, and typically" " indicates a memory leak.",) # While we *could* indicate to the queued result that it has # yet to send its request, that'd practically create a queue of # awaited items in the user application that's growing # unlimitedly, eliminating any chance of timely responses. # Furthermore, the author sees no practical use case that's not # violating MPD's guidance of "Do not manage a client-side copy # of MPD's database". If a use case *does* come up, any change # would need to maintain the property of providing backpressure # information. That would require an API change. raise self._end_idle() # Careful: There can't be any await points between the queue # appending and the write try: self._write_command(result._command, result._args) except BaseException as e: self.disconnect() result.set_exception(e) return result escaped_name = name.replace(" ", "_") f.__name__ = escaped_name setattr(cls, escaped_name, f) # commands that just work differently async def idle(self, subsystems=()): if self.__idle_consumers is None: raise ConnectionError("Can not start idle on a disconnected client") interests_before = self._get_idle_interests() changes = asyncio.Queue() try: entry = (subsystems, changes.put_nowait) self.__idle_consumers.append(entry) if self._get_idle_interests != interests_before: # Technically this does not enter idle *immediately* but rather # only after any commands after IMMEDIATE_COMMAND_TIMEOUT; # practically that should be a good thing. self._end_idle() while True: yield await changes.get() finally: if self.__idle_consumers is not None: self.__idle_consumers.remove(entry) def noidle(self): raise AttributeError("noidle is not supported / required in mpd.asyncio") 0707010000002A000081A40000000000000000000000015FE435FD000066E7000000000000000000000000000000000000001E00000000python-mpd2-3.0.1/mpd/base.py# python-mpd2: Python MPD client library # # Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net> # Copyright (C) 2012 J. Thalheim <jthalheim@gmail.com> # Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at> # # python-mpd2 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 3 of the License, or # (at your option) any later version. # # python-mpd2 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 python-mpd2. If not, see <http://www.gnu.org/licenses/>. import logging import socket import sys import warnings VERSION = (3, 0, 1) HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" def escape(text): return text.replace("\\", "\\\\").replace('"', '\\"') try: from logging import NullHandler except ImportError: # NullHandler was introduced in python2.7 class NullHandler(logging.Handler): def emit(self, record): pass logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) class MPDError(Exception): pass class ConnectionError(MPDError): pass class ProtocolError(MPDError): pass class CommandError(MPDError): pass class CommandListError(MPDError): pass class PendingCommandError(MPDError): pass class IteratingError(MPDError): pass class mpd_commands(object): """Decorator for registering MPD commands with it's corresponding result callback. """ def __init__(self, *commands, **kwargs): self.commands = commands self.is_direct = kwargs.pop("is_direct", False) self.is_binary = kwargs.pop("is_binary", False) if kwargs: raise AttributeError( "mpd_commands() got unexpected keyword" " arguments %s" % ",".join(kwargs) ) def __call__(self, ob): ob.mpd_commands = self.commands ob.mpd_commands_direct = self.is_direct ob.mpd_commands_binary = self.is_binary return ob def mpd_command_provider(cls): """Decorator hooking up registered MPD commands to concrete client implementation. A class using this decorator must inherit from ``MPDClientBase`` and implement it's ``add_command`` function. """ def collect(cls, callbacks=dict()): """Collect MPD command callbacks from given class. Searches class __dict__ on given class and all it's bases for functions which have been decorated with @mpd_commands and returns a dict containing callback name as keys and (callback, callback implementing class) tuples as values. """ for name, ob in cls.__dict__.items(): if hasattr(ob, "mpd_commands") and name not in callbacks: callbacks[name] = (ob, cls) for base in cls.__bases__: callbacks = collect(base, callbacks) return callbacks for name, value in collect(cls).items(): callback, from_ = value for command in callback.mpd_commands: cls.add_command(command, callback) return cls class Noop(object): """An instance of this class represents a MPD command callback which does nothing. """ mpd_commands = None class MPDClientBase(object): """Abstract MPD client. This class defines a general client contract, provides MPD protocol parsers and defines all available MPD commands and it's corresponding result parsing callbacks. There might be the need of overriding some callbacks on subclasses. """ def __init__(self, use_unicode=None): self.iterate = False if use_unicode is not None: warnings.warn( "use_unicode parameter to ``MPDClientBase`` constructor is " "deprecated", DeprecationWarning, stacklevel=2, ) self._reset() @property def use_unicode(self): warnings.warn( "``use_unicode`` is deprecated: python-mpd 2.x always uses " "Unicode", DeprecationWarning, stacklevel=2, ) return True @classmethod def add_command(cls, name, callback): raise NotImplementedError( "Abstract ``MPDClientBase`` does not implement ``add_command``" ) def noidle(self): raise NotImplementedError( "Abstract ``MPDClientBase`` does not implement ``noidle``" ) def command_list_ok_begin(self): raise NotImplementedError( "Abstract ``MPDClientBase`` does not implement " "``command_list_ok_begin``" ) def command_list_end(self): raise NotImplementedError( "Abstract ``MPDClientBase`` does not implement " "``command_list_end``" ) def _reset(self): self.mpd_version = None self._command_list = None def _parse_pair(self, line, separator): if line is None: return pair = line.split(separator, 1) if len(pair) < 2: raise ProtocolError("Could not parse pair: '{}'".format(line)) return pair def _parse_pairs(self, lines, separator=": "): for line in lines: yield self._parse_pair(line, separator) def _parse_objects(self, lines, delimiters=[], lookup_delimiter=False): obj = {} for key, value in self._parse_pairs(lines): key = key.lower() if lookup_delimiter and not delimiters: delimiters = [key] if obj: if key in delimiters: yield obj obj = {} elif key in obj: if not isinstance(obj[key], list): obj[key] = [obj[key], value] else: obj[key].append(value) continue obj[key] = value if obj: yield obj # Use this instead of _parse_objects whenever the result is returned # immediately in a command implementation _parse_objects_direct = _parse_objects def _parse_raw_stickers(self, lines): for key, sticker in self._parse_pairs(lines): value = sticker.split("=", 1) if len(value) < 2: raise ProtocolError("Could not parse sticker: {}".format(repr(sticker))) yield tuple(value) NOOP = mpd_commands("close", "kill")(Noop()) @mpd_commands("plchangesposid", is_direct=True) def _parse_changes(self, lines): return self._parse_objects_direct(lines, ["cpos"]) @mpd_commands("listall", "listallinfo", "listfiles", "lsinfo", is_direct=True) def _parse_database(self, lines): return self._parse_objects_direct(lines, ["file", "directory", "playlist"]) @mpd_commands("idle") def _parse_idle(self, lines): return self._parse_list(lines) @mpd_commands("addid", "config", "replay_gain_status", "rescan", "update") def _parse_item(self, lines): pairs = list(self._parse_pairs(lines)) if len(pairs) != 1: return return pairs[0][1] @mpd_commands( "channels", "commands", "listplaylist", "notcommands", "tagtypes", "urlhandlers" ) def _parse_list(self, lines): seen = None for key, value in self._parse_pairs(lines): if key != seen: if seen is not None: raise ProtocolError("Expected key '{}', got '{}'".format(seen, key)) seen = key yield value @mpd_commands("list", is_direct=True) def _parse_list_groups(self, lines): return self._parse_objects_direct(lines, lookup_delimiter=True) @mpd_commands("readmessages", is_direct=True) def _parse_messages(self, lines): return self._parse_objects_direct(lines, ["channel"]) @mpd_commands("listmounts", is_direct=True) def _parse_mounts(self, lines): return self._parse_objects_direct(lines, ["mount"]) @mpd_commands("listneighbors", is_direct=True) def _parse_neighbors(self, lines): return self._parse_objects_direct(lines, ["neighbor"]) @mpd_commands("listpartitions", is_direct=True) def _parse_partitions(self, lines): return self._parse_objects_direct(lines, ["partition"]) @mpd_commands( "add", "addtagid", "clear", "clearerror", "cleartagid", "consume", "crossfade", "delete", "deleteid", "delpartition", "disableoutput", "enableoutput", "findadd", "load", "mixrampdb", "mixrampdelay", "mount", "move", "moveid", "moveoutput", "newpartition", "next", "outputvolume", "partition", "password", "pause", "ping", "play", "playid", "playlistadd", "playlistclear", "playlistdelete", "playlistmove", "previous", "prio", "prioid", "random", "rangeid", "rename", "repeat", "replay_gain_mode", "rm", "save", "searchadd", "searchaddpl", "seek", "seekcur", "seekid", "sendmessage", "setvol", "shuffle", "single", "sticker delete", "sticker set", "stop", "subscribe", "swap", "swapid", "toggleoutput", "umount", "unsubscribe", "volume", ) def _parse_nothing(self, lines): for line in lines: raise ProtocolError( "Got unexpected return value: '{}'".format(", ".join(lines)) ) @mpd_commands("count", "currentsong", "readcomments", "stats", "status") def _parse_object(self, lines): objs = list(self._parse_objects(lines)) if not objs: return {} return objs[0] @mpd_commands("outputs", is_direct=True) def _parse_outputs(self, lines): return self._parse_objects_direct(lines, ["outputid"]) @mpd_commands("playlist") def _parse_playlist(self, lines): for key, value in self._parse_pairs(lines, ":"): yield value @mpd_commands("listplaylists", is_direct=True) def _parse_playlists(self, lines): return self._parse_objects_direct(lines, ["playlist"]) @mpd_commands("decoders", is_direct=True) def _parse_plugins(self, lines): return self._parse_objects_direct(lines, ["plugin"]) @mpd_commands( "find", "listplaylistinfo", "playlistfind", "playlistid", "playlistinfo", "playlistsearch", "plchanges", "search", "sticker find", is_direct=True, ) def _parse_songs(self, lines): return self._parse_objects_direct(lines, ["file"]) @mpd_commands("sticker get") def _parse_sticker(self, lines): key, value = list(self._parse_raw_stickers(lines))[0] return value @mpd_commands("sticker list") def _parse_stickers(self, lines): return dict(self._parse_raw_stickers(lines)) @mpd_commands("albumart", "readpicture", is_binary=True) def _parse_plain_binary(self, structure): return structure def _create_callback(self, function, wrap_result): """Create MPD command related response callback. """ if not callable(function): return None def command_callback(): # command result callback expects response from MPD as iterable lines, # thus read available lines from socket res = function(self, self._read_lines()) # wrap result in iterator helper if desired if wrap_result: res = self._wrap_iterator(res) return res return command_callback def _create_command(wrapper, name, return_value, wrap_result): """Create MPD command related function. """ def mpd_command(self, *args): callback = _create_callback(self, return_value, wrap_result) return wrapper(self, name, args, callback) return mpd_command class _NotConnected(object): def __getattr__(self, attr): return self._dummy def _dummy(*args): raise ConnectionError("Not connected") @mpd_command_provider class MPDClient(MPDClientBase): idletimeout = None _timeout = None _wrap_iterator_parsers = [ MPDClientBase._parse_list, MPDClientBase._parse_list_groups, MPDClientBase._parse_playlist, MPDClientBase._parse_changes, MPDClientBase._parse_songs, MPDClientBase._parse_mounts, MPDClientBase._parse_neighbors, MPDClientBase._parse_partitions, MPDClientBase._parse_playlists, MPDClientBase._parse_database, MPDClientBase._parse_messages, MPDClientBase._parse_outputs, MPDClientBase._parse_plugins, ] def __init__(self, use_unicode=None): if use_unicode is not None: warnings.warn( "use_unicode parameter to ``MPDClient`` constructor is " "deprecated", DeprecationWarning, stacklevel=2, ) super(MPDClient, self).__init__() def _reset(self): super(MPDClient, self)._reset() self._iterating = False self._sock = None self._rbfile = _NotConnected() self._wfile = _NotConnected() def _execute(self, command, args, retval): if self._iterating: raise IteratingError("Cannot execute '{}' while iterating".format(command)) if self._command_list is not None: if not callable(retval): raise CommandListError( "'{}' not allowed in command list".format(command) ) self._write_command(command, args) self._command_list.append(retval) else: self._write_command(command, args) if callable(retval): return retval() return retval def _write_line(self, line): try: self._wfile.write("{}\n".format(line)) self._wfile.flush() except socket.error as e: error_message = "Connection to server was reset" logger.info(error_message) self._reset() e = ConnectionError(error_message) raise e.with_traceback(sys.exc_info()[2]) def _write_command(self, command, args=[]): parts = [command] for arg in args: if type(arg) is tuple: if len(arg) == 0: parts.append('":"') elif len(arg) == 1: parts.append('"{}:"'.format(int(arg[0]))) else: parts.append('"{}:{}"'.format(int(arg[0]), int(arg[1]))) else: parts.append('"{}"'.format(escape(str(arg)))) # Minimize logging cost if the logging is not activated. if logger.isEnabledFor(logging.DEBUG): if command == "password": logger.debug("Calling MPD password(******)") else: logger.debug("Calling MPD %s%r", command, args) cmd = " ".join(parts) self._write_line(cmd) def _read_line(self): line = self._rbfile.readline().decode("utf-8") if not line.endswith("\n"): self.disconnect() raise ConnectionError("Connection lost while reading line") line = line.rstrip("\n") if line.startswith(ERROR_PREFIX): error = line[len(ERROR_PREFIX) :].strip() raise CommandError(error) if self._command_list is not None: if line == NEXT: return if line == SUCCESS: raise ProtocolError("Got unexpected '{}'".format(SUCCESS)) elif line == SUCCESS: return return line def _read_lines(self): line = self._read_line() while line is not None: yield line line = self._read_line() def _read_chunk(self, amount): chunk = bytearray() while amount > 0: result = self._rbfile.read(amount) if len(result) == 0: break chunk.extend(result) amount -= len(result) return bytes(chunk) def _read_binary(self): """From the data stream, read Unicode lines until one says "binary: <number>\\n"; at that point, read binary data of the given length. This behaves like _parse_objects (with empty set of delimiters; even returning only a single result), but rather than feeding from a lines iterable (which would be preprocessed too far), it reads directly off the stream.""" obj = {} while True: line = self._read_line() if line is None: break key, value = self._parse_pair(line, ": ") if key == "binary": chunk_size = int(value) value = self._read_chunk(chunk_size) if len(value) != chunk_size: self.disconnect() raise ConnectionError( "Connection lost while reading binary data: " "expected %d bytes, got %d" % (chunk_size, len(data)) ) if self._rbfile.read(1) != b"\n": # newline after binary content self.disconnect() raise ConnectionError("Connection lost while reading line") obj[key] = value return obj def _execute_binary(self, command, args): """Execute a command repeatedly with an additional offset argument, keeping all the identical returned dictionary items and concatenating the binary chunks following the binary item into one of exactly size. This differs from _execute in that rather than passing the lines to the callback which'd then call on something like _parse_objects, it builds a parsed object on its own (as a prerequisite to the chunk driving process) and then joins together the chunks into a single big response.""" if self._iterating or self._command_list is not None: raise IteratingError("Cannot execute '{}' with command lists".format(command)) data = None args = list(args) assert len(args) == 1 args.append(0) final_metadata = None while True: self._write_command(command, args) metadata = self._read_binary() chunk = metadata.pop('binary', None) if final_metadata is None: data = chunk final_metadata = metadata if not data: break try: size = int(final_metadata['size']) except KeyError: size = len(chunk) except ValueError: raise CommandError("Size data unsuitable for binary transfer") else: if metadata != final_metadata: raise CommandError("Metadata of binary data changed during transfer") if chunk is None: raise CommandError("Binary field vanished changed during transfer") data += chunk args[-1] = len(data) if len(data) > size: raise CommandListError("Binary data announced size exceeded") elif len(data) == size: break if data is not None: final_metadata['binary'] = data final_metadata.pop('size', None) return final_metadata def _read_command_list(self): try: for retval in self._command_list: yield retval() finally: self._command_list = None self._parse_nothing(self._read_lines()) def _iterator_wrapper(self, iterator): try: for item in iterator: yield item finally: self._iterating = False def _wrap_iterator(self, iterator): if not self.iterate: return list(iterator) self._iterating = True return self._iterator_wrapper(iterator) def _hello(self, line): if not line.endswith("\n"): self.disconnect() raise ConnectionError("Connection lost while reading MPD hello") line = line.rstrip("\n") if not line.startswith(HELLO_PREFIX): raise ProtocolError("Got invalid MPD hello: '{}'".format(line)) self.mpd_version = line[len(HELLO_PREFIX) :].strip() def _connect_unix(self, path): if not hasattr(socket, "AF_UNIX"): raise ConnectionError("Unix domain sockets not supported on this platform") sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(self.timeout) sock.connect(path) return sock def _connect_tcp(self, host, port): try: flags = socket.AI_ADDRCONFIG except AttributeError: flags = 0 err = None for res in socket.getaddrinfo( host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags ): af, socktype, proto, canonname, sa = res sock = None try: sock = socket.socket(af, socktype, proto) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) sock.settimeout(self.timeout) sock.connect(sa) return sock except socket.error as e: err = e if sock is not None: sock.close() if err is not None: raise err else: raise ConnectionError("getaddrinfo returns an empty list") @mpd_commands("idle") def _parse_idle(self, lines): self._sock.settimeout(self.idletimeout) ret = self._wrap_iterator(self._parse_list(lines)) self._sock.settimeout(self._timeout) return ret @property def timeout(self): return self._timeout @timeout.setter def timeout(self, timeout): self._timeout = timeout if self._sock is not None: self._sock.settimeout(timeout) def connect(self, host, port=None, timeout=None): logger.info("Calling MPD connect(%r, %r, timeout=%r)", host, port, timeout) if self._sock is not None: raise ConnectionError("Already connected") if timeout is not None: warnings.warn( "The timeout parameter in connect() is deprecated! " "Use MPDClient.timeout = yourtimeout instead.", DeprecationWarning, ) self.timeout = timeout if host.startswith("/"): self._sock = self._connect_unix(host) else: if port is None: raise ValueError( "port argument must be specified when connecting via tcp" ) self._sock = self._connect_tcp(host, port) # - Force UTF-8 encoding, since this is dependant from the LC_CTYPE # locale. # - by setting newline explicit, we force to send '\n' also on # windows self._rbfile = self._sock.makefile("rb", newline="\n") self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n") try: helloline = self._rbfile.readline().decode("utf-8") self._hello(helloline) except Exception: self.disconnect() raise def disconnect(self): logger.info("Calling MPD disconnect()") if self._rbfile is not None and not isinstance(self._rbfile, _NotConnected): self._rbfile.close() if self._wfile is not None and not isinstance(self._wfile, _NotConnected): self._wfile.close() if self._sock is not None: self._sock.close() self._reset() def fileno(self): if self._sock is None: raise ConnectionError("Not connected") return self._sock.fileno() def command_list_ok_begin(self): if self._command_list is not None: raise CommandListError("Already in command list") if self._iterating: raise IteratingError("Cannot begin command list while iterating") self._write_command("command_list_ok_begin") self._command_list = [] def command_list_end(self): if self._command_list is None: raise CommandListError("Not in command list") if self._iterating: raise IteratingError("Already iterating over a command list") self._write_command("command_list_end") return self._wrap_iterator(self._read_command_list()) @classmethod def add_command(cls, name, callback): wrap_result = callback in cls._wrap_iterator_parsers if callback.mpd_commands_binary: method = lambda self, *args: callback(self, cls._execute_binary(self, name, args)) else: method = _create_command(cls._execute, name, callback, wrap_result) # create new mpd commands as function: escaped_name = name.replace(" ", "_") setattr(cls, escaped_name, method) @classmethod def remove_command(cls, name): if not hasattr(cls, name): raise ValueError("Can't remove not existent '{}' command".format(name)) name = name.replace(" ", "_") delattr(cls, str(name)) # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 0707010000002B000081ED0000000000000000000000015FE435FD0000C7C9000000000000000000000000000000000000001F00000000python-mpd2-3.0.1/mpd/tests.py#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import import itertools import mpd import mpd.asyncio import os import socket import sys import types import warnings import unittest try: from twisted.python.failure import Failure TWISTED_MISSING = False except ImportError: warnings.warn( "No twisted installed: skip twisted related tests! " + "(twisted is not available for python >= 3.0 && python < 3.3)" ) TWISTED_MISSING = True import asyncio try: import mock except ImportError: print("Please install mock from PyPI to run tests!") sys.exit(1) # show deprecation warnings warnings.simplefilter("default") TEST_MPD_HOST, TEST_MPD_PORT = ("example.com", 10000) TEST_MPD_UNIXHOST = "/example/test/host" TEST_MPD_UNIXTIMEOUT = 0.5 class TestMPDClient(unittest.TestCase): longMessage = True def setUp(self): self.socket_patch = mock.patch("mpd.base.socket") self.socket_mock = self.socket_patch.start() self.socket_mock.getaddrinfo.return_value = [range(5)] self.socket_mock.socket.side_effect = ( lambda *a, **kw: # Create a new socket.socket() mock with default attributes, # each time we are calling it back (otherwise, it keeps set # attributes across calls). # That's probablyy what we want, since reconnecting is like # reinitializing the entire connection, and so, the mock. mock.MagicMock(name="socket.socket") ) self.client = mpd.MPDClient() self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) self.client._sock.reset_mock() self.MPDWillReturn("ACK don't forget to setup your mock\n") def tearDown(self): self.socket_patch.stop() def MPDWillReturn(self, *lines): # Return what the caller wants first, then do as if the socket was # disconnected. innerIter = itertools.chain(lines, itertools.repeat("")) if sys.version_info >= (3, 0): self.client._rbfile.readline.side_effect = ( x.encode("utf-8") for x in innerIter ) else: self.client._rbfile.readline.side_effect = innerIter def assertMPDReceived(self, *lines): self.client._wfile.write.assert_called_with(*lines) def test_abstract_functions(self): MPDClientBase = mpd.base.MPDClientBase self.assertRaises( NotImplementedError, lambda: MPDClientBase.add_command("command_name", lambda x: x), ) client = MPDClientBase() self.assertRaises(NotImplementedError, lambda: client.noidle()) self.assertRaises(NotImplementedError, lambda: client.command_list_ok_begin()) self.assertRaises(NotImplementedError, lambda: client.command_list_end()) def test_metaclass_commands(self): # just some random functions self.assertTrue(hasattr(self.client, "commands")) self.assertTrue(hasattr(self.client, "save")) self.assertTrue(hasattr(self.client, "random")) # space should be replaced self.assertFalse(hasattr(self.client, "sticker get")) self.assertTrue(hasattr(self.client, "sticker_get")) def test_duplicate_tags(self): self.MPDWillReturn("Track: file1\n", "Track: file2\n", "OK\n") song = self.client.currentsong() self.assertIsInstance(song, dict) self.assertIsInstance(song["track"], list) self.assertMPDReceived("currentsong\n") def test_parse_nothing(self): self.MPDWillReturn("OK\n", "OK\n") self.assertIsNone(self.client.ping()) self.assertMPDReceived("ping\n") self.assertIsNone(self.client.clearerror()) self.assertMPDReceived("clearerror\n") def test_parse_list(self): self.MPDWillReturn( "tagtype: Artist\n", "tagtype: ArtistSort\n", "tagtype: Album\n", "OK\n" ) result = self.client.tagtypes() self.assertMPDReceived("tagtypes\n") self.assertIsInstance(result, list) self.assertEqual(result, ["Artist", "ArtistSort", "Album",]) def test_parse_list_groups(self): self.MPDWillReturn( "Album: \n", "Album: 20th_Century_Masters_The_Millenium_Collection\n", "Album: Aerosmith's Greatest Hits\n", "OK\n", ) result = self.client.list("album") self.assertMPDReceived('list "album"\n') self.assertIsInstance(result, list) self.assertEqual( result, [ {"album": ""}, {"album": "20th_Century_Masters_The_Millenium_Collection"}, {"album": "Aerosmith's Greatest Hits"}, ], ) self.MPDWillReturn( "Album: \n", "Album: 20th_Century_Masters_The_Millenium_Collection\n", "Artist: Eric Clapton\n", "Album: Aerosmith's Greatest Hits\n", "Artist: Aerosmith\n", "OK\n", ) result = self.client.list("album", "group", "artist") self.assertMPDReceived('list "album" "group" "artist"\n') self.assertIsInstance(result, list) self.assertEqual( result, [ {"album": ""}, { "album": "20th_Century_Masters_The_Millenium_Collection", "artist": "Eric Clapton", }, {"album": "Aerosmith's Greatest Hits", "artist": "Aerosmith"}, ], ) def test_parse_item(self): self.MPDWillReturn("updating_db: 42\n", "OK\n") self.assertIsNotNone(self.client.update()) def test_parse_object(self): # XXX: _read_objects() doesn't wait for the final OK self.MPDWillReturn("volume: 63\n", "OK\n") status = self.client.status() self.assertMPDReceived("status\n") self.assertIsInstance(status, dict) # XXX: _read_objects() doesn't wait for the final OK self.MPDWillReturn("OK\n") stats = self.client.stats() self.assertMPDReceived("stats\n") self.assertIsInstance(stats, dict) def test_parse_songs(self): self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") playlist = self.client.playlistinfo() self.assertMPDReceived("playlistinfo\n") self.assertIsInstance(playlist, list) self.assertEqual(1, len(playlist)) e = playlist[0] self.assertIsInstance(e, dict) self.assertEqual("my-song.ogg", e["file"]) self.assertEqual("0", e["pos"]) self.assertEqual("66", e["id"]) def test_readcomments(self): self.MPDWillReturn( "major_brand: M4V\n", "minor_version: 1\n", "lyrics: Lalala\n", "OK\n" ) comments = self.client.readcomments() self.assertMPDReceived("readcomments\n") self.assertEqual(comments["major_brand"], "M4V") self.assertEqual(comments["minor_version"], "1") self.assertEqual(comments["lyrics"], "Lalala") def test_iterating(self): self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") self.client.iterate = True playlist = self.client.playlistinfo() self.assertMPDReceived("playlistinfo\n") self.assertIsInstance(playlist, types.GeneratorType) for song in playlist: self.assertIsInstance(song, dict) self.assertEqual("my-song.ogg", song["file"]) self.assertEqual("0", song["pos"]) self.assertEqual("66", song["id"]) def test_add_and_remove_command(self): self.MPDWillReturn("ACK awesome command\n") self.client.add_command("awesome command", mpd.MPDClient._parse_nothing) self.assertTrue(hasattr(self.client, "awesome_command")) # should be unknown by mpd self.assertRaises(mpd.CommandError, self.client.awesome_command) self.client.remove_command("awesome_command") self.assertFalse(hasattr(self.client, "awesome_command")) # remove non existing command self.assertRaises(ValueError, self.client.remove_command, "awesome_command") def test_partitions(self): self.MPDWillReturn("partition: default\n", "partition: partition2\n", "OK\n") partitions = self.client.listpartitions() self.assertMPDReceived("listpartitions\n") self.assertEqual( [ {"partition": "default"}, {"partition": "partition2"}, ], partitions ) self.MPDWillReturn("OK\n") self.assertIsNone(self.client.newpartition("Another Partition")) self.assertMPDReceived('newpartition "Another Partition"\n') self.MPDWillReturn("OK\n") self.assertIsNone(self.client.partition("Another Partition")) self.assertMPDReceived('partition "Another Partition"\n') self.MPDWillReturn("OK\n") self.assertIsNone(self.client.delpartition("Another Partition")) self.assertMPDReceived('delpartition "Another Partition"\n') self.MPDWillReturn("OK\n") self.assertIsNone(self.client.moveoutput("My ALSA Device")) self.assertMPDReceived('moveoutput "My ALSA Device"\n') def test_client_to_client(self): # client to client is at this time in beta! self.MPDWillReturn("OK\n") self.assertIsNone(self.client.subscribe("monty")) self.assertMPDReceived('subscribe "monty"\n') self.MPDWillReturn("channel: monty\n", "OK\n") channels = self.client.channels() self.assertMPDReceived("channels\n") self.assertEqual(["monty"], channels) self.MPDWillReturn("OK\n") self.assertIsNone(self.client.sendmessage("monty", "SPAM")) self.assertMPDReceived('sendmessage "monty" "SPAM"\n') self.MPDWillReturn("channel: monty\n", "message: SPAM\n", "OK\n") msg = self.client.readmessages() self.assertMPDReceived("readmessages\n") self.assertEqual(msg, [{"channel": "monty", "message": "SPAM"}]) self.MPDWillReturn("OK\n") self.assertIsNone(self.client.unsubscribe("monty")) self.assertMPDReceived('unsubscribe "monty"\n') self.MPDWillReturn("OK\n") channels = self.client.channels() self.assertMPDReceived("channels\n") self.assertEqual([], channels) def test_unicode_as_command_args(self): self.MPDWillReturn("OK\n") res = self.client.find("file", "☯☾☝♖✽") self.assertIsInstance(res, list) self.assertMPDReceived('find "file" "☯☾☝♖✽"\n') def test_numbers_as_command_args(self): self.MPDWillReturn("OK\n") self.client.find("file", 1) self.assertMPDReceived('find "file" "1"\n') def test_commands_without_callbacks(self): self.MPDWillReturn("\n") self.client.close() self.assertMPDReceived("close\n") # XXX: what are we testing here? # looks like reconnection test? self.client._reset() self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) def test_set_timeout_on_client(self): self.client.timeout = 1 self.client._sock.settimeout.assert_called_with(1) self.assertEqual(self.client.timeout, 1) self.client.timeout = None self.client._sock.settimeout.assert_called_with(None) self.assertEqual(self.client.timeout, None) def test_set_timeout_from_connect(self): self.client.disconnect() with warnings.catch_warnings(record=True) as w: self.client.connect("example.com", 10000, timeout=5) self.client._sock.settimeout.assert_called_with(5) self.assertEqual(len(w), 1) self.assertIn("Use MPDClient.timeout", str(w[0].message)) @unittest.skipIf( sys.version_info < (3, 3), "BrokenPipeError was introduced in python 3.3" ) def test_broken_pipe_error(self): self.MPDWillReturn("volume: 63\n", "OK\n") self.client._wfile.write.side_effect = BrokenPipeError self.socket_mock.error = Exception with self.assertRaises(mpd.ConnectionError): self.client.status() def test_connection_lost(self): # Simulate a connection lost: the socket returns empty strings self.MPDWillReturn("") self.socket_mock.error = Exception with self.assertRaises(mpd.ConnectionError): self.client.status() self.socket_mock.unpack.assert_called() # consistent behaviour, solves bug #11 (github) with self.assertRaises(mpd.ConnectionError): self.client.status() self.socket_mock.unpack.assert_called() self.assertIs(self.client._sock, None) @unittest.skipIf( sys.version_info < (3, 0), "Automatic decoding/encoding from the socket is only " "available in Python 3", ) def test_force_socket_encoding_and_nonbuffering(self): # Force the reconnection to refill the mock self.client.disconnect() self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) self.assertEqual( [ mock.call("rb", newline="\n"), mock.call("w", encoding="utf-8", newline="\n"), ], # We are only interested into the 2 first entries, # otherwise we get all the readline() & co... self.client._sock.makefile.call_args_list[0:2], ) def test_ranges_as_argument(self): self.MPDWillReturn("OK\n") self.client.move((1, 2), 2) self.assertMPDReceived('move "1:2" "2"\n') self.MPDWillReturn("OK\n") self.client.move((1,), 2) self.assertMPDReceived('move "1:" "2"\n') # old code still works! self.MPDWillReturn("OK\n") self.client.move("1:2", 2) self.assertMPDReceived('move "1:2" "2"\n') # empty ranges self.MPDWillReturn("OK\n") self.client.rangeid(1, ()) self.assertMPDReceived('rangeid "1" ":"\n') with self.assertRaises(ValueError): self.MPDWillReturn("OK\n") self.client.move((1, "garbage"), 2) self.assertMPDReceived('move "1:" "2"\n') def test_parse_changes(self): self.MPDWillReturn( "cpos: 0\n", "Id: 66\n", "cpos: 1\n", "Id: 67\n", "cpos: 2\n", "Id: 68\n", "cpos: 3\n", "Id: 69\n", "cpos: 4\n", "Id: 70\n", "OK\n", ) res = self.client.plchangesposid(0) self.assertEqual( [ {"cpos": "0", "id": "66"}, {"cpos": "1", "id": "67"}, {"cpos": "2", "id": "68"}, {"cpos": "3", "id": "69"}, {"cpos": "4", "id": "70"}, ], res, ) def test_parse_database(self): self.MPDWillReturn( "directory: foo\n", "Last-Modified: 2014-01-23T16:42:46Z\n", "file: bar.mp3\n", "size: 59618802\n", "Last-Modified: 2014-11-02T19:57:00Z\n", "OK\n", ) self.client.listfiles("/") def test_parse_mounts(self): self.MPDWillReturn( "mount: \n", "storage: /home/foo/music\n", "mount: foo\n", "storage: nfs://192.168.1.4/export/mp3\n", "OK\n", ) res = self.client.listmounts() self.assertEqual( [ {"mount": "", "storage": "/home/foo/music"}, {"mount": "foo", "storage": "nfs://192.168.1.4/export/mp3"}, ], res, ) def test_parse_neighbors(self): self.MPDWillReturn( "neighbor: smb://FOO\n", "name: FOO (Samba 4.1.11-Debian)\n", "OK\n" ) res = self.client.listneighbors() self.assertEqual( [{"name": "FOO (Samba 4.1.11-Debian)", "neighbor": "smb://FOO"}], res ) def test_parse_outputs(self): self.MPDWillReturn( "outputid: 0\n", "outputname: My ALSA Device\n", "outputenabled: 0\n", "OK\n", ) res = self.client.outputs() self.assertEqual( [{"outputenabled": "0", "outputid": "0", "outputname": "My ALSA Device"}], res, ) def test_parse_playlist(self): self.MPDWillReturn( "0:file: Weezer - Say It Ain't So.mp3\n", "1:file: Dire Straits - Walk of Life.mp3\n", "2:file: 01 - Love Delicatessen.mp3\n", "3:file: Guns N' Roses - Paradise City.mp3\n", "4:file: Nirvana - Lithium.mp3\n", "OK\n", ) res = self.client.playlist() self.assertEqual( [ "file: Weezer - Say It Ain't So.mp3", "file: Dire Straits - Walk of Life.mp3", "file: 01 - Love Delicatessen.mp3", "file: Guns N' Roses - Paradise City.mp3", "file: Nirvana - Lithium.mp3", ], res, ) def test_parse_playlists(self): self.MPDWillReturn( "playlist: Playlist\n", "Last-Modified: 2016-08-13T10:55:56Z\n", "OK\n" ) res = self.client.listplaylists() self.assertEqual( [{"last-modified": "2016-08-13T10:55:56Z", "playlist": "Playlist"}], res ) def test_parse_plugins(self): self.MPDWillReturn( "plugin: vorbis\n", "suffix: ogg\n", "suffix: oga\n", "mime_type: application/ogg\n", "mime_type: application/x-ogg\n", "mime_type: audio/ogg\n", "mime_type: audio/vorbis\n", "mime_type: audio/vorbis+ogg\n", "mime_type: audio/x-ogg\n", "mime_type: audio/x-vorbis\n", "mime_type: audio/x-vorbis+ogg\n", "OK\n", ) res = self.client.decoders() self.assertEqual( [ { "mime_type": [ "application/ogg", "application/x-ogg", "audio/ogg", "audio/vorbis", "audio/vorbis+ogg", "audio/x-ogg", "audio/x-vorbis", "audio/x-vorbis+ogg", ], "plugin": "vorbis", "suffix": ["ogg", "oga"], } ], list(res), ) def test_parse_raw_stickers(self): self.MPDWillReturn("sticker: foo=bar\n", "OK\n") res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "bar")], list(res)) self.MPDWillReturn("sticker: foo=bar\n", "sticker: l=b\n", "OK\n") res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "bar"), ("l", "b")], list(res)) def test_parse_raw_sticker_with_special_value(self): self.MPDWillReturn("sticker: foo==uv=vu\n", "OK\n") res = self.client._parse_raw_stickers(self.client._read_lines()) self.assertEqual([("foo", "=uv=vu")], list(res)) def test_parse_sticket_get_one(self): self.MPDWillReturn("sticker: foo=bar\n", "OK\n") res = self.client.sticker_get("song", "baz", "foo") self.assertEqual("bar", res) def test_parse_sticket_get_no_sticker(self): self.MPDWillReturn("ACK [50@0] {sticker} no such sticker\n") self.assertRaises( mpd.CommandError, self.client.sticker_get, "song", "baz", "foo" ) def test_parse_sticker_list(self): self.MPDWillReturn("sticker: foo=bar\n", "sticker: lom=bok\n", "OK\n") res = self.client.sticker_list("song", "baz") self.assertEqual({"foo": "bar", "lom": "bok"}, res) # Even with only one sticker, we get a dict self.MPDWillReturn("sticker: foo=bar\n", "OK\n") res = self.client.sticker_list("song", "baz") self.assertEqual({"foo": "bar"}, res) def test_command_list(self): self.MPDWillReturn( "list_OK\n", "list_OK\n", "list_OK\n", "list_OK\n", "list_OK\n", "volume: 100\n", "repeat: 1\n", "random: 1\n", "single: 0\n", "consume: 0\n", "playlist: 68\n", "playlistlength: 5\n", "mixrampdb: 0.000000\n", "state: play\n", "xfade: 5\n", "song: 0\n", "songid: 56\n", "time: 0:259\n", "elapsed: 0.000\n", "bitrate: 0\n", "nextsong: 2\n", "nextsongid: 58\n", "list_OK\n", "OK\n", ) self.client.command_list_ok_begin() self.client.clear() self.client.load("Playlist") self.client.random(1) self.client.repeat(1) self.client.play(0) self.client.status() res = self.client.command_list_end() self.assertEqual(None, res[0]) self.assertEqual(None, res[1]) self.assertEqual(None, res[2]) self.assertEqual(None, res[3]) self.assertEqual(None, res[4]) self.assertEqual( [ ("bitrate", "0"), ("consume", "0"), ("elapsed", "0.000"), ("mixrampdb", "0.000000"), ("nextsong", "2"), ("nextsongid", "58"), ("playlist", "68"), ("playlistlength", "5"), ("random", "1"), ("repeat", "1"), ("single", "0"), ("song", "0"), ("songid", "56"), ("state", "play"), ("time", "0:259"), ("volume", "100"), ("xfade", "5"), ], sorted(res[5].items()), ) # MPD client tests which do not mock the socket, but rather replace it # with a real socket from a socket @unittest.skipIf( not hasattr(socket, "socketpair"), "Socketpair is not supported on this platform" ) class TestMPDClientSocket(unittest.TestCase): longMessage = True def setUp(self): self.connect_patch = mock.patch("mpd.MPDClient._connect_unix") self.connect_mock = self.connect_patch.start() test_socketpair = socket.socketpair() self.connect_mock.return_value = test_socketpair[0] self.server_socket = test_socketpair[1] self.server_socket_reader = self.server_socket.makefile("rb") self.server_socket_writer = self.server_socket.makefile("wb") self.MPDWillReturnBinary(b"OK MPD 0.21.24\n") self.client = mpd.MPDClient() self.client.connect(TEST_MPD_UNIXHOST) self.client.timeout = TEST_MPD_UNIXTIMEOUT self.connect_mock.assert_called_once() def tearDown(self): self.close_server_socket() self.connect_patch.stop() def close_server_socket(self): self.server_socket_reader.close() self.server_socket_writer.close() self.server_socket.close() def MPDWillReturnBinary(self, byteStr): self.server_socket_writer.write(byteStr) self.server_socket_writer.flush() def assertMPDReceived(self, byteStr): """ Assert MPD received the given bytestring. Note: this disconnects the client. """ # to ensure we don't block, close the socket on client side self.client.disconnect() # read one extra to ensure nothing extraneous was written received = self.server_socket_reader.read(len(byteStr) + 1) self.assertEqual(received, byteStr) def test_readbinary_error(self): self.MPDWillReturnBinary(b"ACK [50@0] {albumart} No file exists\n") self.assertRaises( mpd.CommandError, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') def test_binary_albumart_disconnect_afterchunk(self): self.MPDWillReturnBinary(b"size: 17\nbinary: 3\n" b"\x00\x00\x00\nOK\n") # we're expecting a timeout self.assertRaises( socket.timeout, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived( b'albumart "a/full/path.mp3" "0"\nalbumart "a/full/path.mp3" "3"\n' ) self.assertIs(self.client._sock, None) def test_binary_albumart_disconnect_midchunk(self): self.MPDWillReturnBinary(b"size: 8\nbinary: 8\n\x00\x01\x02\x03") # we're expecting a timeout or error of some form self.assertRaises( socket.timeout, lambda: self.client.albumart("a/full/path.mp3") ) self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') self.assertIs(self.client._sock, None) def test_binary_albumart_singlechunk_networkmultiwrite(self): # length 16 expected_binary = ( b"\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF" ) self.MPDWillReturnBinary(b"binary: 16\n") self.MPDWillReturnBinary(expected_binary[0:4]) self.MPDWillReturnBinary(expected_binary[4:9]) self.MPDWillReturnBinary(expected_binary[9:14]) self.MPDWillReturnBinary(expected_binary[14:16]) self.MPDWillReturnBinary(b"\nOK\n") real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') self.assertEqual(real_binary, {"binary": expected_binary}) def test_binary_albumart_singlechunk_nosize(self): # length: 16 expected_binary = ( b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" ) self.MPDWillReturnBinary(b"binary: 16\n" + expected_binary + b"\nOK\n") real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') self.assertEqual(real_binary, {"binary": expected_binary}) def test_binary_albumart_singlechunk_sizeheader(self): # length: 16 expected_binary = ( b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" ) self.MPDWillReturnBinary( b"size: 16\nbinary: 16\n" + expected_binary + b"\nOK\n" ) real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') self.assertEqual(real_binary, {"binary": expected_binary}) def test_binary_albumart_even_multichunk(self): # length: 16 each expected_chunk1 = ( b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" ) expected_chunk2 = ( b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11\x13" ) expected_chunk3 = ( b"\x99\x88\x77\xDD\xD0\xF0\x20\x70\x71\x17\x13\x31\xFF\xFF\xDD\xFF" ) expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 # 3 distinct commands expected self.MPDWillReturnBinary( b"size: 48\nbinary: 16\n" + expected_chunk1 + b"\nOK\nsize: 48\nbinary: 16\n" + expected_chunk2 + b"\nOK\nsize: 48\nbinary: 16\n" + expected_chunk3 + b"\nOK\n" ) real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived( b'albumart "a/full/path.mp3" "0"\nalbumart "a/full/path.mp3" "16"' b'\nalbumart "a/full/path.mp3" "32"\n' ) self.assertEqual(real_binary, {"binary": expected_binary}) def test_binary_albumart_odd_multichunk(self): # lengths: 17, 15, 1 expected_chunk1 = ( b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01\x13" ) expected_chunk2 = ( b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11" ) expected_chunk3 = b"\x99" expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 # 3 distinct commands expected self.MPDWillReturnBinary( b"size: 33\nbinary: 17\n" + expected_chunk1 + b"\nOK\nsize: 33\nbinary: 15\n" + expected_chunk2 + b"\nOK\nsize: 33\nbinary: 1\n" + expected_chunk3 + b"\nOK\n" ) real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived( b'albumart "a/full/path.mp3" "0"\nalbumart "a/full/path.mp3" "17"\n' b'albumart "a/full/path.mp3" "32"\n' ) self.assertEqual(real_binary, {"binary": expected_binary}) # MPD server can return empty response if a file exists but is empty def test_binary_albumart_emptyresponse(self): self.MPDWillReturnBinary(b"size: 0\nbinary: 0\n\nOK\n") real_binary = self.client.albumart("a/full/path.mp3") self.assertMPDReceived(b'albumart "a/full/path.mp3" "0"\n') self.assertEqual(real_binary, {"binary": b""}) # readpicture returns empty object if the song exists but has no picture def test_binary_readpicture_emptyresponse(self): self.MPDWillReturnBinary(b"OK\n") real_binary = self.client.readpicture("plainsong.mp3") self.assertMPDReceived(b'readpicture "plainsong.mp3" "0"\n') self.assertEqual(real_binary, {}) def test_binary_readpicture_untyped(self): # length: 16 each expected_chunk1 = ( b"\x01\x02\x00\x03\x04\x00\xFF\x05\x07\x08\x0A\x0F\xF0\xA5\x00\x01" ) expected_chunk2 = ( b"\x0A\x0B\x0C\x0D\x0E\x0F\x10\x1F\x2F\x2D\x33\x0D\x00\x00\x11\x13" ) expected_chunk3 = ( b"\x99\x88\x77\xDD\xD0\xF0\x20\x70\x71\x17\x13\x31\xFF\xFF\xDD\xFF" ) expected_binary = expected_chunk1 + expected_chunk2 + expected_chunk3 # 3 distinct commands expected self.MPDWillReturnBinary( b"size: 48\nbinary: 16\n" + expected_chunk1 + b"\nOK\nsize: 48\nbinary: 16\n" + expected_chunk2 + b"\nOK\nsize: 48\nbinary: 16\n" + expected_chunk3 + b"\nOK\n" ) real_binary = self.client.readpicture("a/full/path.mp3") self.assertMPDReceived( b'readpicture "a/full/path.mp3" "0"\nreadpicture "a/full/path.mp3" "16"' b'\nreadpicture "a/full/path.mp3" "32"\n' ) self.assertEqual(real_binary, {"binary": expected_binary}) def test_binary_readpicture_typed(self): # length: 16 each expected_binary = bytes(range(48)) # 3 distinct commands expected self.MPDWillReturnBinary( b"size: 48\ntype: image/png\nbinary: 16\n" + expected_binary[0:16] + b"\nOK\nsize: 48\ntype: image/png\nbinary: 16\n" + expected_binary[16:32] + b"\nOK\nsize: 48\ntype: image/png\nbinary: 16\n" + expected_binary[32:48] + b"\nOK\n" ) real_binary = self.client.readpicture("a/full/path.mp3") self.assertMPDReceived( b'readpicture "a/full/path.mp3" "0"\nreadpicture "a/full/path.mp3" "16"' b'\nreadpicture "a/full/path.mp3" "32"\n' ) self.assertEqual(real_binary, {"binary": expected_binary, "type": "image/png"}) def test_binary_readpicture_badheaders(self): expected_binary = bytes(range(32)) # inconsistent type header from response 1 to response 2 # exception is expected self.MPDWillReturnBinary( b"size: 32\ntype: image/jpeg\nbinary: 16\n" + expected_binary[0:16] + b"\nOK\nsize: 32\ntype: image/png\nbinary: 16\n" + expected_binary[16:32] + b"\nOK\n" ) self.assertRaises( mpd.CommandError, lambda: self.client.readpicture("song.mp3") ) self.assertMPDReceived( b'readpicture "song.mp3" "0"\nreadpicture "song.mp3" "16"\n' ) class MockTransport(object): def __init__(self): self.written = list() def clear(self): self.written = list() def write(self, data): self.written.append(data) @unittest.skipIf(TWISTED_MISSING, "requires twisted to be installed") class TestMPDProtocol(unittest.TestCase): def init_protocol(self, default_idle=True, idle_result=None): self.protocol = mpd.MPDProtocol( default_idle=default_idle, idle_result=idle_result ) self.protocol.transport = MockTransport() def test_create_command(self): self.init_protocol(default_idle=False) self.assertEqual(self.protocol._create_command("play"), b"play") self.assertEqual( self.protocol._create_command("rangeid", args=["1", ()]), b'rangeid "1" ":"' ) self.assertEqual( self.protocol._create_command("rangeid", args=["1", (1,)]), b'rangeid "1" "1:"', ) self.assertEqual( self.protocol._create_command("rangeid", args=["1", (1, 2)]), b'rangeid "1" "1:2"', ) def test_success(self): self.init_protocol(default_idle=False) def success(result): expected = { "file": "Dire Straits - Walk of Life.mp3", "artist": "Dire Straits", "title": "Walk of Life", "genre": "Rock/Pop", "track": "3", "album": "Brothers in Arms", "id": "13", "last-modified": "2016-08-11T10:57:03Z", "pos": "4", "time": "253", } self.assertEqual(expected, result) self.protocol.currentsong().addCallback(success) self.assertEqual([b"currentsong\n"], self.protocol.transport.written) for line in [ b"file: Dire Straits - Walk of Life.mp3", b"Last-Modified: 2016-08-11T10:57:03Z", b"Time: 253", b"Artist: Dire Straits", b"Title: Walk of Life", b"Album: Brothers in Arms", b"Track: 3", b"Genre: Rock/Pop", b"Pos: 4", b"Id: 13", b"OK", ]: self.protocol.lineReceived(line) def test_failure(self): self.init_protocol(default_idle=False) def error(result): self.assertIsInstance(result, Failure) self.assertEqual(result.getErrorMessage(), "[50@0] {load} No such playlist") self.protocol.load("Foo").addErrback(error) self.assertEqual([b'load "Foo"\n'], self.protocol.transport.written) self.protocol.lineReceived(b"ACK [50@0] {load} No such playlist") def test_default_idle(self): def idle_result(result): self.assertEqual(list(result), ["player"]) self.init_protocol(idle_result=idle_result) self.protocol.lineReceived(b"OK MPD 0.18.0") self.assertEqual([b"idle\n"], self.protocol.transport.written) self.protocol.transport.clear() self.protocol.lineReceived(b"changed: player") self.protocol.lineReceived(b"OK") self.assertEqual([b"idle\n"], self.protocol.transport.written) def test_noidle_when_default_idle(self): self.init_protocol() self.protocol.lineReceived(b"OK MPD 0.18.0") self.protocol.pause() self.protocol.lineReceived(b"OK") self.protocol.lineReceived(b"OK") self.assertEqual( [b"idle\n", b"noidle\n", b"pause\n", b"idle\n"], self.protocol.transport.written, ) def test_already_idle(self): self.init_protocol(default_idle=False) self.protocol.idle() self.assertRaises(mpd.CommandError, lambda: self.protocol.idle()) def test_already_noidle(self): self.init_protocol(default_idle=False) self.assertRaises(mpd.CommandError, lambda: self.protocol.noidle()) def test_command_list(self): self.init_protocol(default_idle=False) def success(result): self.assertEqual([None, None], result) self.protocol.command_list_ok_begin() self.protocol.play() self.protocol.stop() self.protocol.command_list_end().addCallback(success) self.assertEqual( [b"command_list_ok_begin\n", b"play\n", b"stop\n", b"command_list_end\n",], self.protocol.transport.written, ) self.protocol.transport.clear() self.protocol.lineReceived(b"list_OK") self.protocol.lineReceived(b"list_OK") self.protocol.lineReceived(b"OK") def test_command_list_failure(self): self.init_protocol(default_idle=False) def load_command_error(result): self.assertIsInstance(result, Failure) self.assertEqual(result.getErrorMessage(), "[50@0] {load} No such playlist") def command_list_general_error(result): self.assertIsInstance(result, Failure) self.assertEqual(result.getErrorMessage(), "An earlier command failed.") self.protocol.command_list_ok_begin() self.protocol.load("Foo").addErrback(load_command_error) self.protocol.play().addErrback(command_list_general_error) self.protocol.command_list_end().addErrback(load_command_error) self.assertEqual( [ b"command_list_ok_begin\n", b'load "Foo"\n', b"play\n", b"command_list_end\n", ], self.protocol.transport.written, ) self.protocol.lineReceived(b"ACK [50@0] {load} No such playlist") def test_command_list_when_default_idle(self): self.init_protocol() self.protocol.lineReceived(b"OK MPD 0.18.0") def success(result): self.assertEqual([None, None], result) self.protocol.command_list_ok_begin() self.protocol.play() self.protocol.stop() self.protocol.command_list_end().addCallback(success) self.assertEqual( [ b"idle\n", b"noidle\n", b"command_list_ok_begin\n", b"play\n", b"stop\n", b"command_list_end\n", ], self.protocol.transport.written, ) self.protocol.transport.clear() self.protocol.lineReceived(b"OK") self.protocol.lineReceived(b"list_OK") self.protocol.lineReceived(b"list_OK") self.protocol.lineReceived(b"OK") self.assertEqual([b"idle\n"], self.protocol.transport.written) def test_command_list_failure_when_default_idle(self): self.init_protocol() self.protocol.lineReceived(b"OK MPD 0.18.0") def load_command_error(result): self.assertIsInstance(result, Failure) self.assertEqual(result.getErrorMessage(), "[50@0] {load} No such playlist") def command_list_general_error(result): self.assertIsInstance(result, Failure) self.assertEqual(result.getErrorMessage(), "An earlier command failed.") self.protocol.command_list_ok_begin() self.protocol.load("Foo").addErrback(load_command_error) self.protocol.play().addErrback(command_list_general_error) self.protocol.command_list_end().addErrback(load_command_error) self.assertEqual( [ b"idle\n", b"noidle\n", b"command_list_ok_begin\n", b'load "Foo"\n', b"play\n", b"command_list_end\n", ], self.protocol.transport.written, ) self.protocol.transport.clear() self.protocol.lineReceived(b"OK") self.protocol.lineReceived(b"ACK [50@0] {load} No such playlist") self.assertEqual([b"idle\n"], self.protocol.transport.written) def test_command_list_item_is_generator(self): self.init_protocol(default_idle=False) def success(result): self.assertEqual( result, [ [ "Weezer - Say It Ain't So.mp3", "Dire Straits - Walk of Life.mp3", "01 - Love Delicatessen.mp3", "Guns N' Roses - Paradise City.mp3", ] ], ) self.protocol.command_list_ok_begin() self.protocol.listplaylist("Foo") self.protocol.command_list_end().addCallback(success) self.protocol.lineReceived(b"file: Weezer - Say It Ain't So.mp3") self.protocol.lineReceived(b"file: Dire Straits - Walk of Life.mp3") self.protocol.lineReceived(b"file: 01 - Love Delicatessen.mp3") self.protocol.lineReceived(b"file: Guns N' Roses - Paradise City.mp3") self.protocol.lineReceived(b"list_OK") self.protocol.lineReceived(b"OK") def test_already_in_command_list(self): self.init_protocol(default_idle=False) self.protocol.command_list_ok_begin() self.assertRaises( mpd.CommandListError, lambda: self.protocol.command_list_ok_begin() ) def test_not_in_command_list(self): self.init_protocol(default_idle=False) self.assertRaises( mpd.CommandListError, lambda: self.protocol.command_list_end() ) def test_invalid_command_in_command_list(self): self.init_protocol(default_idle=False) self.protocol.command_list_ok_begin() self.assertRaises(mpd.CommandListError, lambda: self.protocol.kill()) def test_close(self): self.init_protocol(default_idle=False) def success(result): self.assertEqual(result, None) self.protocol.close().addCallback(success) class AsyncMockServer: def __init__(self): self._output = asyncio.Queue() self._expectations = [] def get_streams(self): result = asyncio.Future() result.set_result((self, self)) return result def readline(self): # directly passing around the awaitable return self._output.get() async def readexactly(self, length): ret = await self._output.get() if len(ret) != length: self.error("Mock data is not chuncked in the way the client expects to read it") return ret def write(self, data): try: next_write = self._expectations[0][0][0] except IndexError: self.error("Data written to mock even though none expected: %r" % data) if next_write == data: self._expectations[0][0].pop(0) self._feed() else: self.error("Mock got %r, expected %r" % (data, next_write)) def close(self): # todo: make sure calls to self.write fail after calling close pass def error(self, message): raise AssertionError(message) def _feed(self): if len(self._expectations[0][0]) == 0: _, response_lines = self._expectations.pop(0) for l in response_lines: self._output.put_nowait(l) def expect_exchange(self, request_lines, response_lines): self._expectations.append((request_lines, response_lines)) self._feed() class TestAsyncioMPD(unittest.TestCase): def init_client(self, odd_hello=None): self.loop = asyncio.get_event_loop() self.mockserver = AsyncMockServer() asyncio.open_connection = mock.MagicMock( return_value=self.mockserver.get_streams() ) if odd_hello is None: hello_lines = [b"OK MPD mocker\n"] else: hello_lines = odd_hello self.mockserver.expect_exchange([], hello_lines) self.client = mpd.asyncio.MPDClient() self._await(self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT, loop=self.loop)) asyncio.open_connection.assert_called_with( TEST_MPD_HOST, TEST_MPD_PORT, loop=self.loop ) def _await(self, future): return self.loop.run_until_complete(future) def test_oddhello(self): self.assertRaises( mpd.base.ProtocolError, self.init_client, odd_hello=[b"NOT OK\n"] ) @unittest.skipIf( os.getenv("RUN_SLOW_TESTS") is None, "This test would add 5 seconds of idling to the run (export RUN_SLOW_TESTS=1 to run anyway)", ) def test_noresponse(self): self.assertRaises(mpd.base.ConnectionError, self.init_client, odd_hello=[]) def test_status(self): self.init_client() self.mockserver.expect_exchange( [b"status\n"], [ b"volume: 70\n", b"repeat: 0\n", b"random: 1\n", b"single: 0\n", b"consume: 0\n", b"playlist: 416\n", b"playlistlength: 7\n", b"mixrampdb: 0.000000\n", b"state: play\n", b"song: 4\n", b"songid: 19\n", b"time: 28:403\n", b"elapsed: 28.003\n", b"bitrate: 465\n", b"duration: 403.066\n", b"audio: 44100:16:2\n", b"OK\n", ], ) status = self._await(self.client.status()) self.assertEqual( status, { "audio": "44100:16:2", "bitrate": "465", "consume": "0", "duration": "403.066", "elapsed": "28.003", "mixrampdb": "0.000000", "playlist": "416", "playlistlength": "7", "random": "1", "repeat": "0", "single": "0", "song": "4", "songid": "19", "state": "play", "time": "28:403", "volume": "70", }, ) async def _test_outputs(self): self.mockserver.expect_exchange( [b"outputs\n"], [ b"outputid: 0\n", b"outputname: My ALSA Device\n", b"plugin: alsa\n", b"outputenabled: 0\n", b"attribute: dop=0\n", b"outputid: 1\n", b"outputname: My FM transmitter\n", b"plugin: fmradio\n", b"outputenabled: 1\n", b"OK\n", ], ) outputs = self.client.outputs() expected = iter( [ { "outputid": "0", "outputname": "My ALSA Device", "plugin": "alsa", "outputenabled": "0", "attribute": "dop=0", }, { "outputid": "1", "outputname": "My FM transmitter", "plugin": "fmradio", "outputenabled": "1", }, ] ) async for o in outputs: self.assertEqual(o, next(expected)) self.assertRaises(StopIteration, next, expected) def test_outputs(self): self.init_client() self._await(self._test_outputs()) async def _test_list(self): self.mockserver.expect_exchange( [b'list "album"\n'], [b"Album: first\n", b"Album: second\n", b"OK\n",] ) list_ = self.client.list("album") expected = iter([{"album": "first"}, {"album": "second"},]) async for o in list_: self.assertEqual(o, next(expected)) self.assertRaises(StopIteration, next, expected) def test_list(self): self.init_client() self._await(self._test_list()) async def _test_albumart(self): self.mockserver.expect_exchange( [b'albumart "x.mp3" "0"\n'], [ b"size: 32\n", b"binary: 16\n", bytes(range(16)), b"\n", b"OK\n", ] ) self.mockserver.expect_exchange( [b'albumart "x.mp3" "16"\n'], [ b"size: 32\n", b"binary: 16\n", bytes(range(16)), b"\n", b"OK\n", ], ) albumart = await self.client.albumart("x.mp3") expected = {"binary": bytes(range(16)) + bytes(range(16))} self.assertEqual(albumart, expected) async def _test_readpicture(self): self.mockserver.expect_exchange( [b'readpicture "x.mp3" "0"\n'], [ b"size: 32\n", b"type: image/jpeg\n", b"binary: 16\n", bytes(range(16)), b"\n", b"OK\n", ] ) self.mockserver.expect_exchange( [b'readpicture "x.mp3" "16"\n'], [ b"size: 32\n", b"type: image/jpeg\n", b"binary: 16\n", bytes(range(16)), b"\n", b"OK\n", ], ) art = await self.client.readpicture("x.mp3") expected = {"binary": bytes(range(16)) + bytes(range(16)), "type": "image/jpeg"} self.assertEqual(art, expected) async def _test_readpicture_empty(self): self.mockserver.expect_exchange( [b'readpicture "x.mp3" "0"\n'], [ b"OK\n", ] ) art = await self.client.readpicture("x.mp3") expected = {} self.assertEqual(art, expected) def test_albumart(self): self.init_client() self._await(self._test_albumart()) def test_readpicture(self): self.init_client() self._await(self._test_readpicture()) def test_readpicture_empty(self): self.init_client() self._await(self._test_readpicture_empty()) def test_mocker(self): """Does the mock server refuse unexpected writes?""" self.init_client() self.mockserver.expect_exchange([b"expecting odd things\n"], [b""]) self.assertRaises(AssertionError, self._await, self.client.status()) if __name__ == "__main__": unittest.main() 0707010000002C000081A40000000000000000000000015FE435FD000023D9000000000000000000000000000000000000002100000000python-mpd2-3.0.1/mpd/twisted.py# python-mpd2: Python MPD client library # # Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net> # Copyright (C) 2010 Jasper St. Pierre <jstpierre@mecheye.net> # Copyright (C) 2010-2011 Oliver Mader <b52@reaktor42.de> # Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at> # # python-mpd2 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 3 of the License, or # (at your option) any later version. # # python-mpd2 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 python-mpd2. If not, see <http://www.gnu.org/licenses/>. # # THIS MODULE IS EXPERIMENTAL. AS SOON AS IT IS CONSIDERED STABLE THIS NOTE # WILL BE REMOVED. PLEASE REPORT INCONSISTENCIES, BUGS AND IMPROVEMENTS AT # https://github.com/Mic92/python-mpd2/issues from __future__ import absolute_import from __future__ import unicode_literals from mpd.base import CommandError from mpd.base import CommandListError from mpd.base import ERROR_PREFIX from mpd.base import HELLO_PREFIX from mpd.base import MPDClientBase from mpd.base import NEXT from mpd.base import SUCCESS from mpd.base import escape from mpd.base import logger from mpd.base import mpd_command_provider from mpd.base import mpd_commands from twisted.internet import defer from twisted.protocols import basic import threading import types def lock(func): def wrapped(self, *args, **kwargs): with self._lock: return func(self, *args, **kwargs) return wrapped def _create_command(wrapper, name, callback): def mpd_command(self, *args): def bound_callback(lines): return callback(self, lines) bound_callback.callback = callback return wrapper(self, name, args, bound_callback) return mpd_command @mpd_command_provider class MPDProtocol(basic.LineReceiver, MPDClientBase): delimiter = b"\n" def __init__(self, default_idle=True, idle_result=None): super(MPDProtocol, self).__init__() # flag whether client should idle by default self._default_idle = default_idle self.idle_result = idle_result self._reset() self._lock = threading.RLock() def _reset(self): super(MPDProtocol, self)._reset() self.mpd_version = None self._command_list = False self._command_list_results = [] self._rcvd_lines = [] self._state = [] self._idle = False @classmethod def add_command(cls, name, callback): # ignore commands which are implemented on class directly if getattr(cls, name, None) is not None: return # create command and hook it on class func = _create_command(cls._execute, name, callback) escaped_name = name.replace(" ", "_") setattr(cls, escaped_name, func) @lock def lineReceived(self, line): line = line.decode("utf-8") command_list = self._state and isinstance(self._state[0], list) state_list = self._state[0] if command_list else self._state if line.startswith(HELLO_PREFIX): self.mpd_version = line[len(HELLO_PREFIX) :].strip() # default state idle, enter idle if self._default_idle: self.idle().addCallback(self._dispatch_idle_result) elif line.startswith(ERROR_PREFIX): error = line[len(ERROR_PREFIX) :].strip() if command_list: state_list[0].errback(CommandError(error)) for state in state_list[1:-1]: state.errback(CommandListError("An earlier command failed.")) state_list[-1].errback(CommandListError(error)) del self._state[0] del self._command_list_results[0] else: state_list.pop(0).errback(CommandError(error)) self._continue_idle() elif line == SUCCESS or (command_list and line == NEXT): state_list.pop(0).callback(self._rcvd_lines[:]) self._rcvd_lines = [] if command_list and line == SUCCESS: del self._state[0] self._continue_idle() else: self._rcvd_lines.append(line) def _lookup_callback(self, parser): if hasattr(parser, "callback"): return parser.callback return parser @lock def _execute(self, command, args, parser): # close or kill command in command list not allowed if self._command_list and self._lookup_callback(parser) is self.NOOP: msg = "{} not allowed in command list".format(command) raise CommandListError(msg) # default state idle and currently in idle state, trigger noidle if self._default_idle and self._idle and command != "idle": self.noidle().addCallback(self._dispatch_noidle_result) # write command to MPD self._write_command(command, args) # create command related deferred deferred = defer.Deferred() # extend pending result queue state = self._state[-1] if self._command_list else self._state state.append(deferred) # NOOP is for close and kill commands if self._lookup_callback(parser) is not self.NOOP: # attach command related result parser deferred.addCallback(parser) # command list, attach handler for collecting command list results if self._command_list: deferred.addCallback(self._parse_command_list_item) return deferred def _create_command(self, command, args=[]): # XXX: this function should be generalized in future. There exists # almost identical code in ``MPDClient._write_command``, with the # difference that it's using ``encode_str`` for text arguments. parts = [command] for arg in args: if type(arg) is tuple: if len(arg) == 0: parts.append('":"') elif len(arg) == 1: parts.append('"{}:"'.format(int(arg[0]))) else: parts.append('"{}:{}"'.format(int(arg[0]), int(arg[1]))) else: parts.append('"{}"'.format(escape(arg))) return " ".join(parts).encode("utf-8") def _write_command(self, command, args=[]): self.sendLine(self._create_command(command, args)) def _parse_command_list_item(self, result): if isinstance(result, types.GeneratorType): result = list(result) self._command_list_results[0].append(result) return result def _parse_command_list_end(self, lines): return self._command_list_results.pop(0) @mpd_commands(*MPDClientBase._parse_nothing.mpd_commands) def _parse_nothing(self, lines): return None def _continue_idle(self): if self._default_idle and not self._idle and not self._state: self.idle().addCallback(self._dispatch_idle_result) def _do_dispatch(self, result): if self.idle_result: self.idle_result(result) else: res = list(result) msg = "MPDProtocol: no idle callback defined: {}".format(res) logger.warning(msg) def _dispatch_noidle_result(self, result): self._do_dispatch(result) def _dispatch_idle_result(self, result): self._idle = False self._do_dispatch(result) self._continue_idle() def idle(self): if self._idle: raise CommandError("Already in idle state") self._idle = True return self._execute("idle", [], self._parse_list) def noidle(self): if not self._idle: raise CommandError("Not in idle state") # delete first pending deferred, idle returns nothing when # noidle gets called self._state.pop(0) self._idle = False return self._execute("noidle", [], self._parse_list) def command_list_ok_begin(self): if self._command_list: raise CommandListError("Already in command list") if self._default_idle and self._idle: self.noidle().addCallback(self._dispatch_noidle_result) self._write_command("command_list_ok_begin") self._command_list = True self._command_list_results.append([]) self._state.append([]) def command_list_end(self): if not self._command_list: raise CommandListError("Not in command list") self._write_command("command_list_end") deferred = defer.Deferred() deferred.addCallback(self._parse_command_list_end) self._state[-1].append(deferred) self._command_list = False return deferred # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 0707010000002D000081A40000000000000000000000015FE435FD000000B9000000000000000000000000000000000000001C00000000python-mpd2-3.0.1/setup.cfg[sdist] formats = gztar [bdist_wheel] universal=1 [build_sphinx] source-dir = doc/ build-dir = doc/_build all_files = 1 [upload_sphinx] upload-dir = doc/_build/html [easy_install] 0707010000002E000081A40000000000000000000000015FE435FD00000AAE000000000000000000000000000000000000001B00000000python-mpd2-3.0.1/setup.py#! /usr/bin/env python from setuptools import find_packages from setuptools import setup from setuptools.command.test import test as TestCommand import mpd import os import sys if sys.version_info[0] == 2: from io import open VERSION = ".".join(map(str, mpd.VERSION)) CLASSIFIERS = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ] LICENSE = """\ Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net> Copyright (C) 2012-2017 Joerg Thalheim <joerg@thalheim.io> Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at> python-mpd2 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 3 of the License, or (at your option) any later version. python-mpd2 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 python-mpd2. If not, see <http://www.gnu.org/licenses/>.\ """ class Tox(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import tox errno = tox.cmdline(self.test_args) sys.exit(errno) def read(fname): with open(os.path.join(os.path.dirname(__file__), fname), encoding="utf8") as fd: return fd.read() setup( name="python-mpd2", version=VERSION, python_requires='>=3.6', description="A Python MPD client library", long_description=read('README.rst'), classifiers=CLASSIFIERS, author="Joerg Thalheim", author_email="joerg@thalheim.io", license="GNU Lesser General Public License v3 (LGPLv3)", url="https://github.com/Mic92/python-mpd2", packages=find_packages(), zip_safe=True, keywords=["mpd"], test_suite="mpd.tests", tests_require=[ 'tox', 'mock', 'Twisted' ], cmdclass={ 'test': Tox }, extras_require={ 'twisted': ['Twisted'] } ) # vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 0707010000002F000081A40000000000000000000000015FE435FD000000E9000000000000000000000000000000000000001C00000000python-mpd2-3.0.1/shell.nixwith import <nixpkgs> {}; stdenv.mkDerivation { name = "env"; buildInputs = [ bashInteractive python36 python37 (python38.withPackages(ps: [ps.setuptools ps.tox ps.wheel ps.twine])) python39 pypy3 ]; } 07070100000030000081A40000000000000000000000015FE435FD000000F7000000000000000000000000000000000000001A00000000python-mpd2-3.0.1/tox.ini[tox] envlist = py36,py37,py38,py39,pypy3 [testenv] deps = mock coverage Twisted commands = coverage erase coverage run -m unittest mpd.tests coverage report coverage html -d coverage_html/{envname} 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!474 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor