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