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 *
|
||||
prune docs/_build
|
||||
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:
|
||||
|
||||
|
||||
: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
|
||||
=========================================================
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ there.
|
||||
|
||||
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_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
@ -12,17 +12,31 @@ Another great release.
|
||||
|
||||
**Changes**
|
||||
|
||||
- License changed from GPLv2 to Apache License, version 2.0.
|
||||
- GStreamer is now a required dependency.
|
||||
- Exit early if not Python >= 2.6, < 3.
|
||||
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
|
||||
the packages created by ``setup.py`` for i.e. PyPI.
|
||||
- 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:
|
||||
|
||||
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
|
||||
- Split gigantic protocol implementation into eleven modules.
|
||||
- Search improvements, including support for multi-word search.
|
||||
- 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:
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
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
|
||||
# |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
|
||||
non-supported functionality.
|
||||
- Read-only support for Spotify through :mod:`mopidy.backends.despotify` and/or
|
||||
:mod:`mopidy.backends.libspotify`.
|
||||
- Read-only support for Spotify through :mod:`mopidy.backends.libspotify`.
|
||||
- Initial support for local file playback through
|
||||
:mod:`mopidy.backends.local`. The state of local file playback will not
|
||||
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
|
||||
==================================================
|
||||
|
||||
- Replace libspotify with `openspotify
|
||||
<http://github.com/noahwilliamsson/openspotify>`_ for
|
||||
:mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify
|
||||
development has stalled.
|
||||
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies
|
||||
for all our dependencies and Mopidy itself to make OS X installation a
|
||||
breeze. See `Homebrew's issue #1612
|
||||
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
|
||||
- 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
|
||||
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.
|
||||
- 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
|
||||
|
||||
@ -9,6 +9,7 @@ User documentation
|
||||
installation/index
|
||||
changes
|
||||
authors
|
||||
licenses
|
||||
|
||||
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
|
||||
************
|
||||
|
||||
Mopidy itself is a breeze to install, as it just requires a standard Python
|
||||
installation and the GStreamer library. The libraries we depend on to connect
|
||||
to the Spotify service is far more tricky to get working for the time being.
|
||||
Until installation of these libraries are either well documented by their
|
||||
developers, or the libraries are packaged for various Linux distributions, we
|
||||
will supply our own installation guides, as linked to below.
|
||||
To get a basic version of Mopidy running, you need Python and the GStreamer
|
||||
library. To use Spotify with Mopidy, you also need :doc:`libspotify and
|
||||
pyspotify <libspotify>`. Mopidy itself can either be installed from the Python
|
||||
package index, PyPI, or from git.
|
||||
|
||||
|
||||
Install dependencies
|
||||
@ -18,12 +16,11 @@ Install dependencies
|
||||
|
||||
gstreamer
|
||||
libspotify
|
||||
despotify
|
||||
|
||||
Make sure you got the required dependencies installed.
|
||||
|
||||
- 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:
|
||||
|
||||
- :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:
|
||||
|
||||
- :mod:`mopidy.backends.despotify` (Linux and OS X)
|
||||
|
||||
- :doc:`Despotify and spytify <despotify>`
|
||||
|
||||
- :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
|
||||
|
||||
- :doc:`libspotify and pyspotify <libspotify>`
|
||||
@ -106,14 +99,9 @@ username and password into the file, like this::
|
||||
SPOTIFY_USERNAME = u'myusername'
|
||||
SPOTIFY_PASSWORD = u'mysecret'
|
||||
|
||||
Currently :mod:`mopidy.backends.despotify` is the default
|
||||
backend.
|
||||
|
||||
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',)
|
||||
Currently :mod:`mopidy.backends.libspotify` is the default
|
||||
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.local`, add the following setting::
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
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.
|
||||
To use the libspotify backend you must install libspotify and
|
||||
`pyspotify <http://github.com/winjer/pyspotify>`_.
|
||||
@ -57,22 +57,10 @@ Installing pyspotify
|
||||
|
||||
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::
|
||||
|
||||
git clone git://github.com/jodal/pyspotify.git
|
||||
cd pyspotify/pyspotify/
|
||||
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/')
|
||||
core_queue = multiprocessing.Queue()
|
||||
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()
|
||||
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 os
|
||||
import multiprocessing
|
||||
import threading
|
||||
|
||||
from spotify import Link, SpotifyError
|
||||
from spotify.manager import SpotifySessionManager
|
||||
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
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.libspotify')
|
||||
|
||||
@ -28,15 +16,19 @@ class LibspotifyBackend(BaseBackend):
|
||||
for libspotify. It got no documentation, but multiple examples are
|
||||
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
|
||||
"""
|
||||
|
||||
# Imports inside methods are to prevent loading of __init__.py to fail on
|
||||
# missing spotify dependencies.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from .library import LibspotifyLibraryController
|
||||
from .playback import LibspotifyPlaybackController
|
||||
from .stored_playlists import LibspotifyStoredPlaylistsController
|
||||
|
||||
super(LibspotifyBackend, self).__init__(*args, **kwargs)
|
||||
|
||||
self.current_playlist = BaseCurrentPlaylistController(backend=self)
|
||||
self.library = LibspotifyLibraryController(backend=self)
|
||||
self.playback = LibspotifyPlaybackController(backend=self)
|
||||
@ -46,6 +38,8 @@ class LibspotifyBackend(BaseBackend):
|
||||
self.spotify = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
from .session_manager import LibspotifySessionManager
|
||||
|
||||
logger.info(u'Connecting to Spotify')
|
||||
spotify = LibspotifySessionManager(
|
||||
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
|
||||
@ -53,259 +47,3 @@ class LibspotifyBackend(BaseBackend):
|
||||
output_queue=self.output_queue)
|
||||
spotify.start()
|
||||
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:
|
||||
cpid = int(cpid)
|
||||
if frontend.backend.playback.current_cpid == cpid:
|
||||
frontend.backend.playback.next()
|
||||
return frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||
@ -257,7 +259,8 @@ def playlistsearch(frontend, tag, needle):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
*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
|
||||
``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
|
||||
if int(version) < frontend.backend.current_playlist.version:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^consume (?P<state>[01])$')
|
||||
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
||||
def consume(frontend, state):
|
||||
"""
|
||||
@ -86,16 +87,28 @@ def next_(frontend):
|
||||
"""
|
||||
return frontend.backend.playback.next()
|
||||
|
||||
@handle_pattern(r'^pause$')
|
||||
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||
def pause(frontend, state):
|
||||
def pause(frontend, state=None):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``pause {PAUSE}``
|
||||
|
||||
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()
|
||||
else:
|
||||
frontend.backend.playback.resume()
|
||||
@ -135,8 +148,8 @@ def playid(frontend, cpid):
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||
|
||||
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
|
||||
@handle_pattern(r'^play "(?P<songpos>-1)"$')
|
||||
@handle_pattern(r'^play (?P<songpos>-?\d+)$')
|
||||
@handle_pattern(r'^play "(?P<songpos>-?\d+)"$')
|
||||
def playpos(frontend, songpos):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
@ -149,6 +162,10 @@ def playpos(frontend, songpos):
|
||||
|
||||
- issues ``play "-1"`` after playlist replacement to start playback at
|
||||
the first track.
|
||||
|
||||
*BitMPC:*
|
||||
|
||||
- issues ``play 6`` without quotes around the argument.
|
||||
"""
|
||||
songpos = int(songpos)
|
||||
try:
|
||||
@ -208,6 +225,7 @@ def previous(frontend):
|
||||
"""
|
||||
return frontend.backend.playback.previous()
|
||||
|
||||
@handle_pattern(r'^random (?P<state>[01])$')
|
||||
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||
def random(frontend, state):
|
||||
"""
|
||||
@ -222,6 +240,7 @@ def random(frontend, state):
|
||||
else:
|
||||
frontend.backend.playback.random = False
|
||||
|
||||
@handle_pattern(r'^repeat (?P<state>[01])$')
|
||||
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
||||
def repeat(frontend, state):
|
||||
"""
|
||||
@ -303,6 +322,7 @@ def setvol(frontend, volume):
|
||||
volume = 100
|
||||
frontend.backend.mixer.volume = volume
|
||||
|
||||
@handle_pattern(r'^single (?P<state>[01])$')
|
||||
@handle_pattern(r'^single "(?P<state>[01])"$')
|
||||
def single(frontend, state):
|
||||
"""
|
||||
|
||||
@ -30,14 +30,14 @@ class MpdServer(asyncore.dispatcher):
|
||||
else:
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.set_reuse_addr()
|
||||
hostname = self._format_hostname(settings.SERVER_HOSTNAME)
|
||||
port = settings.SERVER_PORT
|
||||
hostname = self._format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
logger.debug(u'Binding to [%s]:%s', hostname, port)
|
||||
self.bind((hostname, port))
|
||||
self.listen(1)
|
||||
logger.info(u'MPD server running at [%s]:%s',
|
||||
self._format_hostname(settings.SERVER_HOSTNAME),
|
||||
settings.SERVER_PORT)
|
||||
self._format_hostname(settings.MPD_SERVER_HOSTNAME),
|
||||
settings.MPD_SERVER_PORT)
|
||||
except IOError, 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.
|
||||
"""
|
||||
|
||||
pipeline_description = 'appsrc name=data ! volume name=volume ! autoaudiosink name=sink'
|
||||
|
||||
def __init__(self, core_queue, output_queue):
|
||||
super(GStreamerProcess, self).__init__()
|
||||
self.core_queue = core_queue
|
||||
@ -65,8 +67,10 @@ class GStreamerProcess(BaseProcess):
|
||||
messages_thread.daemon = True
|
||||
messages_thread.start()
|
||||
|
||||
# A pipeline consisting of many elements
|
||||
self.gst_pipeline = gst.Pipeline("pipeline")
|
||||
self.gst_pipeline = gst.parse_launch(self.pipeline_description)
|
||||
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
|
||||
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.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):
|
||||
"""Process messages from the rest of Mopidy."""
|
||||
if message['command'] == 'play_uri':
|
||||
|
||||
@ -28,16 +28,23 @@ class BaseProcess(multiprocessing.Process):
|
||||
except SettingsError as e:
|
||||
logger.error(e.message)
|
||||
sys.exit(1)
|
||||
except ImportError as e:
|
||||
logger.error(e)
|
||||
sys.exit(1)
|
||||
|
||||
def run_inside_try(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CoreProcess(BaseProcess):
|
||||
def __init__(self, core_queue):
|
||||
def __init__(self, core_queue, output_class, backend_class,
|
||||
frontend_class):
|
||||
super(CoreProcess, self).__init__()
|
||||
self.core_queue = core_queue
|
||||
self.output_queue = None
|
||||
self.output_class = output_class
|
||||
self.backend_class = backend_class
|
||||
self.frontend_class = frontend_class
|
||||
self.output = None
|
||||
self.backend = None
|
||||
self.frontend = None
|
||||
@ -50,11 +57,9 @@ class CoreProcess(BaseProcess):
|
||||
|
||||
def setup(self):
|
||||
self.output_queue = multiprocessing.Queue()
|
||||
self.output = get_class(settings.OUTPUT)(self.core_queue,
|
||||
self.output_queue)
|
||||
self.backend = get_class(settings.BACKENDS[0])(self.core_queue,
|
||||
self.output_queue)
|
||||
self.frontend = get_class(settings.FRONTEND)(self.backend)
|
||||
self.output = self.output_class(self.core_queue, self.output_queue)
|
||||
self.backend = self.backend_class(self.core_queue, self.output_queue)
|
||||
self.frontend = self.frontend_class(self.backend)
|
||||
|
||||
def process_message(self, message):
|
||||
if message.get('to') == 'output':
|
||||
|
||||
@ -3,24 +3,26 @@ Available settings and their default values.
|
||||
|
||||
.. warning::
|
||||
|
||||
Do *not* change settings in ``mopidy/settings.py``. Instead, add a file
|
||||
called ``~/.mopidy/settings.py`` and redefine settings there.
|
||||
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
|
||||
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
|
||||
import os
|
||||
import sys
|
||||
|
||||
#: 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::
|
||||
#: Currently only the first backend in the list is used.
|
||||
BACKENDS = (
|
||||
u'mopidy.backends.despotify.DespotifyBackend',
|
||||
#u'mopidy.backends.libspotify.LibspotifyBackend',
|
||||
u'mopidy.backends.libspotify.LibspotifyBackend',
|
||||
)
|
||||
|
||||
#: The log format used on the console. See
|
||||
@ -29,32 +31,51 @@ BACKENDS = (
|
||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)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_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'
|
||||
|
||||
#: Protocol frontend to use. Default::
|
||||
#: Protocol frontend to use.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: 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'
|
||||
|
||||
#: 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'
|
||||
|
||||
#: 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'
|
||||
@ -87,6 +108,7 @@ MIXER_ALSA_CONTROL = False
|
||||
#: External mixers only. Which port the mixer is connected to.
|
||||
#:
|
||||
#: This must point to the device port like ``/dev/ttyUSB0``.
|
||||
#:
|
||||
#: Default: :class:`None`
|
||||
MIXER_EXT_PORT = None
|
||||
|
||||
@ -105,17 +127,23 @@ MIXER_EXT_SPEAKERS_A = None
|
||||
#: Default: :class:`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'
|
||||
|
||||
#: Server to use. Default::
|
||||
#: Server to use.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: 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``
|
||||
#: 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 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
|
||||
SERVER_PORT = 6600
|
||||
#: Which TCP port Mopidy's MPD server should listen to.
|
||||
#:
|
||||
#: 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''
|
||||
|
||||
#: Your Spotify Premium password. Used by all Spotify backends.
|
||||
#: Your Spotify Premium password.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.libspotify`.
|
||||
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
|
||||
dotdir = os.path.expanduser(u'~/.mopidy/')
|
||||
settings_file = os.path.join(dotdir, u'settings.py')
|
||||
|
||||
@ -24,8 +24,11 @@ def get_class(name):
|
||||
module_name = name[:name.rindex('.')]
|
||||
class_name = name[name.rindex('.') + 1:]
|
||||
logger.debug('Loading: %s', name)
|
||||
module = import_module(module_name)
|
||||
class_object = getattr(module, class_name)
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
class_object = getattr(module, class_name)
|
||||
except (ImportError, AttributeError):
|
||||
raise ImportError("Couldn't load: %s" % name)
|
||||
return class_object
|
||||
|
||||
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.command.install_data import install_data
|
||||
from distutils.command.install import INSTALL_SCHEMES
|
||||
import os
|
||||
import sys
|
||||
|
||||
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):
|
||||
"""
|
||||
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
|
||||
# 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():
|
||||
scheme['data'] = scheme['purelib']
|
||||
|
||||
@ -49,17 +75,19 @@ setup(
|
||||
author='Stein Magnus Jodal',
|
||||
author_email='stein.magnus@jodal.no',
|
||||
packages=packages,
|
||||
package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']},
|
||||
cmdclass=cmdclasses,
|
||||
data_files=data_files,
|
||||
scripts=['bin/mopidy'],
|
||||
url='http://www.mopidy.com/',
|
||||
license='GPLv2',
|
||||
license='Apache License, Version 2.0',
|
||||
description='MPD server with Spotify support',
|
||||
long_description=open('README.rst').read(),
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'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 :: POSIX :: Linux',
|
||||
'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'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):
|
||||
self.b.current_playlist.load([Track(), Track(), Track()])
|
||||
result = self.h.handle_request(u'plchangesposid "0"')
|
||||
|
||||
@ -16,11 +16,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
|
||||
self.assertFalse(self.b.playback.consume)
|
||||
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):
|
||||
result = self.h.handle_request(u'consume "1"')
|
||||
self.assertTrue(self.b.playback.consume)
|
||||
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):
|
||||
result = self.h.handle_request(u'crossfade "10"')
|
||||
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.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):
|
||||
result = self.h.handle_request(u'random "1"')
|
||||
self.assertTrue(self.b.playback.random)
|
||||
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):
|
||||
result = self.h.handle_request(u'repeat "0"')
|
||||
self.assertFalse(self.b.playback.repeat)
|
||||
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):
|
||||
result = self.h.handle_request(u'repeat "1"')
|
||||
self.assertTrue(self.b.playback.repeat)
|
||||
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):
|
||||
result = self.h.handle_request(u'setvol "-10"')
|
||||
self.assert_(u'OK' in result)
|
||||
@ -80,11 +110,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
|
||||
self.assertFalse(self.b.playback.single)
|
||||
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):
|
||||
result = self.h.handle_request(u'single "1"')
|
||||
self.assertTrue(self.b.playback.single)
|
||||
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):
|
||||
result = self.h.handle_request(u'replay_gain_mode "off"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
@ -136,8 +176,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
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'pause "1"')
|
||||
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)
|
||||
|
||||
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"')
|
||||
result = self.h.handle_request(u'pause "1"')
|
||||
self.assert_(u'OK' in result)
|
||||
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):
|
||||
track = Track()
|
||||
self.b.current_playlist.load([track])
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
result = self.h.handle_request(u'play')
|
||||
self.assert_(u'OK' in result)
|
||||
@ -166,6 +215,12 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
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):
|
||||
self.b.current_playlist.load([])
|
||||
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
|
||||
|
||||
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):
|
||||
def setUp(self):
|
||||
self.parent = tempfile.mkdtemp()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user