diff --git a/COPYING b/COPYING deleted file mode 100644 index d511905c..00000000 --- a/COPYING +++ /dev/null @@ -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. - - - Copyright (C) - - 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. - - , 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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MANIFEST.in b/MANIFEST.in index cb752f87..38819adb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/docs/api/backends.rst b/docs/api/backends.rst index adb87e56..f675541a 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -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 ========================================================= diff --git a/docs/api/settings.rst b/docs/api/settings.rst index a8ff6446..cfc270d6 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -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' diff --git a/docs/changes.rst b/docs/changes.rst index d38a135a..c20b2ad1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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: diff --git a/docs/conf.py b/docs/conf.py index b190e8a9..c95c39df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 7dc284df..243243ab 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -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 - `_ for - :mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify - development has stalled. +- **[PENDING]** Create `Homebrew `_ recipies + for all our dependencies and Mopidy itself to make OS X installation a + breeze. See `Homebrew's issue #1612 + `_. - Create `Debian packages `_ 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 `_ 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 diff --git a/docs/index.rst b/docs/index.rst index 49c41226..7c53572c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ User documentation installation/index changes authors + licenses Reference documentation ======================= diff --git a/docs/installation/despotify.rst b/docs/installation/despotify.rst deleted file mode 100644 index 6787070d..00000000 --- a/docs/installation/despotify.rst +++ /dev/null @@ -1,73 +0,0 @@ -********************** -Despotify installation -********************** - -To use the `Despotify `_ 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 `_ and -`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. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 73ae62cb..d5e76cce 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -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 `. 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 ` (>= 0.10 ?) with Python bindings +- :doc:`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 ` - - :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows) - :doc:`libspotify and pyspotify ` @@ -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:: diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 9dc9689f..635c0495 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -2,7 +2,7 @@ libspotify installation *********************** -As an alternative to the despotify backend, we are working on a +We are working on a `libspotify `_ backend. To use the libspotify backend you must install libspotify and `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 diff --git a/docs/licenses.rst b/docs/licenses.rst new file mode 100644 index 00000000..c7bf9433 --- /dev/null +++ b/docs/licenses.rst @@ -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. diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 7c62033b..c92ce1ed 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -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() diff --git a/mopidy/backends/despotify/__init__.py b/mopidy/backends/despotify/__init__.py deleted file mode 100644 index 78c7f774..00000000 --- a/mopidy/backends/despotify/__init__.py +++ /dev/null @@ -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 - `_. - - `spytify `_ - 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') diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index c256b55d..7a971bc5 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -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 - `_, 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') diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py new file mode 100644 index 00000000..c2b70dca --- /dev/null +++ b/mopidy/backends/libspotify/library.py @@ -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 diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py new file mode 100644 index 00000000..3ba91d5f --- /dev/null +++ b/mopidy/backends/libspotify/playback.py @@ -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 diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py new file mode 100644 index 00000000..707423aa --- /dev/null +++ b/mopidy/backends/libspotify/session_manager.py @@ -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) diff --git a/mopidy/backends/libspotify/spotify_appkey.key b/mopidy/backends/libspotify/spotify_appkey.key new file mode 100644 index 00000000..1f840b96 Binary files /dev/null and b/mopidy/backends/libspotify/spotify_appkey.key differ diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/libspotify/stored_playlists.py new file mode 100644 index 00000000..3339578c --- /dev/null +++ b/mopidy/backends/libspotify/stored_playlists.py @@ -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 diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py new file mode 100644 index 00000000..3a39aad5 --- /dev/null +++ b/mopidy/backends/libspotify/translator.py @@ -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], + ) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index da052fff..30acbe89 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -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\d+)"$') +@handle_pattern(r'^plchanges (?P-?\d+)$') +@handle_pattern(r'^plchanges "(?P-?\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: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 719bd8b5..cf803c6d 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,6 +1,7 @@ from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError, MpdNotImplemented) +@handle_pattern(r'^consume (?P[01])$') @handle_pattern(r'^consume "(?P[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[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\d+)"$') -@handle_pattern(r'^play "(?P-1)"$') +@handle_pattern(r'^play (?P-?\d+)$') +@handle_pattern(r'^play "(?P-?\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[01])$') @handle_pattern(r'^random "(?P[01])"$') def random(frontend, state): """ @@ -222,6 +240,7 @@ def random(frontend, state): else: frontend.backend.playback.random = False +@handle_pattern(r'^repeat (?P[01])$') @handle_pattern(r'^repeat "(?P[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[01])$') @handle_pattern(r'^single "(?P[01])"$') def single(frontend, state): """ diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 5bdbb85a..91d8e67a 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -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) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 65b65504..b81fbd0f 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -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': diff --git a/mopidy/process.py b/mopidy/process.py index 9759c4e6..b1cdc8af 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -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': diff --git a/mopidy/settings.py b/mopidy/settings.py index 8fdc3535..b17af913 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -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') diff --git a/mopidy/utils.py b/mopidy/utils.py index ff032b4e..bdc0b632 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.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): diff --git a/setup.py b/setup.py index bbf300f7..fabc8353 100644 --- a/setup.py +++ b/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', diff --git a/tests/backends/despotify_integrationtest.py b/tests/backends/despotify_integrationtest.py deleted file mode 100644 index 4192bf7b..00000000 --- a/tests/backends/despotify_integrationtest.py +++ /dev/null @@ -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 diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index ce1e4069..0d639f89 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -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"') diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index aee05d6c..3cf0a11f 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -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"') diff --git a/tests/utils_test.py b/tests/utils_test.py index d5c98d86..ca44de45 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -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()