Merge branch 'gstreamer' of git://github.com/jodal/mopidy into gstreamer
This commit is contained in:
commit
940f65393c
339
COPYING
339
COPYING
@ -1,339 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) 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
|
|
||||||
this service 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 make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. 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.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the 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 a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE 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.
|
|
||||||
|
|
||||||
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
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the 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 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision 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, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This 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.
|
|
||||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@ -1,4 +1,5 @@
|
|||||||
include COPYING pylintrc *.rst *.txt
|
include LICENSE pylintrc *.rst *.txt
|
||||||
|
include mopidy/backends/libspotify/spotify_appkey.key
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
prune docs/_build
|
prune docs/_build
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
|
|||||||
@ -82,14 +82,6 @@ Manages the music library, e.g. searching for tracks to be added to a playlist.
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
:mod:`mopidy.backends.despotify` -- Despotify backend
|
|
||||||
=====================================================
|
|
||||||
|
|
||||||
.. automodule:: mopidy.backends.despotify
|
|
||||||
:synopsis: Spotify backend using the Despotify library
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
|
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ there.
|
|||||||
|
|
||||||
A complete ``~/.mopidy/settings.py`` may look like this::
|
A complete ``~/.mopidy/settings.py`` may look like this::
|
||||||
|
|
||||||
SERVER_HOSTNAME = u'0.0.0.0'
|
MPD_SERVER_HOSTNAME = u'::'
|
||||||
SPOTIFY_USERNAME = u'alice'
|
SPOTIFY_USERNAME = u'alice'
|
||||||
SPOTIFY_PASSWORD = u'mysecret'
|
SPOTIFY_PASSWORD = u'mysecret'
|
||||||
|
|
||||||
|
|||||||
@ -12,17 +12,31 @@ Another great release.
|
|||||||
|
|
||||||
**Changes**
|
**Changes**
|
||||||
|
|
||||||
|
- License changed from GPLv2 to Apache License, version 2.0.
|
||||||
- GStreamer is now a required dependency.
|
- GStreamer is now a required dependency.
|
||||||
- Exit early if not Python >= 2.6, < 3.
|
- Exit early if not Python >= 2.6, < 3.
|
||||||
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
|
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
|
||||||
the packages created by ``setup.py`` for i.e. PyPI.
|
the packages created by ``setup.py`` for i.e. PyPI.
|
||||||
- Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`.
|
- Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`.
|
||||||
|
- Changed ``SERVER_HOSTNAME`` and ``SERVER_PORT`` settings to
|
||||||
|
``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT``.
|
||||||
|
- Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained
|
||||||
|
and the Libspotify backend is working much better.
|
||||||
|
- :mod:`mopidy.backends.libspotify` is now the default backend.
|
||||||
|
- A Spotify application key is now bundled with the source. The
|
||||||
|
``SPOTIFY_LIB_APPKEY`` setting is thus removed.
|
||||||
- MPD frontend:
|
- MPD frontend:
|
||||||
|
|
||||||
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
|
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
|
||||||
- Split gigantic protocol implementation into eleven modules.
|
- Split gigantic protocol implementation into eleven modules.
|
||||||
- Search improvements, including support for multi-word search.
|
- Search improvements, including support for multi-word search.
|
||||||
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
|
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
|
||||||
|
- Support ``plchanges "-1"`` to work better with MPDroid.
|
||||||
|
- Support ``pause`` without arguments to work better with MPDroid.
|
||||||
|
- Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and
|
||||||
|
``single`` without quotes to work better with BitMPC.
|
||||||
|
- Fixed delete current playing track from playlist, which crashed several
|
||||||
|
clients.
|
||||||
|
|
||||||
- Backend API:
|
- Backend API:
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Mopidy'
|
project = u'Mopidy'
|
||||||
copyright = u'2010, Stein Magnus Jodal'
|
copyright = u'2010, Stein Magnus Jodal and contributors'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
|||||||
@ -11,8 +11,7 @@ Version 0.1
|
|||||||
|
|
||||||
- Core MPD server functionality working. Gracefully handle clients' use of
|
- Core MPD server functionality working. Gracefully handle clients' use of
|
||||||
non-supported functionality.
|
non-supported functionality.
|
||||||
- Read-only support for Spotify through :mod:`mopidy.backends.despotify` and/or
|
- Read-only support for Spotify through :mod:`mopidy.backends.libspotify`.
|
||||||
:mod:`mopidy.backends.libspotify`.
|
|
||||||
- Initial support for local file playback through
|
- Initial support for local file playback through
|
||||||
:mod:`mopidy.backends.local`. The state of local file playback will not
|
:mod:`mopidy.backends.local`. The state of local file playback will not
|
||||||
block the release of 0.1.
|
block the release of 0.1.
|
||||||
@ -32,15 +31,13 @@ released when we reach the other goal.
|
|||||||
Stuff we really want to do, but just not right now
|
Stuff we really want to do, but just not right now
|
||||||
==================================================
|
==================================================
|
||||||
|
|
||||||
- Replace libspotify with `openspotify
|
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies
|
||||||
<http://github.com/noahwilliamsson/openspotify>`_ for
|
for all our dependencies and Mopidy itself to make OS X installation a
|
||||||
:mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify
|
breeze. See `Homebrew's issue #1612
|
||||||
development has stalled.
|
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
|
||||||
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all our
|
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all our
|
||||||
dependencies and Mopidy itself (hosted in our own Debian repo until we get
|
dependencies and Mopidy itself (hosted in our own Debian repo until we get
|
||||||
stuff into the various distros) to make Debian/Ubuntu installation a breeze.
|
stuff into the various distros) to make Debian/Ubuntu installation a breeze.
|
||||||
- **[WIP]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies for
|
|
||||||
all our dependencies and Mopidy itself to make OS X installation a breeze.
|
|
||||||
- Run frontend tests against a real MPD server to ensure we are in sync.
|
- Run frontend tests against a real MPD server to ensure we are in sync.
|
||||||
- Start working with MPD client maintainers to get rid of weird assumptions
|
- Start working with MPD client maintainers to get rid of weird assumptions
|
||||||
like only searching for first two letters and doing the rest of the filtering
|
like only searching for first two letters and doing the rest of the filtering
|
||||||
|
|||||||
@ -9,6 +9,7 @@ User documentation
|
|||||||
installation/index
|
installation/index
|
||||||
changes
|
changes
|
||||||
authors
|
authors
|
||||||
|
licenses
|
||||||
|
|
||||||
Reference documentation
|
Reference documentation
|
||||||
=======================
|
=======================
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
**********************
|
|
||||||
Despotify installation
|
|
||||||
**********************
|
|
||||||
|
|
||||||
To use the `Despotify <http://despotify.se/>`_ backend, you first need to
|
|
||||||
install Despotify and spytify.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
This backend requires a Spotify premium account.
|
|
||||||
|
|
||||||
|
|
||||||
Installing Despotify on Linux
|
|
||||||
=============================
|
|
||||||
|
|
||||||
Install Despotify's dependencies. At Debian/Ubuntu systems::
|
|
||||||
|
|
||||||
sudo aptitude install libssl-dev zlib1g-dev libvorbis-dev \
|
|
||||||
libtool libncursesw5-dev libao-dev python-dev
|
|
||||||
|
|
||||||
Check out revision 508 of the Despotify source code::
|
|
||||||
|
|
||||||
svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508
|
|
||||||
|
|
||||||
Build and install Despotify::
|
|
||||||
|
|
||||||
cd despotify/src/
|
|
||||||
sudo make install
|
|
||||||
|
|
||||||
When Despotify has been installed, continue with :ref:`spytify_installation`.
|
|
||||||
|
|
||||||
|
|
||||||
Installing Despotify on OS X
|
|
||||||
============================
|
|
||||||
|
|
||||||
In OS X you need to have `XCode <http://developer.apple.com/tools/xcode/>`_ and
|
|
||||||
`Homebrew <http://mxcl.github.com/homebrew/>`_ installed. Then, to install
|
|
||||||
Despotify::
|
|
||||||
|
|
||||||
brew install despotify
|
|
||||||
|
|
||||||
When Despotify has been installed, continue with :ref:`spytify_installation`.
|
|
||||||
|
|
||||||
|
|
||||||
.. _spytify_installation:
|
|
||||||
|
|
||||||
Installing spytify
|
|
||||||
==================
|
|
||||||
|
|
||||||
spytify's source comes bundled with despotify. If you haven't already checkout
|
|
||||||
out the despotify source, do it now::
|
|
||||||
|
|
||||||
svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508
|
|
||||||
|
|
||||||
Build and install spytify::
|
|
||||||
|
|
||||||
cd despotify/src/bindings/python/
|
|
||||||
export PKG_CONFIG_PATH=../../lib # Needed on OS X
|
|
||||||
sudo make install
|
|
||||||
|
|
||||||
|
|
||||||
Testing the installation
|
|
||||||
========================
|
|
||||||
|
|
||||||
To validate that everything is working, run the ``test.py`` script which is
|
|
||||||
distributed with spytify::
|
|
||||||
|
|
||||||
python test.py
|
|
||||||
|
|
||||||
The test script should ask for your username and password (which must be for a
|
|
||||||
Spotify Premium account), ask for a search query, list all your playlists with
|
|
||||||
tracks, play 10s from a random song from the search result, pause for two
|
|
||||||
seconds, play for five more seconds, and quit.
|
|
||||||
@ -2,12 +2,10 @@
|
|||||||
Installation
|
Installation
|
||||||
************
|
************
|
||||||
|
|
||||||
Mopidy itself is a breeze to install, as it just requires a standard Python
|
To get a basic version of Mopidy running, you need Python and the GStreamer
|
||||||
installation and the GStreamer library. The libraries we depend on to connect
|
library. To use Spotify with Mopidy, you also need :doc:`libspotify and
|
||||||
to the Spotify service is far more tricky to get working for the time being.
|
pyspotify <libspotify>`. Mopidy itself can either be installed from the Python
|
||||||
Until installation of these libraries are either well documented by their
|
package index, PyPI, or from git.
|
||||||
developers, or the libraries are packaged for various Linux distributions, we
|
|
||||||
will supply our own installation guides, as linked to below.
|
|
||||||
|
|
||||||
|
|
||||||
Install dependencies
|
Install dependencies
|
||||||
@ -18,12 +16,11 @@ Install dependencies
|
|||||||
|
|
||||||
gstreamer
|
gstreamer
|
||||||
libspotify
|
libspotify
|
||||||
despotify
|
|
||||||
|
|
||||||
Make sure you got the required dependencies installed.
|
Make sure you got the required dependencies installed.
|
||||||
|
|
||||||
- Python >= 2.6, < 3
|
- Python >= 2.6, < 3
|
||||||
- :doc:`GStreamer <gstreamer>` (>= 0.10 ?) with Python bindings
|
- :doc:`GStreamer <gstreamer>` >= 0.10, with Python bindings
|
||||||
- Dependencies for at least one Mopidy mixer:
|
- Dependencies for at least one Mopidy mixer:
|
||||||
|
|
||||||
- :mod:`mopidy.mixers.alsa` (Linux only)
|
- :mod:`mopidy.mixers.alsa` (Linux only)
|
||||||
@ -44,10 +41,6 @@ Make sure you got the required dependencies installed.
|
|||||||
|
|
||||||
- Dependencies for at least one Mopidy backend:
|
- Dependencies for at least one Mopidy backend:
|
||||||
|
|
||||||
- :mod:`mopidy.backends.despotify` (Linux and OS X)
|
|
||||||
|
|
||||||
- :doc:`Despotify and spytify <despotify>`
|
|
||||||
|
|
||||||
- :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
|
- :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
|
||||||
|
|
||||||
- :doc:`libspotify and pyspotify <libspotify>`
|
- :doc:`libspotify and pyspotify <libspotify>`
|
||||||
@ -106,14 +99,9 @@ username and password into the file, like this::
|
|||||||
SPOTIFY_USERNAME = u'myusername'
|
SPOTIFY_USERNAME = u'myusername'
|
||||||
SPOTIFY_PASSWORD = u'mysecret'
|
SPOTIFY_PASSWORD = u'mysecret'
|
||||||
|
|
||||||
Currently :mod:`mopidy.backends.despotify` is the default
|
Currently :mod:`mopidy.backends.libspotify` is the default
|
||||||
backend.
|
backend. Before you can use :mod:`mopidy.backends.libspotify`, you must copy
|
||||||
|
the Spotify application key to ``~/.mopidy/spotify_appkey.key``.
|
||||||
If you want to use :mod:`mopidy.backends.libspotify`, copy the Spotify
|
|
||||||
application key to ``~/.mopidy/spotify_appkey.key``, and add the following
|
|
||||||
setting::
|
|
||||||
|
|
||||||
BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',)
|
|
||||||
|
|
||||||
If you want to use :mod:`mopidy.backends.local`, add the following setting::
|
If you want to use :mod:`mopidy.backends.local`, add the following setting::
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
libspotify installation
|
libspotify installation
|
||||||
***********************
|
***********************
|
||||||
|
|
||||||
As an alternative to the despotify backend, we are working on a
|
We are working on a
|
||||||
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_ backend.
|
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_ backend.
|
||||||
To use the libspotify backend you must install libspotify and
|
To use the libspotify backend you must install libspotify and
|
||||||
`pyspotify <http://github.com/winjer/pyspotify>`_.
|
`pyspotify <http://github.com/winjer/pyspotify>`_.
|
||||||
@ -57,22 +57,10 @@ Installing pyspotify
|
|||||||
|
|
||||||
Install pyspotify's dependencies. At Debian/Ubuntu systems::
|
Install pyspotify's dependencies. At Debian/Ubuntu systems::
|
||||||
|
|
||||||
sudo aptitude install python-dev python-alsaaudio
|
sudo aptitude install python-dev
|
||||||
|
|
||||||
Check out the pyspotify code, and install it::
|
Check out the pyspotify code, and install it::
|
||||||
|
|
||||||
git clone git://github.com/jodal/pyspotify.git
|
git clone git://github.com/jodal/pyspotify.git
|
||||||
cd pyspotify/pyspotify/
|
cd pyspotify/pyspotify/
|
||||||
sudo python setup.py install
|
sudo python setup.py install
|
||||||
|
|
||||||
|
|
||||||
Testing the installation
|
|
||||||
========================
|
|
||||||
|
|
||||||
Apply for an application key at
|
|
||||||
https://developer.spotify.com/en/libspotify/application-key, download the
|
|
||||||
binary version, and place the file at ``pyspotify/spotify_appkey.key``.
|
|
||||||
|
|
||||||
Test your libspotify setup::
|
|
||||||
|
|
||||||
examples/example1.py -u USERNAME -p PASSWORD
|
|
||||||
|
|||||||
34
docs/licenses.rst
Normal file
34
docs/licenses.rst
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
********
|
||||||
|
Licenses
|
||||||
|
********
|
||||||
|
|
||||||
|
For a list of contributors, see :ref:`authors`. For details on who have
|
||||||
|
contributed what, please refer to our git repository.
|
||||||
|
|
||||||
|
Source code license
|
||||||
|
===================
|
||||||
|
|
||||||
|
Copyright 2009-2010 Stein Magnus Jodal and contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
Documentation license
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Copyright 2010 Stein Magnus Jodal and contributors
|
||||||
|
|
||||||
|
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||||
|
Unported License. To view a copy of this license, visit
|
||||||
|
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
|
||||||
|
Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
|
||||||
@ -22,7 +22,10 @@ def main():
|
|||||||
get_or_create_folder('~/.mopidy/')
|
get_or_create_folder('~/.mopidy/')
|
||||||
core_queue = multiprocessing.Queue()
|
core_queue = multiprocessing.Queue()
|
||||||
get_class(settings.SERVER)(core_queue).start()
|
get_class(settings.SERVER)(core_queue).start()
|
||||||
core = CoreProcess(core_queue)
|
output_class = get_class(settings.OUTPUT)
|
||||||
|
backend_class = get_class(settings.BACKENDS[0])
|
||||||
|
frontend_class = get_class(settings.FRONTEND)
|
||||||
|
core = CoreProcess(core_queue, output_class, backend_class, frontend_class)
|
||||||
core.start()
|
core.start()
|
||||||
asyncore.loop()
|
asyncore.loop()
|
||||||
|
|
||||||
|
|||||||
@ -1,209 +0,0 @@
|
|||||||
import datetime as dt
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import spytify
|
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
|
||||||
BaseLibraryController, BasePlaybackController,
|
|
||||||
BaseStoredPlaylistsController)
|
|
||||||
from mopidy.models import Artist, Album, Track, Playlist
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.despotify')
|
|
||||||
|
|
||||||
ENCODING = 'utf-8'
|
|
||||||
|
|
||||||
class DespotifyBackend(BaseBackend):
|
|
||||||
"""
|
|
||||||
A Spotify backend which uses the open source `despotify library
|
|
||||||
<http://despotify.se/>`_.
|
|
||||||
|
|
||||||
`spytify <http://despotify.svn.sourceforge.net/viewvc/despotify/src/bindings/python/>`_
|
|
||||||
is the Python bindings for the despotify library. It got litle
|
|
||||||
documentation, but a couple of examples are available.
|
|
||||||
|
|
||||||
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-despotify
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DespotifyBackend, self).__init__(*args, **kwargs)
|
|
||||||
self.current_playlist = DespotifyCurrentPlaylistController(backend=self)
|
|
||||||
self.library = DespotifyLibraryController(backend=self)
|
|
||||||
self.playback = DespotifyPlaybackController(backend=self)
|
|
||||||
self.stored_playlists = DespotifyStoredPlaylistsController(backend=self)
|
|
||||||
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
|
|
||||||
self.spotify = self._connect()
|
|
||||||
self.stored_playlists.refresh()
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
logger.info(u'Connecting to Spotify')
|
|
||||||
try:
|
|
||||||
return DespotifySessionManager(
|
|
||||||
settings.SPOTIFY_USERNAME.encode(ENCODING),
|
|
||||||
settings.SPOTIFY_PASSWORD.encode(ENCODING),
|
|
||||||
core_queue=self.core_queue)
|
|
||||||
except spytify.SpytifyError as e:
|
|
||||||
logger.exception(e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyCurrentPlaylistController(BaseCurrentPlaylistController):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyLibraryController(BaseLibraryController):
|
|
||||||
def find_exact(self, **query):
|
|
||||||
return self.search(**query)
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
track = self.backend.spotify.lookup(uri.encode(ENCODING))
|
|
||||||
return DespotifyTranslator.to_mopidy_track(track)
|
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def search(self, **query):
|
|
||||||
spotify_query = []
|
|
||||||
for (field, values) in query.iteritems():
|
|
||||||
if not hasattr(values, '__iter__'):
|
|
||||||
values = [values]
|
|
||||||
for value in values:
|
|
||||||
if field == u'track':
|
|
||||||
field = u'title'
|
|
||||||
if field == u'any':
|
|
||||||
spotify_query.append(value)
|
|
||||||
else:
|
|
||||||
spotify_query.append(u'%s:"%s"' % (field, value))
|
|
||||||
spotify_query = u' '.join(spotify_query)
|
|
||||||
logger.debug(u'Spotify search query: %s', spotify_query)
|
|
||||||
result = self.backend.spotify.search(spotify_query.encode(ENCODING))
|
|
||||||
if (result is None or result.playlist.tracks[0].get_uri() ==
|
|
||||||
'spotify:track:0000000000000000000000'):
|
|
||||||
return Playlist()
|
|
||||||
return DespotifyTranslator.to_mopidy_playlist(result.playlist)
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyPlaybackController(BasePlaybackController):
|
|
||||||
def _pause(self):
|
|
||||||
try:
|
|
||||||
self.backend.spotify.pause()
|
|
||||||
return True
|
|
||||||
except spytify.SpytifyError as e:
|
|
||||||
logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _play(self, track):
|
|
||||||
try:
|
|
||||||
self.backend.spotify.play(self.backend.spotify.lookup(track.uri))
|
|
||||||
return True
|
|
||||||
except spytify.SpytifyError as e:
|
|
||||||
logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _resume(self):
|
|
||||||
try:
|
|
||||||
self.backend.spotify.resume()
|
|
||||||
return True
|
|
||||||
except spytify.SpytifyError as e:
|
|
||||||
logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _seek(self, time_position):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def _stop(self):
|
|
||||||
try:
|
|
||||||
self.backend.spotify.stop()
|
|
||||||
return True
|
|
||||||
except spytify.SpytifyError as e:
|
|
||||||
logger.error(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
|
||||||
def create(self, name):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def delete(self, playlist):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
logger.info(u'Caching stored playlists')
|
|
||||||
playlists = []
|
|
||||||
for spotify_playlist in self.backend.spotify.stored_playlists:
|
|
||||||
playlists.append(
|
|
||||||
DespotifyTranslator.to_mopidy_playlist(spotify_playlist))
|
|
||||||
self._playlists = playlists
|
|
||||||
logger.debug(u'Available playlists: %s',
|
|
||||||
u', '.join([u'<%s>' % p.name for p in self.playlists]))
|
|
||||||
logger.info(u'Done caching stored playlists')
|
|
||||||
|
|
||||||
def rename(self, playlist, new_name):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def save(self, playlist):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyTranslator(object):
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_artist(cls, spotify_artist):
|
|
||||||
return Artist(
|
|
||||||
uri=spotify_artist.get_uri(),
|
|
||||||
name=spotify_artist.name.decode(ENCODING)
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_album(cls, spotify_album_name):
|
|
||||||
return Album(name=spotify_album_name.decode(ENCODING))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_track(cls, spotify_track):
|
|
||||||
if spotify_track is None or not spotify_track.has_meta_data():
|
|
||||||
return None
|
|
||||||
if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR:
|
|
||||||
date = dt.date(spotify_track.year, 1, 1)
|
|
||||||
else:
|
|
||||||
date = None
|
|
||||||
return Track(
|
|
||||||
uri=spotify_track.get_uri(),
|
|
||||||
name=spotify_track.title.decode(ENCODING),
|
|
||||||
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists],
|
|
||||||
album=cls.to_mopidy_album(spotify_track.album),
|
|
||||||
track_no=spotify_track.tracknumber,
|
|
||||||
date=date,
|
|
||||||
length=spotify_track.length,
|
|
||||||
bitrate=320,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_playlist(cls, spotify_playlist):
|
|
||||||
return Playlist(
|
|
||||||
uri=spotify_playlist.get_uri(),
|
|
||||||
name=spotify_playlist.name.decode(ENCODING),
|
|
||||||
tracks=filter(None,
|
|
||||||
[cls.to_mopidy_track(t) for t in spotify_playlist.tracks]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifySessionManager(spytify.Spytify):
|
|
||||||
DESPOTIFY_NEW_TRACK = 1
|
|
||||||
DESPOTIFY_TIME_TELL = 2
|
|
||||||
DESPOTIFY_END_OF_PLAYLIST = 3
|
|
||||||
DESPOTIFY_TRACK_PLAY_ERROR = 4
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs['callback'] = self.callback
|
|
||||||
self.core_queue = kwargs.pop('core_queue')
|
|
||||||
super(DespotifySessionManager, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def callback(self, signal, data):
|
|
||||||
if signal == self.DESPOTIFY_END_OF_PLAYLIST:
|
|
||||||
logger.debug('Despotify signalled end of playlist')
|
|
||||||
self.core_queue.put({'command': 'end_of_track'})
|
|
||||||
elif signal == self.DESPOTIFY_TRACK_PLAY_ERROR:
|
|
||||||
logger.error('Despotify signalled track play error')
|
|
||||||
@ -1,19 +1,7 @@
|
|||||||
import datetime as dt
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import multiprocessing
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from spotify import Link, SpotifyError
|
from mopidy import settings
|
||||||
from spotify.manager import SpotifySessionManager
|
from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController
|
||||||
from spotify.alsahelper import AlsaController
|
|
||||||
|
|
||||||
from mopidy import get_version, settings
|
|
||||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
|
||||||
BaseLibraryController, BasePlaybackController,
|
|
||||||
BaseStoredPlaylistsController)
|
|
||||||
from mopidy.models import Artist, Album, Track, Playlist
|
|
||||||
from mopidy.process import pickle_connection
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.libspotify')
|
logger = logging.getLogger('mopidy.backends.libspotify')
|
||||||
|
|
||||||
@ -28,15 +16,19 @@ class LibspotifyBackend(BaseBackend):
|
|||||||
for libspotify. It got no documentation, but multiple examples are
|
for libspotify. It got no documentation, but multiple examples are
|
||||||
available. Like libspotify, pyspotify's calls are mostly asynchronous.
|
available. Like libspotify, pyspotify's calls are mostly asynchronous.
|
||||||
|
|
||||||
This backend should also work with `openspotify
|
|
||||||
<http://github.com/noahwilliamsson/openspotify>`_, but we haven't tested
|
|
||||||
that yet.
|
|
||||||
|
|
||||||
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
|
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Imports inside methods are to prevent loading of __init__.py to fail on
|
||||||
|
# missing spotify dependencies.
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
from .library import LibspotifyLibraryController
|
||||||
|
from .playback import LibspotifyPlaybackController
|
||||||
|
from .stored_playlists import LibspotifyStoredPlaylistsController
|
||||||
|
|
||||||
super(LibspotifyBackend, self).__init__(*args, **kwargs)
|
super(LibspotifyBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.current_playlist = BaseCurrentPlaylistController(backend=self)
|
self.current_playlist = BaseCurrentPlaylistController(backend=self)
|
||||||
self.library = LibspotifyLibraryController(backend=self)
|
self.library = LibspotifyLibraryController(backend=self)
|
||||||
self.playback = LibspotifyPlaybackController(backend=self)
|
self.playback = LibspotifyPlaybackController(backend=self)
|
||||||
@ -46,6 +38,8 @@ class LibspotifyBackend(BaseBackend):
|
|||||||
self.spotify = self._connect()
|
self.spotify = self._connect()
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
|
from .session_manager import LibspotifySessionManager
|
||||||
|
|
||||||
logger.info(u'Connecting to Spotify')
|
logger.info(u'Connecting to Spotify')
|
||||||
spotify = LibspotifySessionManager(
|
spotify = LibspotifySessionManager(
|
||||||
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
|
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
|
||||||
@ -53,259 +47,3 @@ class LibspotifyBackend(BaseBackend):
|
|||||||
output_queue=self.output_queue)
|
output_queue=self.output_queue)
|
||||||
spotify.start()
|
spotify.start()
|
||||||
return spotify
|
return spotify
|
||||||
|
|
||||||
|
|
||||||
class LibspotifyLibraryController(BaseLibraryController):
|
|
||||||
def find_exact(self, **query):
|
|
||||||
return self.search(**query)
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
spotify_track = Link.from_string(uri).as_track()
|
|
||||||
return LibspotifyTranslator.to_mopidy_track(spotify_track)
|
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def search(self, **query):
|
|
||||||
spotify_query = []
|
|
||||||
for (field, values) in query.iteritems():
|
|
||||||
if not hasattr(values, '__iter__'):
|
|
||||||
values = [values]
|
|
||||||
for value in values:
|
|
||||||
if field == u'track':
|
|
||||||
field = u'title'
|
|
||||||
if field == u'any':
|
|
||||||
spotify_query.append(value)
|
|
||||||
else:
|
|
||||||
spotify_query.append(u'%s:"%s"' % (field, value))
|
|
||||||
spotify_query = u' '.join(spotify_query)
|
|
||||||
logger.debug(u'In search method, search for: %s' % spotify_query)
|
|
||||||
my_end, other_end = multiprocessing.Pipe()
|
|
||||||
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
|
|
||||||
logger.debug(u'In Library.search(), waiting for search results')
|
|
||||||
my_end.poll(None)
|
|
||||||
logger.debug(u'In Library.search(), receiving search results')
|
|
||||||
playlist = my_end.recv()
|
|
||||||
logger.debug(u'In Library.search(), done receiving search results')
|
|
||||||
logger.debug(['%s' % t.name for t in playlist.tracks])
|
|
||||||
return playlist
|
|
||||||
|
|
||||||
|
|
||||||
class LibspotifyPlaybackController(BasePlaybackController):
|
|
||||||
def _set_output_state(self, state_name):
|
|
||||||
logger.debug(u'Setting output state to %s ...', state_name)
|
|
||||||
(my_end, other_end) = multiprocessing.Pipe()
|
|
||||||
self.backend.output_queue.put({
|
|
||||||
'command': 'set_state',
|
|
||||||
'state': state_name,
|
|
||||||
'reply_to': pickle_connection(other_end),
|
|
||||||
})
|
|
||||||
my_end.poll(None)
|
|
||||||
return my_end.recv()
|
|
||||||
|
|
||||||
def _pause(self):
|
|
||||||
return self._set_output_state('PAUSED')
|
|
||||||
|
|
||||||
def _play(self, track):
|
|
||||||
self._set_output_state('READY')
|
|
||||||
if self.state == self.PLAYING:
|
|
||||||
self.stop()
|
|
||||||
if track.uri is None:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
self.backend.spotify.session.load(
|
|
||||||
Link.from_string(track.uri).as_track())
|
|
||||||
self.backend.spotify.session.play(1)
|
|
||||||
self._set_output_state('PLAYING')
|
|
||||||
return True
|
|
||||||
except SpotifyError as e:
|
|
||||||
logger.warning('Play %s failed: %s', track.uri, e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _resume(self):
|
|
||||||
return self._set_output_state('PLAYING')
|
|
||||||
|
|
||||||
def _seek(self, time_position):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def _stop(self):
|
|
||||||
result = self._set_output_state('READY')
|
|
||||||
self.backend.spotify.session.play(0)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
|
||||||
def create(self, name):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def delete(self, playlist):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def rename(self, playlist, new_name):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
def save(self, playlist):
|
|
||||||
pass # TODO
|
|
||||||
|
|
||||||
|
|
||||||
class LibspotifyTranslator(object):
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_artist(cls, spotify_artist):
|
|
||||||
if not spotify_artist.is_loaded():
|
|
||||||
return Artist(name=u'[loading...]')
|
|
||||||
return Artist(
|
|
||||||
uri=str(Link.from_artist(spotify_artist)),
|
|
||||||
name=spotify_artist.name().decode(ENCODING),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_album(cls, spotify_album):
|
|
||||||
if not spotify_album.is_loaded():
|
|
||||||
return Album(name=u'[loading...]')
|
|
||||||
# TODO pyspotify got much more data on albums than this
|
|
||||||
return Album(name=spotify_album.name().decode(ENCODING))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_track(cls, spotify_track):
|
|
||||||
if not spotify_track.is_loaded():
|
|
||||||
return Track(name=u'[loading...]')
|
|
||||||
uri = str(Link.from_track(spotify_track, 0))
|
|
||||||
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
|
|
||||||
date = dt.date(spotify_track.album().year(), 1, 1)
|
|
||||||
else:
|
|
||||||
date = None
|
|
||||||
return Track(
|
|
||||||
uri=uri,
|
|
||||||
name=spotify_track.name().decode(ENCODING),
|
|
||||||
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
|
|
||||||
album=cls.to_mopidy_album(spotify_track.album()),
|
|
||||||
track_no=spotify_track.index(),
|
|
||||||
date=date,
|
|
||||||
length=spotify_track.duration(),
|
|
||||||
bitrate=320,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_mopidy_playlist(cls, spotify_playlist):
|
|
||||||
if not spotify_playlist.is_loaded():
|
|
||||||
return Playlist(name=u'[loading...]')
|
|
||||||
return Playlist(
|
|
||||||
uri=str(Link.from_playlist(spotify_playlist)),
|
|
||||||
name=spotify_playlist.name().decode(ENCODING),
|
|
||||||
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
|
|
||||||
)
|
|
||||||
|
|
||||||
class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
|
||||||
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
|
||||||
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
|
||||||
appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY)
|
|
||||||
user_agent = 'Mopidy %s' % get_version()
|
|
||||||
|
|
||||||
def __init__(self, username, password, core_queue, output_queue):
|
|
||||||
SpotifySessionManager.__init__(self, username, password)
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.core_queue = core_queue
|
|
||||||
self.output_queue = output_queue
|
|
||||||
self.connected = threading.Event()
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
def logged_in(self, session, error):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.info('Logged in')
|
|
||||||
self.session = session
|
|
||||||
self.connected.set()
|
|
||||||
|
|
||||||
def logged_out(self, session):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.info('Logged out')
|
|
||||||
|
|
||||||
def metadata_updated(self, session):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.debug('Metadata updated, refreshing stored playlists')
|
|
||||||
playlists = []
|
|
||||||
for spotify_playlist in session.playlist_container():
|
|
||||||
playlists.append(
|
|
||||||
LibspotifyTranslator.to_mopidy_playlist(spotify_playlist))
|
|
||||||
self.core_queue.put({
|
|
||||||
'command': 'set_stored_playlists',
|
|
||||||
'playlists': playlists,
|
|
||||||
})
|
|
||||||
|
|
||||||
def connection_error(self, session, error):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.error('Connection error: %s', error)
|
|
||||||
|
|
||||||
def message_to_user(self, session, message):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.info(message)
|
|
||||||
|
|
||||||
def notify_main_thread(self, session):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.debug('Notify main thread')
|
|
||||||
|
|
||||||
def music_delivery(self, session, frames, frame_size, num_frames,
|
|
||||||
sample_type, sample_rate, channels):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
# TODO Base caps_string on arguments
|
|
||||||
caps_string = """
|
|
||||||
audio/x-raw-int,
|
|
||||||
endianness=(int)1234,
|
|
||||||
channels=(int)2,
|
|
||||||
width=(int)16,
|
|
||||||
depth=(int)16,
|
|
||||||
signed=True,
|
|
||||||
rate=(int)44100
|
|
||||||
"""
|
|
||||||
self.output_queue.put({
|
|
||||||
'command': 'deliver_data',
|
|
||||||
'caps': caps_string,
|
|
||||||
'data': bytes(frames),
|
|
||||||
})
|
|
||||||
|
|
||||||
def play_token_lost(self, session):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.debug('Play token lost')
|
|
||||||
self.core_queue.put({'command': 'stop_playback'})
|
|
||||||
|
|
||||||
def log_message(self, session, data):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.debug(data)
|
|
||||||
|
|
||||||
def end_of_track(self, session):
|
|
||||||
"""Callback used by pyspotify"""
|
|
||||||
logger.debug('End of data stream.')
|
|
||||||
self.output_queue.put({'command': 'end_of_data_stream'})
|
|
||||||
|
|
||||||
def search(self, query, connection):
|
|
||||||
"""Search method used by Mopidy backend"""
|
|
||||||
def callback(results, userdata):
|
|
||||||
logger.debug(u'In SessionManager.search().callback(), '
|
|
||||||
'translating search results')
|
|
||||||
logger.debug(results.tracks())
|
|
||||||
# TODO Include results from results.albums(), etc. too
|
|
||||||
playlist = Playlist(tracks=[
|
|
||||||
LibspotifyTranslator.to_mopidy_track(t)
|
|
||||||
for t in results.tracks()])
|
|
||||||
logger.debug(u'In SessionManager.search().callback(), '
|
|
||||||
'sending search results')
|
|
||||||
logger.debug(['%s' % t.name for t in playlist.tracks])
|
|
||||||
connection.send(playlist)
|
|
||||||
logger.debug(u'In SessionManager.search().callback(), '
|
|
||||||
'done sending search results')
|
|
||||||
logger.debug(u'In SessionManager.search(), '
|
|
||||||
'waiting for Spotify connection')
|
|
||||||
self.connected.wait()
|
|
||||||
logger.debug(u'In SessionManager.search(), '
|
|
||||||
'sending search query')
|
|
||||||
self.session.search(query, callback)
|
|
||||||
logger.debug(u'In SessionManager.search(), '
|
|
||||||
'done sending search query')
|
|
||||||
|
|||||||
41
mopidy/backends/libspotify/library.py
Normal file
41
mopidy/backends/libspotify/library.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from spotify import Link
|
||||||
|
|
||||||
|
from mopidy.backends.base import BaseLibraryController
|
||||||
|
from mopidy.backends.libspotify import ENCODING
|
||||||
|
from mopidy.backends.libspotify.translator import LibspotifyTranslator
|
||||||
|
|
||||||
|
logger = logging.getLogger('mopidy.backends.libspotify.library')
|
||||||
|
|
||||||
|
class LibspotifyLibraryController(BaseLibraryController):
|
||||||
|
def find_exact(self, **query):
|
||||||
|
return self.search(**query)
|
||||||
|
|
||||||
|
def lookup(self, uri):
|
||||||
|
spotify_track = Link.from_string(uri).as_track()
|
||||||
|
return LibspotifyTranslator.to_mopidy_track(spotify_track)
|
||||||
|
|
||||||
|
def refresh(self, uri=None):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def search(self, **query):
|
||||||
|
spotify_query = []
|
||||||
|
for (field, values) in query.iteritems():
|
||||||
|
if not hasattr(values, '__iter__'):
|
||||||
|
values = [values]
|
||||||
|
for value in values:
|
||||||
|
if field == u'track':
|
||||||
|
field = u'title'
|
||||||
|
if field == u'any':
|
||||||
|
spotify_query.append(value)
|
||||||
|
else:
|
||||||
|
spotify_query.append(u'%s:"%s"' % (field, value))
|
||||||
|
spotify_query = u' '.join(spotify_query)
|
||||||
|
logger.debug(u'Spotify search query: %s' % spotify_query)
|
||||||
|
my_end, other_end = multiprocessing.Pipe()
|
||||||
|
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
|
||||||
|
my_end.poll(None)
|
||||||
|
playlist = my_end.recv()
|
||||||
|
return playlist
|
||||||
51
mopidy/backends/libspotify/playback.py
Normal file
51
mopidy/backends/libspotify/playback.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from spotify import Link, SpotifyError
|
||||||
|
|
||||||
|
from mopidy.backends.base import BasePlaybackController
|
||||||
|
from mopidy.process import pickle_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger('mopidy.backends.libspotify.playback')
|
||||||
|
|
||||||
|
class LibspotifyPlaybackController(BasePlaybackController):
|
||||||
|
def _set_output_state(self, state_name):
|
||||||
|
logger.debug(u'Setting output state to %s ...', state_name)
|
||||||
|
(my_end, other_end) = multiprocessing.Pipe()
|
||||||
|
self.backend.output_queue.put({
|
||||||
|
'command': 'set_state',
|
||||||
|
'state': state_name,
|
||||||
|
'reply_to': pickle_connection(other_end),
|
||||||
|
})
|
||||||
|
my_end.poll(None)
|
||||||
|
return my_end.recv()
|
||||||
|
|
||||||
|
def _pause(self):
|
||||||
|
return self._set_output_state('PAUSED')
|
||||||
|
|
||||||
|
def _play(self, track):
|
||||||
|
self._set_output_state('READY')
|
||||||
|
if self.state == self.PLAYING:
|
||||||
|
self.stop()
|
||||||
|
if track.uri is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.backend.spotify.session.load(
|
||||||
|
Link.from_string(track.uri).as_track())
|
||||||
|
self.backend.spotify.session.play(1)
|
||||||
|
self._set_output_state('PLAYING')
|
||||||
|
return True
|
||||||
|
except SpotifyError as e:
|
||||||
|
logger.warning('Play %s failed: %s', track.uri, e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _resume(self):
|
||||||
|
return self._set_output_state('PLAYING')
|
||||||
|
|
||||||
|
def _seek(self, time_position):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def _stop(self):
|
||||||
|
result = self._set_output_state('READY')
|
||||||
|
self.backend.spotify.session.play(0)
|
||||||
|
return result
|
||||||
106
mopidy/backends/libspotify/session_manager.py
Normal file
106
mopidy/backends/libspotify/session_manager.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from spotify.manager import SpotifySessionManager
|
||||||
|
|
||||||
|
from mopidy import get_version, settings
|
||||||
|
from mopidy.models import Playlist
|
||||||
|
from mopidy.backends.libspotify.translator import LibspotifyTranslator
|
||||||
|
|
||||||
|
logger = logging.getLogger('mopidy.backends.libspotify.session_manager')
|
||||||
|
|
||||||
|
class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
||||||
|
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
||||||
|
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
||||||
|
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
||||||
|
user_agent = 'Mopidy %s' % get_version()
|
||||||
|
|
||||||
|
def __init__(self, username, password, core_queue, output_queue):
|
||||||
|
SpotifySessionManager.__init__(self, username, password)
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.core_queue = core_queue
|
||||||
|
self.output_queue = output_queue
|
||||||
|
self.connected = threading.Event()
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def logged_in(self, session, error):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.info('Logged in')
|
||||||
|
self.session = session
|
||||||
|
self.connected.set()
|
||||||
|
|
||||||
|
def logged_out(self, session):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.info('Logged out')
|
||||||
|
|
||||||
|
def metadata_updated(self, session):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.debug('Metadata updated, refreshing stored playlists')
|
||||||
|
playlists = []
|
||||||
|
for spotify_playlist in session.playlist_container():
|
||||||
|
playlists.append(
|
||||||
|
LibspotifyTranslator.to_mopidy_playlist(spotify_playlist))
|
||||||
|
self.core_queue.put({
|
||||||
|
'command': 'set_stored_playlists',
|
||||||
|
'playlists': playlists,
|
||||||
|
})
|
||||||
|
|
||||||
|
def connection_error(self, session, error):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.error('Connection error: %s', error)
|
||||||
|
|
||||||
|
def message_to_user(self, session, message):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.info(message.strip())
|
||||||
|
|
||||||
|
def notify_main_thread(self, session):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.debug('Notify main thread')
|
||||||
|
|
||||||
|
def music_delivery(self, session, frames, frame_size, num_frames,
|
||||||
|
sample_type, sample_rate, channels):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
# TODO Base caps_string on arguments
|
||||||
|
caps_string = """
|
||||||
|
audio/x-raw-int,
|
||||||
|
endianness=(int)1234,
|
||||||
|
channels=(int)2,
|
||||||
|
width=(int)16,
|
||||||
|
depth=(int)16,
|
||||||
|
signed=True,
|
||||||
|
rate=(int)44100
|
||||||
|
"""
|
||||||
|
self.output_queue.put({
|
||||||
|
'command': 'deliver_data',
|
||||||
|
'caps': caps_string,
|
||||||
|
'data': bytes(frames),
|
||||||
|
})
|
||||||
|
|
||||||
|
def play_token_lost(self, session):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.debug('Play token lost')
|
||||||
|
self.core_queue.put({'command': 'stop_playback'})
|
||||||
|
|
||||||
|
def log_message(self, session, data):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.debug(data.strip())
|
||||||
|
|
||||||
|
def end_of_track(self, session):
|
||||||
|
"""Callback used by pyspotify"""
|
||||||
|
logger.debug('End of data stream.')
|
||||||
|
self.output_queue.put({'command': 'end_of_data_stream'})
|
||||||
|
|
||||||
|
def search(self, query, connection):
|
||||||
|
"""Search method used by Mopidy backend"""
|
||||||
|
def callback(results, userdata):
|
||||||
|
# TODO Include results from results.albums(), etc. too
|
||||||
|
playlist = Playlist(tracks=[
|
||||||
|
LibspotifyTranslator.to_mopidy_track(t)
|
||||||
|
for t in results.tracks()])
|
||||||
|
connection.send(playlist)
|
||||||
|
self.connected.wait()
|
||||||
|
self.session.search(query, callback)
|
||||||
BIN
mopidy/backends/libspotify/spotify_appkey.key
Normal file
BIN
mopidy/backends/libspotify/spotify_appkey.key
Normal file
Binary file not shown.
20
mopidy/backends/libspotify/stored_playlists.py
Normal file
20
mopidy/backends/libspotify/stored_playlists.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from mopidy.backends.base import BaseStoredPlaylistsController
|
||||||
|
|
||||||
|
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||||
|
def create(self, name):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def delete(self, playlist):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def lookup(self, uri):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def rename(self, playlist, new_name):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def save(self, playlist):
|
||||||
|
pass # TODO
|
||||||
53
mopidy/backends/libspotify/translator.py
Normal file
53
mopidy/backends/libspotify/translator.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from spotify import Link
|
||||||
|
|
||||||
|
from mopidy.models import Artist, Album, Track, Playlist
|
||||||
|
from mopidy.backends.libspotify import ENCODING
|
||||||
|
|
||||||
|
class LibspotifyTranslator(object):
|
||||||
|
@classmethod
|
||||||
|
def to_mopidy_artist(cls, spotify_artist):
|
||||||
|
if not spotify_artist.is_loaded():
|
||||||
|
return Artist(name=u'[loading...]')
|
||||||
|
return Artist(
|
||||||
|
uri=str(Link.from_artist(spotify_artist)),
|
||||||
|
name=spotify_artist.name().decode(ENCODING),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_mopidy_album(cls, spotify_album):
|
||||||
|
if not spotify_album.is_loaded():
|
||||||
|
return Album(name=u'[loading...]')
|
||||||
|
# TODO pyspotify got much more data on albums than this
|
||||||
|
return Album(name=spotify_album.name().decode(ENCODING))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_mopidy_track(cls, spotify_track):
|
||||||
|
if not spotify_track.is_loaded():
|
||||||
|
return Track(name=u'[loading...]')
|
||||||
|
uri = str(Link.from_track(spotify_track, 0))
|
||||||
|
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
|
||||||
|
date = dt.date(spotify_track.album().year(), 1, 1)
|
||||||
|
else:
|
||||||
|
date = None
|
||||||
|
return Track(
|
||||||
|
uri=uri,
|
||||||
|
name=spotify_track.name().decode(ENCODING),
|
||||||
|
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
|
||||||
|
album=cls.to_mopidy_album(spotify_track.album()),
|
||||||
|
track_no=spotify_track.index(),
|
||||||
|
date=date,
|
||||||
|
length=spotify_track.duration(),
|
||||||
|
bitrate=320,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_mopidy_playlist(cls, spotify_playlist):
|
||||||
|
if not spotify_playlist.is_loaded():
|
||||||
|
return Playlist(name=u'[loading...]')
|
||||||
|
return Playlist(
|
||||||
|
uri=str(Link.from_playlist(spotify_playlist)),
|
||||||
|
name=spotify_playlist.name().decode(ENCODING),
|
||||||
|
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
|
||||||
|
)
|
||||||
@ -83,6 +83,8 @@ def deleteid(frontend, cpid):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cpid = int(cpid)
|
cpid = int(cpid)
|
||||||
|
if frontend.backend.playback.current_cpid == cpid:
|
||||||
|
frontend.backend.playback.next()
|
||||||
return frontend.backend.current_playlist.remove(cpid=cpid)
|
return frontend.backend.current_playlist.remove(cpid=cpid)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||||
@ -257,7 +259,8 @@ def playlistsearch(frontend, tag, needle):
|
|||||||
"""
|
"""
|
||||||
raise MpdNotImplemented # TODO
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
@handle_pattern(r'^plchanges "(?P<version>\d+)"$')
|
@handle_pattern(r'^plchanges (?P<version>-?\d+)$')
|
||||||
|
@handle_pattern(r'^plchanges "(?P<version>-?\d+)"$')
|
||||||
def plchanges(frontend, version):
|
def plchanges(frontend, version):
|
||||||
"""
|
"""
|
||||||
*musicpd.org, current playlist section:*
|
*musicpd.org, current playlist section:*
|
||||||
@ -268,6 +271,10 @@ def plchanges(frontend, version):
|
|||||||
|
|
||||||
To detect songs that were deleted at the end of the playlist, use
|
To detect songs that were deleted at the end of the playlist, use
|
||||||
``playlistlength`` returned by status command.
|
``playlistlength`` returned by status command.
|
||||||
|
|
||||||
|
*MPDroid:*
|
||||||
|
|
||||||
|
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
|
||||||
"""
|
"""
|
||||||
# XXX Naive implementation that returns all tracks as changed
|
# XXX Naive implementation that returns all tracks as changed
|
||||||
if int(version) < frontend.backend.current_playlist.version:
|
if int(version) < frontend.backend.current_playlist.version:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||||
MpdNotImplemented)
|
MpdNotImplemented)
|
||||||
|
|
||||||
|
@handle_pattern(r'^consume (?P<state>[01])$')
|
||||||
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
||||||
def consume(frontend, state):
|
def consume(frontend, state):
|
||||||
"""
|
"""
|
||||||
@ -86,16 +87,28 @@ def next_(frontend):
|
|||||||
"""
|
"""
|
||||||
return frontend.backend.playback.next()
|
return frontend.backend.playback.next()
|
||||||
|
|
||||||
|
@handle_pattern(r'^pause$')
|
||||||
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||||
def pause(frontend, state):
|
def pause(frontend, state=None):
|
||||||
"""
|
"""
|
||||||
*musicpd.org, playback section:*
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
``pause {PAUSE}``
|
``pause {PAUSE}``
|
||||||
|
|
||||||
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
|
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
|
||||||
|
|
||||||
|
*MPDroid:*
|
||||||
|
|
||||||
|
- Calls ``pause`` without any arguments to toogle pause.
|
||||||
"""
|
"""
|
||||||
if int(state):
|
if state is None:
|
||||||
|
if (frontend.backend.playback.state ==
|
||||||
|
frontend.backend.playback.PLAYING):
|
||||||
|
frontend.backend.playback.pause()
|
||||||
|
elif (frontend.backend.playback.state ==
|
||||||
|
frontend.backend.playback.PAUSED):
|
||||||
|
frontend.backend.playback.resume()
|
||||||
|
elif int(state):
|
||||||
frontend.backend.playback.pause()
|
frontend.backend.playback.pause()
|
||||||
else:
|
else:
|
||||||
frontend.backend.playback.resume()
|
frontend.backend.playback.resume()
|
||||||
@ -135,8 +148,8 @@ def playid(frontend, cpid):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||||
|
|
||||||
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
|
@handle_pattern(r'^play (?P<songpos>-?\d+)$')
|
||||||
@handle_pattern(r'^play "(?P<songpos>-1)"$')
|
@handle_pattern(r'^play "(?P<songpos>-?\d+)"$')
|
||||||
def playpos(frontend, songpos):
|
def playpos(frontend, songpos):
|
||||||
"""
|
"""
|
||||||
*musicpd.org, playback section:*
|
*musicpd.org, playback section:*
|
||||||
@ -149,6 +162,10 @@ def playpos(frontend, songpos):
|
|||||||
|
|
||||||
- issues ``play "-1"`` after playlist replacement to start playback at
|
- issues ``play "-1"`` after playlist replacement to start playback at
|
||||||
the first track.
|
the first track.
|
||||||
|
|
||||||
|
*BitMPC:*
|
||||||
|
|
||||||
|
- issues ``play 6`` without quotes around the argument.
|
||||||
"""
|
"""
|
||||||
songpos = int(songpos)
|
songpos = int(songpos)
|
||||||
try:
|
try:
|
||||||
@ -208,6 +225,7 @@ def previous(frontend):
|
|||||||
"""
|
"""
|
||||||
return frontend.backend.playback.previous()
|
return frontend.backend.playback.previous()
|
||||||
|
|
||||||
|
@handle_pattern(r'^random (?P<state>[01])$')
|
||||||
@handle_pattern(r'^random "(?P<state>[01])"$')
|
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||||
def random(frontend, state):
|
def random(frontend, state):
|
||||||
"""
|
"""
|
||||||
@ -222,6 +240,7 @@ def random(frontend, state):
|
|||||||
else:
|
else:
|
||||||
frontend.backend.playback.random = False
|
frontend.backend.playback.random = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^repeat (?P<state>[01])$')
|
||||||
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
||||||
def repeat(frontend, state):
|
def repeat(frontend, state):
|
||||||
"""
|
"""
|
||||||
@ -303,6 +322,7 @@ def setvol(frontend, volume):
|
|||||||
volume = 100
|
volume = 100
|
||||||
frontend.backend.mixer.volume = volume
|
frontend.backend.mixer.volume = volume
|
||||||
|
|
||||||
|
@handle_pattern(r'^single (?P<state>[01])$')
|
||||||
@handle_pattern(r'^single "(?P<state>[01])"$')
|
@handle_pattern(r'^single "(?P<state>[01])"$')
|
||||||
def single(frontend, state):
|
def single(frontend, state):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -30,14 +30,14 @@ class MpdServer(asyncore.dispatcher):
|
|||||||
else:
|
else:
|
||||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.set_reuse_addr()
|
self.set_reuse_addr()
|
||||||
hostname = self._format_hostname(settings.SERVER_HOSTNAME)
|
hostname = self._format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||||
port = settings.SERVER_PORT
|
port = settings.MPD_SERVER_PORT
|
||||||
logger.debug(u'Binding to [%s]:%s', hostname, port)
|
logger.debug(u'Binding to [%s]:%s', hostname, port)
|
||||||
self.bind((hostname, port))
|
self.bind((hostname, port))
|
||||||
self.listen(1)
|
self.listen(1)
|
||||||
logger.info(u'MPD server running at [%s]:%s',
|
logger.info(u'MPD server running at [%s]:%s',
|
||||||
self._format_hostname(settings.SERVER_HOSTNAME),
|
self._format_hostname(settings.MPD_SERVER_HOSTNAME),
|
||||||
settings.SERVER_PORT)
|
settings.MPD_SERVER_PORT)
|
||||||
except IOError, e:
|
except IOError, e:
|
||||||
sys.exit('MPD server startup failed: %s' % e)
|
sys.exit('MPD server startup failed: %s' % e)
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,8 @@ class GStreamerProcess(BaseProcess):
|
|||||||
http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html.
|
http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pipeline_description = 'appsrc name=data ! volume name=volume ! autoaudiosink name=sink'
|
||||||
|
|
||||||
def __init__(self, core_queue, output_queue):
|
def __init__(self, core_queue, output_queue):
|
||||||
super(GStreamerProcess, self).__init__()
|
super(GStreamerProcess, self).__init__()
|
||||||
self.core_queue = core_queue
|
self.core_queue = core_queue
|
||||||
@ -65,8 +67,10 @@ class GStreamerProcess(BaseProcess):
|
|||||||
messages_thread.daemon = True
|
messages_thread.daemon = True
|
||||||
messages_thread.start()
|
messages_thread.start()
|
||||||
|
|
||||||
# A pipeline consisting of many elements
|
self.gst_pipeline = gst.parse_launch(self.pipeline_description)
|
||||||
self.gst_pipeline = gst.Pipeline("pipeline")
|
self.gst_data_src = self.gst_pipeline.get_by_name('data')
|
||||||
|
self.gst_volume = self.gst_pipeline.get_by_name('volume')
|
||||||
|
self.gst_sink = self.gst_pipeline.get_by_name('sink')
|
||||||
|
|
||||||
# Setup bus and message processor
|
# Setup bus and message processor
|
||||||
self.gst_bus = self.gst_pipeline.get_bus()
|
self.gst_bus = self.gst_pipeline.get_bus()
|
||||||
@ -74,42 +78,6 @@ class GStreamerProcess(BaseProcess):
|
|||||||
self.gst_bus_id = self.gst_bus.connect('message',
|
self.gst_bus_id = self.gst_bus.connect('message',
|
||||||
self.process_gst_message)
|
self.process_gst_message)
|
||||||
|
|
||||||
# Bin for playing audio URIs
|
|
||||||
#self.gst_uri_src = gst.element_factory_make('uridecodebin', 'uri_src')
|
|
||||||
#self.gst_pipeline.add(self.gst_uri_src)
|
|
||||||
|
|
||||||
# Bin for playing audio data
|
|
||||||
self.gst_data_src = gst.element_factory_make('appsrc', 'data_src')
|
|
||||||
self.gst_pipeline.add(self.gst_data_src)
|
|
||||||
|
|
||||||
# Volume filter
|
|
||||||
self.gst_volume = gst.element_factory_make('volume', 'volume')
|
|
||||||
self.gst_pipeline.add(self.gst_volume)
|
|
||||||
|
|
||||||
# Audio output sink
|
|
||||||
self.gst_sink = gst.element_factory_make('autoaudiosink', 'sink')
|
|
||||||
self.gst_pipeline.add(self.gst_sink)
|
|
||||||
|
|
||||||
# Add callback that will link uri_src output with volume filter input
|
|
||||||
# when the output pad is ready.
|
|
||||||
# See http://stackoverflow.com/questions/2993777 for details.
|
|
||||||
def on_new_decoded_pad(dbin, pad, is_last):
|
|
||||||
uri_src = pad.get_parent()
|
|
||||||
pipeline = uri_src.get_parent()
|
|
||||||
volume = pipeline.get_by_name('volume')
|
|
||||||
uri_src.link(volume)
|
|
||||||
logger.debug("Linked uri_src's new decoded pad to volume filter")
|
|
||||||
# FIXME uridecodebin got no new-decoded-pad signal, but it's
|
|
||||||
# subcomponent decodebin2 got that signal. Fixing this is postponed
|
|
||||||
# till after data_src is up and running perfectly
|
|
||||||
#self.gst_uri_src.connect('new-decoded-pad', on_new_decoded_pad)
|
|
||||||
|
|
||||||
# Link data source output with volume filter input
|
|
||||||
self.gst_data_src.link(self.gst_volume)
|
|
||||||
|
|
||||||
# Link volume filter output to audio sink input
|
|
||||||
self.gst_volume.link(self.gst_sink)
|
|
||||||
|
|
||||||
def process_mopidy_message(self, message):
|
def process_mopidy_message(self, message):
|
||||||
"""Process messages from the rest of Mopidy."""
|
"""Process messages from the rest of Mopidy."""
|
||||||
if message['command'] == 'play_uri':
|
if message['command'] == 'play_uri':
|
||||||
|
|||||||
@ -28,16 +28,23 @@ class BaseProcess(multiprocessing.Process):
|
|||||||
except SettingsError as e:
|
except SettingsError as e:
|
||||||
logger.error(e.message)
|
logger.error(e.message)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def run_inside_try(self):
|
def run_inside_try(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class CoreProcess(BaseProcess):
|
class CoreProcess(BaseProcess):
|
||||||
def __init__(self, core_queue):
|
def __init__(self, core_queue, output_class, backend_class,
|
||||||
|
frontend_class):
|
||||||
super(CoreProcess, self).__init__()
|
super(CoreProcess, self).__init__()
|
||||||
self.core_queue = core_queue
|
self.core_queue = core_queue
|
||||||
self.output_queue = None
|
self.output_queue = None
|
||||||
|
self.output_class = output_class
|
||||||
|
self.backend_class = backend_class
|
||||||
|
self.frontend_class = frontend_class
|
||||||
self.output = None
|
self.output = None
|
||||||
self.backend = None
|
self.backend = None
|
||||||
self.frontend = None
|
self.frontend = None
|
||||||
@ -50,11 +57,9 @@ class CoreProcess(BaseProcess):
|
|||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self.output_queue = multiprocessing.Queue()
|
self.output_queue = multiprocessing.Queue()
|
||||||
self.output = get_class(settings.OUTPUT)(self.core_queue,
|
self.output = self.output_class(self.core_queue, self.output_queue)
|
||||||
self.output_queue)
|
self.backend = self.backend_class(self.core_queue, self.output_queue)
|
||||||
self.backend = get_class(settings.BACKENDS[0])(self.core_queue,
|
self.frontend = self.frontend_class(self.backend)
|
||||||
self.output_queue)
|
|
||||||
self.frontend = get_class(settings.FRONTEND)(self.backend)
|
|
||||||
|
|
||||||
def process_message(self, message):
|
def process_message(self, message):
|
||||||
if message.get('to') == 'output':
|
if message.get('to') == 'output':
|
||||||
|
|||||||
@ -3,24 +3,26 @@ Available settings and their default values.
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Do *not* change settings in ``mopidy/settings.py``. Instead, add a file
|
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
|
||||||
called ``~/.mopidy/settings.py`` and redefine settings there.
|
file called ``~/.mopidy/settings.py`` and redefine settings there.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Absolute import needed to import ~/.mopidy/settings.py and not ourselves
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
#: List of playback backends to use. See :mod:`mopidy.backends` for all
|
#: List of playback backends to use. See :mod:`mopidy.backends` for all
|
||||||
#: available backends. Default::
|
#: available backends.
|
||||||
#:
|
#:
|
||||||
#: BACKENDS = (u'mopidy.backends.despotify.DespotifyBackend',)
|
#: Default::
|
||||||
|
#:
|
||||||
|
#: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',)
|
||||||
#:
|
#:
|
||||||
#: .. note::
|
#: .. note::
|
||||||
#: Currently only the first backend in the list is used.
|
#: Currently only the first backend in the list is used.
|
||||||
BACKENDS = (
|
BACKENDS = (
|
||||||
u'mopidy.backends.despotify.DespotifyBackend',
|
u'mopidy.backends.libspotify.LibspotifyBackend',
|
||||||
#u'mopidy.backends.libspotify.LibspotifyBackend',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
#: The log format used on the console. See
|
#: The log format used on the console. See
|
||||||
@ -29,32 +31,51 @@ BACKENDS = (
|
|||||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
|
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
|
||||||
' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
|
' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
|
||||||
|
|
||||||
#: The log format used for dump logs. Default::
|
#: The log format used for dump logs.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: DUMP_LOG_FILENAME = CONSOLE_LOG_FORMAT
|
#: DUMP_LOG_FILENAME = CONSOLE_LOG_FORMAT
|
||||||
DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT
|
DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT
|
||||||
|
|
||||||
#: The file to dump debug log data to. Default::
|
#: The file to dump debug log data to when Mopidy is run with the
|
||||||
|
#: :option:`--dump` option.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: DUMP_LOG_FILENAME = u'dump.log'
|
#: DUMP_LOG_FILENAME = u'dump.log'
|
||||||
DUMP_LOG_FILENAME = u'dump.log'
|
DUMP_LOG_FILENAME = u'dump.log'
|
||||||
|
|
||||||
#: Protocol frontend to use. Default::
|
#: Protocol frontend to use.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
||||||
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
||||||
|
|
||||||
#: Path to folder with local music. Default::
|
#: Path to folder with local music.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.local`.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: LOCAL_MUSIC_FOLDER = u'~/music'
|
#: LOCAL_MUSIC_FOLDER = u'~/music'
|
||||||
LOCAL_MUSIC_FOLDER = u'~/music'
|
LOCAL_MUSIC_FOLDER = u'~/music'
|
||||||
|
|
||||||
#: Path to playlist folder with m3u files for local music. Default::
|
#: Path to playlist folder with m3u files for local music.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.local`.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
|
#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
|
||||||
LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
|
LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
|
||||||
|
|
||||||
#: Path to tag cache for local music. Default::
|
#: Path to tag cache for local music.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.local`.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
|
#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
|
||||||
LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
|
LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
|
||||||
@ -87,6 +108,7 @@ MIXER_ALSA_CONTROL = False
|
|||||||
#: External mixers only. Which port the mixer is connected to.
|
#: External mixers only. Which port the mixer is connected to.
|
||||||
#:
|
#:
|
||||||
#: This must point to the device port like ``/dev/ttyUSB0``.
|
#: This must point to the device port like ``/dev/ttyUSB0``.
|
||||||
|
#:
|
||||||
#: Default: :class:`None`
|
#: Default: :class:`None`
|
||||||
MIXER_EXT_PORT = None
|
MIXER_EXT_PORT = None
|
||||||
|
|
||||||
@ -105,17 +127,23 @@ MIXER_EXT_SPEAKERS_A = None
|
|||||||
#: Default: :class:`None`.
|
#: Default: :class:`None`.
|
||||||
MIXER_EXT_SPEAKERS_B = None
|
MIXER_EXT_SPEAKERS_B = None
|
||||||
|
|
||||||
#: Audio output handler to use. Default::
|
#: Audio output handler to use.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
||||||
OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
||||||
|
|
||||||
#: Server to use. Default::
|
#: Server to use.
|
||||||
|
#:
|
||||||
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
#: SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
||||||
SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
||||||
|
|
||||||
#: Which address Mopidy should bind to. Examples:
|
#: Which address Mopidy's MPD server should bind to.
|
||||||
|
#:
|
||||||
|
#:Examples:
|
||||||
#:
|
#:
|
||||||
#: ``127.0.0.1``
|
#: ``127.0.0.1``
|
||||||
#: Listens only on the IPv4 loopback interface. Default.
|
#: Listens only on the IPv4 loopback interface. Default.
|
||||||
@ -125,23 +153,28 @@ SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
|||||||
#: Listens on all IPv4 interfaces.
|
#: Listens on all IPv4 interfaces.
|
||||||
#: ``::``
|
#: ``::``
|
||||||
#: Listens on all interfaces, both IPv4 and IPv6.
|
#: Listens on all interfaces, both IPv4 and IPv6.
|
||||||
SERVER_HOSTNAME = u'127.0.0.1'
|
MPD_SERVER_HOSTNAME = u'127.0.0.1'
|
||||||
|
|
||||||
#: Which TCP port Mopidy should listen to. Default: 6600
|
#: Which TCP port Mopidy's MPD server should listen to.
|
||||||
SERVER_PORT = 6600
|
#:
|
||||||
|
#: Default: 6600
|
||||||
|
MPD_SERVER_PORT = 6600
|
||||||
|
|
||||||
#: Your Spotify Premium username. Used by all Spotify backends.
|
#: Path to the libspotify cache.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.libspotify`.
|
||||||
|
SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache'
|
||||||
|
|
||||||
|
#: Your Spotify Premium username.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.libspotify`.
|
||||||
SPOTIFY_USERNAME = u''
|
SPOTIFY_USERNAME = u''
|
||||||
|
|
||||||
#: Your Spotify Premium password. Used by all Spotify backends.
|
#: Your Spotify Premium password.
|
||||||
|
#:
|
||||||
|
#: Used by :mod:`mopidy.backends.libspotify`.
|
||||||
SPOTIFY_PASSWORD = u''
|
SPOTIFY_PASSWORD = u''
|
||||||
|
|
||||||
#: Path to your libspotify application key. Used by LibspotifyBackend.
|
|
||||||
SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key'
|
|
||||||
|
|
||||||
#: Path to the libspotify cache. Used by LibspotifyBackend.
|
|
||||||
SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache'
|
|
||||||
|
|
||||||
# Import user specific settings
|
# Import user specific settings
|
||||||
dotdir = os.path.expanduser(u'~/.mopidy/')
|
dotdir = os.path.expanduser(u'~/.mopidy/')
|
||||||
settings_file = os.path.join(dotdir, u'settings.py')
|
settings_file = os.path.join(dotdir, u'settings.py')
|
||||||
|
|||||||
@ -24,8 +24,11 @@ def get_class(name):
|
|||||||
module_name = name[:name.rindex('.')]
|
module_name = name[:name.rindex('.')]
|
||||||
class_name = name[name.rindex('.') + 1:]
|
class_name = name[name.rindex('.') + 1:]
|
||||||
logger.debug('Loading: %s', name)
|
logger.debug('Loading: %s', name)
|
||||||
module = import_module(module_name)
|
try:
|
||||||
class_object = getattr(module, class_name)
|
module = import_module(module_name)
|
||||||
|
class_object = getattr(module, class_name)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
raise ImportError("Couldn't load: %s" % name)
|
||||||
return class_object
|
return class_object
|
||||||
|
|
||||||
def get_or_create_folder(folder):
|
def get_or_create_folder(folder):
|
||||||
|
|||||||
36
setup.py
36
setup.py
@ -1,9 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Most of this file is taken from the Django project, which is BSD licensed.
|
||||||
|
"""
|
||||||
|
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
from distutils.command.install_data import install_data
|
||||||
from distutils.command.install import INSTALL_SCHEMES
|
from distutils.command.install import INSTALL_SCHEMES
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from mopidy import get_version
|
from mopidy import get_version
|
||||||
|
|
||||||
|
class osx_install_data(install_data):
|
||||||
|
# On MacOS, the platform-specific lib dir is
|
||||||
|
# /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied
|
||||||
|
# with MacOS 10.5 has an Apple-specific fix for this in
|
||||||
|
# distutils.command.install_data#306. It fixes install_lib but not
|
||||||
|
# install_data, which is why we roll our own install_data class.
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
# By the time finalize_options is called, install.install_lib is set to
|
||||||
|
# the fixed directory, so we set the installdir to install_lib. The
|
||||||
|
# install_data class uses ('install_data', 'install_dir') instead.
|
||||||
|
self.set_undefined_options('install', ('install_lib', 'install_dir'))
|
||||||
|
install_data.finalize_options(self)
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
cmdclasses = {'install_data': osx_install_data}
|
||||||
|
else:
|
||||||
|
cmdclasses = {'install_data': install_data}
|
||||||
|
|
||||||
def fullsplit(path, result=None):
|
def fullsplit(path, result=None):
|
||||||
"""
|
"""
|
||||||
Split a pathname into components (the opposite of os.path.join) in a
|
Split a pathname into components (the opposite of os.path.join) in a
|
||||||
@ -20,7 +45,8 @@ def fullsplit(path, result=None):
|
|||||||
|
|
||||||
# Tell distutils to put the data_files in platform-specific installation
|
# Tell distutils to put the data_files in platform-specific installation
|
||||||
# locations. See here for an explanation:
|
# locations. See here for an explanation:
|
||||||
# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
|
# http://groups.google.com/group/comp.lang.python/browse_thread/
|
||||||
|
# thread/35ec7b2fed36eaec/2105ee4d9e8042cb
|
||||||
for scheme in INSTALL_SCHEMES.values():
|
for scheme in INSTALL_SCHEMES.values():
|
||||||
scheme['data'] = scheme['purelib']
|
scheme['data'] = scheme['purelib']
|
||||||
|
|
||||||
@ -49,17 +75,19 @@ setup(
|
|||||||
author='Stein Magnus Jodal',
|
author='Stein Magnus Jodal',
|
||||||
author_email='stein.magnus@jodal.no',
|
author_email='stein.magnus@jodal.no',
|
||||||
packages=packages,
|
packages=packages,
|
||||||
|
package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']},
|
||||||
|
cmdclass=cmdclasses,
|
||||||
data_files=data_files,
|
data_files=data_files,
|
||||||
scripts=['bin/mopidy'],
|
scripts=['bin/mopidy'],
|
||||||
url='http://www.mopidy.com/',
|
url='http://www.mopidy.com/',
|
||||||
license='GPLv2',
|
license='Apache License, Version 2.0',
|
||||||
description='MPD server with Spotify support',
|
description='MPD server with Spotify support',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.rst').read(),
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 4 - Beta',
|
||||||
'Environment :: No Input/Output (Daemon)',
|
'Environment :: No Input/Output (Daemon)',
|
||||||
'Intended Audience :: End Users/Desktop',
|
'Intended Audience :: End Users/Desktop',
|
||||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
'License :: OSI Approved :: Apache Software License',
|
||||||
'Operating System :: MacOS :: MacOS X',
|
'Operating System :: MacOS :: MacOS X',
|
||||||
'Operating System :: POSIX :: Linux',
|
'Operating System :: POSIX :: Linux',
|
||||||
'Programming Language :: Python :: 2.6',
|
'Programming Language :: Python :: 2.6',
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
# TODO This integration test is work in progress.
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from mopidy.backends.despotify import DespotifyBackend
|
|
||||||
from mopidy.models import Track
|
|
||||||
|
|
||||||
from tests.backends.base import *
|
|
||||||
|
|
||||||
uris = [
|
|
||||||
'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt',
|
|
||||||
'spotify:track:111sulhaZqgsnypz3MkiaW',
|
|
||||||
'spotify:track:7t8oznvbeiAPMDRuK0R5ZT',
|
|
||||||
]
|
|
||||||
|
|
||||||
class DespotifyCurrentPlaylistControllerTest(
|
|
||||||
BaseCurrentPlaylistControllerTest, unittest.TestCase):
|
|
||||||
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
|
|
||||||
backend_class = DespotifyBackend
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyPlaybackControllerTest(
|
|
||||||
BasePlaybackControllerTest, unittest.TestCase):
|
|
||||||
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
|
|
||||||
backend_class = DespotifyBackend
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyStoredPlaylistsControllerTest(
|
|
||||||
BaseStoredPlaylistsControllerTest, unittest.TestCase):
|
|
||||||
backend_class = DespotifyBackend
|
|
||||||
|
|
||||||
|
|
||||||
class DespotifyLibraryControllerTest(
|
|
||||||
BaseLibraryControllerTest, unittest.TestCase):
|
|
||||||
backend_class = DespotifyBackend
|
|
||||||
@ -321,6 +321,24 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
|||||||
self.assert_(u'Title: c' in result)
|
self.assert_(u'Title: c' in result)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_plchanges_with_minus_one_returns_entire_playlist(self):
|
||||||
|
self.b.current_playlist.load(
|
||||||
|
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||||
|
result = self.h.handle_request(u'plchanges "-1"')
|
||||||
|
self.assert_(u'Title: a' in result)
|
||||||
|
self.assert_(u'Title: b' in result)
|
||||||
|
self.assert_(u'Title: c' in result)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_plchanges_without_quotes_works(self):
|
||||||
|
self.b.current_playlist.load(
|
||||||
|
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||||
|
result = self.h.handle_request(u'plchanges 0')
|
||||||
|
self.assert_(u'Title: a' in result)
|
||||||
|
self.assert_(u'Title: b' in result)
|
||||||
|
self.assert_(u'Title: c' in result)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_plchangesposid(self):
|
def test_plchangesposid(self):
|
||||||
self.b.current_playlist.load([Track(), Track(), Track()])
|
self.b.current_playlist.load([Track(), Track(), Track()])
|
||||||
result = self.h.handle_request(u'plchangesposid "0"')
|
result = self.h.handle_request(u'plchangesposid "0"')
|
||||||
|
|||||||
@ -16,11 +16,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
|
|||||||
self.assertFalse(self.b.playback.consume)
|
self.assertFalse(self.b.playback.consume)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_consume_off_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'consume 0')
|
||||||
|
self.assertFalse(self.b.playback.consume)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_consume_on(self):
|
def test_consume_on(self):
|
||||||
result = self.h.handle_request(u'consume "1"')
|
result = self.h.handle_request(u'consume "1"')
|
||||||
self.assertTrue(self.b.playback.consume)
|
self.assertTrue(self.b.playback.consume)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_consume_on_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'consume 1')
|
||||||
|
self.assertTrue(self.b.playback.consume)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_crossfade(self):
|
def test_crossfade(self):
|
||||||
result = self.h.handle_request(u'crossfade "10"')
|
result = self.h.handle_request(u'crossfade "10"')
|
||||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||||
@ -30,21 +40,41 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
|
|||||||
self.assertFalse(self.b.playback.random)
|
self.assertFalse(self.b.playback.random)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_random_off_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'random 0')
|
||||||
|
self.assertFalse(self.b.playback.random)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_random_on(self):
|
def test_random_on(self):
|
||||||
result = self.h.handle_request(u'random "1"')
|
result = self.h.handle_request(u'random "1"')
|
||||||
self.assertTrue(self.b.playback.random)
|
self.assertTrue(self.b.playback.random)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_random_on_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'random 1')
|
||||||
|
self.assertTrue(self.b.playback.random)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_repeat_off(self):
|
def test_repeat_off(self):
|
||||||
result = self.h.handle_request(u'repeat "0"')
|
result = self.h.handle_request(u'repeat "0"')
|
||||||
self.assertFalse(self.b.playback.repeat)
|
self.assertFalse(self.b.playback.repeat)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_repeat_off_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'repeat 0')
|
||||||
|
self.assertFalse(self.b.playback.repeat)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_repeat_on(self):
|
def test_repeat_on(self):
|
||||||
result = self.h.handle_request(u'repeat "1"')
|
result = self.h.handle_request(u'repeat "1"')
|
||||||
self.assertTrue(self.b.playback.repeat)
|
self.assertTrue(self.b.playback.repeat)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_repeat_on_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'repeat 1')
|
||||||
|
self.assertTrue(self.b.playback.repeat)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_setvol_below_min(self):
|
def test_setvol_below_min(self):
|
||||||
result = self.h.handle_request(u'setvol "-10"')
|
result = self.h.handle_request(u'setvol "-10"')
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
@ -80,11 +110,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
|
|||||||
self.assertFalse(self.b.playback.single)
|
self.assertFalse(self.b.playback.single)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_single_off_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'single 0')
|
||||||
|
self.assertFalse(self.b.playback.single)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_single_on(self):
|
def test_single_on(self):
|
||||||
result = self.h.handle_request(u'single "1"')
|
result = self.h.handle_request(u'single "1"')
|
||||||
self.assertTrue(self.b.playback.single)
|
self.assertTrue(self.b.playback.single)
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
|
def test_single_on_without_quotes(self):
|
||||||
|
result = self.h.handle_request(u'single 1')
|
||||||
|
self.assertTrue(self.b.playback.single)
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_replay_gain_mode_off(self):
|
def test_replay_gain_mode_off(self):
|
||||||
result = self.h.handle_request(u'replay_gain_mode "off"')
|
result = self.h.handle_request(u'replay_gain_mode "off"')
|
||||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||||
@ -136,8 +176,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
|||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_pause_off(self):
|
def test_pause_off(self):
|
||||||
track = Track()
|
self.b.current_playlist.load([Track()])
|
||||||
self.b.current_playlist.load([track])
|
|
||||||
self.h.handle_request(u'play "0"')
|
self.h.handle_request(u'play "0"')
|
||||||
self.h.handle_request(u'pause "1"')
|
self.h.handle_request(u'pause "1"')
|
||||||
result = self.h.handle_request(u'pause "0"')
|
result = self.h.handle_request(u'pause "0"')
|
||||||
@ -145,16 +184,26 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
|||||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||||
|
|
||||||
def test_pause_on(self):
|
def test_pause_on(self):
|
||||||
track = Track()
|
self.b.current_playlist.load([Track()])
|
||||||
self.b.current_playlist.load([track])
|
|
||||||
self.h.handle_request(u'play "0"')
|
self.h.handle_request(u'play "0"')
|
||||||
result = self.h.handle_request(u'pause "1"')
|
result = self.h.handle_request(u'pause "1"')
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
|
self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
|
||||||
|
|
||||||
|
def test_pause_toggle(self):
|
||||||
|
self.b.current_playlist.load([Track()])
|
||||||
|
result = self.h.handle_request(u'play "0"')
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||||
|
result = self.h.handle_request(u'pause')
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
|
||||||
|
result = self.h.handle_request(u'pause')
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||||
|
|
||||||
def test_play_without_pos(self):
|
def test_play_without_pos(self):
|
||||||
track = Track()
|
self.b.current_playlist.load([Track()])
|
||||||
self.b.current_playlist.load([track])
|
|
||||||
self.b.playback.state = self.b.playback.PAUSED
|
self.b.playback.state = self.b.playback.PAUSED
|
||||||
result = self.h.handle_request(u'play')
|
result = self.h.handle_request(u'play')
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
@ -166,6 +215,12 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
|||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||||
|
|
||||||
|
def test_play_with_pos_without_quotes(self):
|
||||||
|
self.b.current_playlist.load([Track()])
|
||||||
|
result = self.h.handle_request(u'play 0')
|
||||||
|
self.assert_(u'OK' in result)
|
||||||
|
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||||
|
|
||||||
def test_play_with_pos_out_of_bounds(self):
|
def test_play_with_pos_out_of_bounds(self):
|
||||||
self.b.current_playlist.load([])
|
self.b.current_playlist.load([])
|
||||||
result = self.h.handle_request(u'play "0"')
|
result = self.h.handle_request(u'play "0"')
|
||||||
|
|||||||
@ -11,6 +11,25 @@ from mopidy.models import Track, Artist, Album
|
|||||||
|
|
||||||
from tests import SkipTest, data_folder
|
from tests import SkipTest, data_folder
|
||||||
|
|
||||||
|
class GetClassTest(unittest.TestCase):
|
||||||
|
def test_loading_module_that_does_not_exist(self):
|
||||||
|
test = lambda: get_class('foo.bar.Baz')
|
||||||
|
self.assertRaises(ImportError, test)
|
||||||
|
|
||||||
|
def test_loading_class_that_does_not_exist(self):
|
||||||
|
test = lambda: get_class('unittest.FooBarBaz')
|
||||||
|
self.assertRaises(ImportError, test)
|
||||||
|
|
||||||
|
def test_import_error_message_contains_complete_class_path(self):
|
||||||
|
try:
|
||||||
|
get_class('foo.bar.Baz')
|
||||||
|
except ImportError as e:
|
||||||
|
self.assert_('foo.bar.Baz' in str(e))
|
||||||
|
|
||||||
|
def test_loading_existing_class(self):
|
||||||
|
cls = get_class('unittest.TestCase')
|
||||||
|
self.assertEqual(cls.__name__, 'TestCase')
|
||||||
|
|
||||||
class GetOrCreateFolderTest(unittest.TestCase):
|
class GetOrCreateFolderTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.parent = tempfile.mkdtemp()
|
self.parent = tempfile.mkdtemp()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user