diff --git a/README.md b/README.md
index 546f860..bf9cd58 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@ https://github.com/gordielachance/plugin.audio.subsonic
## Installation
* Navigate to your `.kodi/addons/` folder
* Clone this repository: `git clone https://github.com/gordielachance/plugin.audio.subsonic.git`
-* Install the SimplePlugin dependency by cloning its repository: `git clone https://github.com/romanvm/script.module.simpleplugin.git`
* (Re)start Kodi.
## TODO
diff --git a/addon.xml b/addon.xml
index 01c1710..2910039 100644
--- a/addon.xml
+++ b/addon.xml
@@ -3,7 +3,6 @@
-
audio
diff --git a/lib/py-sonic/.gitignore b/lib/py-sonic/.gitignore
deleted file mode 100644
index 977ac6e..0000000
--- a/lib/py-sonic/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-*.pyc
-test.py
-MANIFEST
-dist
-build
diff --git a/lib/py-sonic/CHANGELOG.md b/lib/py-sonic/CHANGELOG.md
deleted file mode 100644
index a9c7e59..0000000
--- a/lib/py-sonic/CHANGELOG.md
+++ /dev/null
@@ -1,57 +0,0 @@
-## 0.6.2
-
-* Added an option to use GET requests, instead of the default POST requests
-
-## 0.6.1
-
-* Added `legacyAuth` option for pre-1.13.0 support
-
-## 0.6.0
-
-* Added API 1.14.0 support
-
-## 0.5.1
-
-* Added the ability to use a netrc file for credentials
-
-## 0.5.0
-
-* Added support for using credentials via a netrc file
-
-## 0.4.1
-
-* Fixed SSL handling issues
-
-## 0.4.0
-
-* Added missing 1.12.0 API items
-* Added 1.13.0 API items
-* All timestamps both passed in, and returned, should now be in **proper** unix time, which is seconds since the epoch, **not** milliseconds since the epoch
-
-## 0.3.5
-
-* allow for self-signed certs
-
-## 0.3.4
-
-* Add missing parameters to getAlbumList2 (thanks to basilfx)
-* Remove trailing whitespace (thanks to basilfx)
-
-## 0.3.3
-
-* Added support for API version 1.11.0
-* Added a couple of additions from API version 1.10.x that were previously
- missed
-
-## 0.3.1
-
-* Incorporated unofficial API calls (beallio)
-
-## 0.2.1
-
-* Added a patch to force SSLv3 as some users were apparently having issues
- with the 4.7 release of Subsonic and SSL. (thanks to orangepeelbeef)
-
-## 0.2.0
-
-* Added support for API version 1.8.0 (Subsonic verion 4.7)
diff --git a/lib/py-sonic/LICENSE b/lib/py-sonic/LICENSE
deleted file mode 100644
index 94a9ed0..0000000
--- a/lib/py-sonic/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- 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 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/lib/py-sonic/MANIFEST.in b/lib/py-sonic/MANIFEST.in
deleted file mode 100644
index 7f21ea1..0000000
--- a/lib/py-sonic/MANIFEST.in
+++ /dev/null
@@ -1,3 +0,0 @@
-include LICENSE
-include CHANGELOG.md
-include README.md
diff --git a/lib/py-sonic/README.md b/lib/py-sonic/README.md
deleted file mode 100644
index 42b3a0f..0000000
--- a/lib/py-sonic/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# py-sonic #
-
-## INSTALL ##
-
-Installation is fairly simple. Just do the standard install as root:
-
- tar -xvzf py-sonic-*.tar.gz
- cd py-sonic-*
- python setup.py install
-
-You can also install directly using *pip* or *easy_install*
-
- pip install py-sonic
-
-## PYTHON 3 ##
-
-I've added experimental support for Python 3. This is not fully tested, but I
-do encourage people to fork the repository and test off the "python3" branch
-and issue pull requests. If you don't want to go through that trouble, bug
-reports are always welcome too!
-
-## USAGE ##
-
-This library follows the REST API almost exactly (for now). If you follow the
-documentation on http://www.subsonic.org/pages/api.jsp or you do a:
-
- pydoc libsonic.connection
-
-I have also added documentation at http://stuffivelearned.org/doku.php?id=programming:python:py-sonic
-
-## BASIC TUTORIAL ##
-
-This is about as basic as it gets. We are just going to set up the connection
-and then get a couple of random songs.
-
-```python
-#!/usr/bin/env python
-
-from pprint import pprint
-import libsonic
-
-# We pass in the base url, the username, password, and port number
-# Be sure to use https:// if this is an ssl connection!
-conn = libsonic.Connection('https://music.example.com' , 'myuser' ,
- 'secretpass' , port=443)
-# Let's get 2 completely random songs
-songs = conn.getRandomSongs(size=2)
-# We'll just pretty print the results we got to the terminal
-pprint(songs)
-```
-
-As you can see, it's really pretty simple. If you use the documentation
-provided in the library:
-
- pydoc libsonic.connection
-
-or the api docs on subsonic.org (listed above), you should be able to make use
-of your server without too much trouble.
-
-Right now, only plain old dictionary structures are returned. The plan
-for a later release includes the following:
-
-* Proper object representations for Artist, Album, Song, etc.
-* Lazy access of members (the song objects aren't created until you want to
- do something with them)
-
-## TODO ##
-
-In the future, I would like to make this a little more "pythonic" and add
-some classes to wrap up the data returned from the server. Right now, the data
-is just returned in the form of a dict, but I would like to have actual
-Song, Album, Folder, etc. classes instead, or at least an alternative. For
-now, it works.
-
-*NOTE:* I've noticed a wart with the upstream Subsonic API wherein any
-of the Connection methods that would normally return a list of dictionary
-elements (getPlaylists() for example), will only return a dictionary if there
-is a single return element. I plan on changing this in py-sonic so that
-any methods of that nature will *always* return a list, even if there is
-only a single dict in the list.
diff --git a/lib/py-sonic/libsonic/__init__.py b/lib/py-sonic/libsonic/__init__.py
deleted file mode 100644
index 7832d82..0000000
--- a/lib/py-sonic/libsonic/__init__.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""
-This file is part of py-sonic.
-
-py-sonic is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-py-sonic 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 py-sonic. If not, see
-
-For information on method calls, see 'pydoc libsonic.connection'
-
-----------
-Basic example:
-----------
-
-import libsonic
-
-conn = libsonic.Connection('http://localhost' , 'admin' , 'password')
-print conn.ping()
-
-"""
-
-from connection import *
-
-__version__ = '0.6.2'
diff --git a/lib/py-sonic/libsonic/connection.py b/lib/py-sonic/libsonic/connection.py
deleted file mode 100644
index ec116ef..0000000
--- a/lib/py-sonic/libsonic/connection.py
+++ /dev/null
@@ -1,2770 +0,0 @@
-"""
-This file is part of py-sonic.
-
-py-sonic is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-py-sonic 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 py-sonic. If not, see
-"""
-
-from urllib import urlencode
-from .errors import *
-from pprint import pprint
-from cStringIO import StringIO
-from netrc import netrc
-from hashlib import md5
-import json, urllib2, httplib, logging, socket, ssl, sys, os
-
-API_VERSION = '1.14.0'
-
-logger = logging.getLogger(__name__)
-
-class HTTPSConnectionChain(httplib.HTTPSConnection):
- _preferred_ssl_protos = sorted([ p for p in dir(ssl)
- if p.startswith('PROTOCOL_') ], reverse=True)
- _ssl_working_proto = None
-
- def _create_sock(self):
- sock = socket.create_connection((self.host, self.port), self.timeout)
- if self._tunnel_host:
- self.sock = sock
- self._tunnel()
- return sock
-
- def connect(self):
- if self._ssl_working_proto is not None:
- # If we have a working proto, let's use that straight away
- logger.debug("Using known working proto: '%s'",
- self._ssl_working_proto)
- sock = self._create_sock()
- self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
- ssl_version=self._ssl_working_proto)
- return
-
- # Try connecting via the different SSL protos in preference order
- for proto_name in self._preferred_ssl_protos:
- sock = self._create_sock()
- proto = getattr(ssl, proto_name, None)
- try:
- self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
- ssl_version=proto)
- except:
- sock.close()
- else:
- # Cache the working ssl version
- HTTPSConnectionChain._ssl_working_proto = proto
- break
-
-
-class HTTPSHandlerChain(urllib2.HTTPSHandler):
- def https_open(self, req):
- return self.do_open(HTTPSConnectionChain, req)
-
-# install opener
-urllib2.install_opener(urllib2.build_opener(HTTPSHandlerChain()))
-
-class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
- """
- This class is used to override the default behavior of the
- HTTPRedirectHandler, which does *not* redirect POST data
- """
- def redirect_request(self, req, fp, code, msg, headers, newurl):
- m = req.get_method()
- if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
- or code in (301, 302, 303) and m == "POST"):
- newurl = newurl.replace(' ', '%20')
- newheaders = dict((k, v) for k, v in req.headers.items()
- if k.lower() not in ("content-length", "content-type")
- )
- data = None
- if req.has_data():
- data = req.get_data()
- return urllib2.Request(newurl,
- data=data,
- headers=newheaders,
- origin_req_host=req.get_origin_req_host(),
- unverifiable=True)
- else:
- raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
-
-class Connection(object):
- def __init__(self, baseUrl, username=None, password=None, port=4040,
- serverPath='/rest', appName='py-sonic', apiVersion=API_VERSION,
- insecure=False, useNetrc=None, legacyAuth=False, useGET=False):
- """
- This will create a connection to your subsonic server
-
- baseUrl:str The base url for your server. Be sure to use
- "https" for SSL connections. If you are using
- a port other than the default 4040, be sure to
- specify that with the port argument. Do *not*
- append it here.
-
- ex: http://subsonic.example.com
-
- If you are running subsonic under a different
- path, specify that with the "serverPath" arg,
- *not* here. For example, if your subsonic
- lives at:
-
- https://mydomain.com:8080/path/to/subsonic/rest
-
- You would set the following:
-
- baseUrl = "https://mydomain.com"
- port = 8080
- serverPath = "/path/to/subsonic/rest"
- username:str The username to use for the connection. This
- can be None if `useNetrc' is True (and you
- have a valid entry in your netrc file)
- password:str The password to use for the connection. This
- can be None if `useNetrc' is True (and you
- have a valid entry in your netrc file)
- port:int The port number to connect on. The default for
- unencrypted subsonic connections is 4040
- serverPath:str The base resource path for the subsonic views.
- This is useful if you have your subsonic server
- behind a proxy and the path that you are proxying
- is different from the default of '/rest'.
- Ex:
- serverPath='/path/to/subs'
-
- The full url that would be built then would be
- (assuming defaults and using "example.com" and
- you are using the "ping" view):
-
- http://example.com:4040/path/to/subs/ping.view
- appName:str The name of your application.
- apiVersion:str The API version you wish to use for your
- application. Subsonic will throw an error if you
- try to use/send an api version higher than what
- the server supports. See the Subsonic API docs
- to find the Subsonic version -> API version table.
- This is useful if you are connecting to an older
- version of Subsonic.
- insecure:bool This will allow you to use self signed
- certificates when connecting if set to True.
- useNetrc:str|bool You can either specify a specific netrc
- formatted file or True to use your default
- netrc file ($HOME/.netrc).
- legacyAuth:bool Use pre-1.13.0 API version authentication
- useGET:bool Use a GET request instead of the default POST
- request. This is not recommended as request
- URLs can get very long with some API calls
- """
- self._baseUrl = baseUrl
- self._hostname = baseUrl.split('://')[1].strip()
- self._username = username
- self._rawPass = password
- self._legacyAuth = legacyAuth
- self._useGET = useGET
-
- self._netrc = None
- if useNetrc is not None:
- self._process_netrc(useNetrc)
- elif username is None or password is None:
- raise CredentialError('You must specify either a username/password '
- 'combination or "useNetrc" must be either True or a string '
- 'representing a path to a netrc file')
-
- self._port = int(port)
- self._apiVersion = apiVersion
- self._appName = appName
- self._serverPath = serverPath.strip('/')
- self._insecure = insecure
- self._opener = self._getOpener(self._username, self._rawPass)
-
- # Properties
- def setBaseUrl(self, url):
- self._baseUrl = url
- self._opener = self._getOpener(self._username, self._rawPass)
- baseUrl = property(lambda s: s._baseUrl, setBaseUrl)
-
- def setPort(self, port):
- self._port = int(port)
- port = property(lambda s: s._port, setPort)
-
- def setUsername(self, username):
- self._username = username
- self._opener = self._getOpener(self._username, self._rawPass)
- username = property(lambda s: s._username, setUsername)
-
- def setPassword(self, password):
- self._rawPass = password
- # Redo the opener with the new creds
- self._opener = self._getOpener(self._username, self._rawPass)
- password = property(lambda s: s._rawPass, setPassword)
-
- apiVersion = property(lambda s: s._apiVersion)
-
- def setAppName(self, appName):
- self._appName = appName
- appName = property(lambda s: s._appName, setAppName)
-
- def setServerPath(self, path):
- self._serverPath = path.strip('/')
- serverPath = property(lambda s: s._serverPath, setServerPath)
-
- def setInsecure(self, insecure):
- self._insecure = insecure
- insecure = property(lambda s: s._insecure, setInsecure)
-
- def setLegacyAuth(self, lauth):
- self._legacyAuth = lauth
- legacyAuth = property(lambda s: s._legacyAuth, setLegacyAuth)
-
- def setGET(self, get):
- self._useGET = get
- useGET = property(lambda s: s._useGET, setGET)
-
- # API methods
- def ping(self):
- """
- since: 1.0.0
-
- Returns a boolean True if the server is alive, False otherwise
- """
- methodName = 'ping'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- try:
- res = self._doInfoReq(req)
- except:
- return False
- if res['status'] == 'ok':
- return True
- elif res['status'] == 'failed':
- exc = getExcByCode(res['error']['code'])
- raise exc(res['error']['message'])
- return False
-
- def getLicense(self):
- """
- since: 1.0.0
-
- Gets details related to the software license
-
- Returns a dict like the following:
-
- {u'license': {u'date': u'2010-05-21T11:14:39',
- u'email': u'email@example.com',
- u'key': u'12345678901234567890123456789012',
- u'valid': True},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getLicense'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getMusicFolders(self):
- """
- since: 1.0.0
-
- Returns all configured music folders
-
- Returns a dict like the following:
-
- {u'musicFolders': {u'musicFolder': [{u'id': 0, u'name': u'folder1'},
- {u'id': 1, u'name': u'folder2'},
- {u'id': 2, u'name': u'folder3'}]},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getMusicFolders'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getNowPlaying(self):
- """
- since: 1.0.0
-
- Returns what is currently being played by all users
-
- Returns a dict like the following:
-
- {u'nowPlaying': {u'entry': {u'album': u"Jazz 'Round Midnight 12",
- u'artist': u'Astrud Gilberto',
- u'bitRate': 172,
- u'contentType': u'audio/mpeg',
- u'coverArt': u'98349284',
- u'duration': 325,
- u'genre': u'Jazz',
- u'id': u'2424324',
- u'isDir': False,
- u'isVideo': False,
- u'minutesAgo': 0,
- u'parent': u'542352',
- u'path': u"Astrud Gilberto/Jazz 'Round Midnight 12/01 - The Girl From Ipanema.mp3",
- u'playerId': 1,
- u'size': 7004089,
- u'suffix': u'mp3',
- u'title': u'The Girl From Ipanema',
- u'track': 1,
- u'username': u'user1',
- u'year': 1996}},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getNowPlaying'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getIndexes(self, musicFolderId=None, ifModifiedSince=0):
- """
- since: 1.0.0
-
- Returns an indexed structure of all artists
-
- musicFolderId:int If this is specified, it will only return
- artists for the given folder ID from
- the getMusicFolders call
- ifModifiedSince:int If specified, return a result if the artist
- collection has changed since the given
- unix timestamp
-
- Returns a dict like the following:
-
- {u'indexes': {u'index': [{u'artist': [{u'id': u'29834728934',
- u'name': u'A Perfect Circle'},
- {u'id': u'238472893',
- u'name': u'A Small Good Thing'},
- {u'id': u'9327842983',
- u'name': u'A Tribe Called Quest'},
- {u'id': u'29348729874',
- u'name': u'A-Teens, The'},
- {u'id': u'298472938',
- u'name': u'ABA STRUCTURE'}],
- u'lastModified': 1303318347000L},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getIndexes'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'musicFolderId': musicFolderId,
- 'ifModifiedSince': self._ts2milli(ifModifiedSince)})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- self._fixLastModified(res)
- return res
-
- def getMusicDirectory(self, mid):
- """
- since: 1.0.0
-
- Returns a listing of all files in a music directory. Typically used
- to get a list of albums for an artist or list of songs for an album.
-
- mid:str The string ID value which uniquely identifies the
- folder. Obtained via calls to getIndexes or
- getMusicDirectory. REQUIRED
-
- Returns a dict like the following:
-
- {u'directory': {u'child': [{u'artist': u'A Tribe Called Quest',
- u'coverArt': u'223484',
- u'id': u'329084',
- u'isDir': True,
- u'parent': u'234823940',
- u'title': u'Beats, Rhymes And Life'},
- {u'artist': u'A Tribe Called Quest',
- u'coverArt': u'234823794',
- u'id': u'238472893',
- u'isDir': True,
- u'parent': u'2308472938',
- u'title': u'Midnight Marauders'},
- {u'artist': u'A Tribe Called Quest',
- u'coverArt': u'39284792374',
- u'id': u'983274892',
- u'isDir': True,
- u'parent': u'9823749',
- u'title': u"People's Instinctive Travels And The Paths Of Rhythm"},
- {u'artist': u'A Tribe Called Quest',
- u'coverArt': u'289347293',
- u'id': u'3894723934',
- u'isDir': True,
- u'parent': u'9832942',
- u'title': u'The Anthology'},
- {u'artist': u'A Tribe Called Quest',
- u'coverArt': u'923847923',
- u'id': u'29834729',
- u'isDir': True,
- u'parent': u'2934872893',
- u'title': u'The Love Movement'},
- {u'artist': u'A Tribe Called Quest',
- u'coverArt': u'9238742893',
- u'id': u'238947293',
- u'isDir': True,
- u'parent': u'9432878492',
- u'title': u'The Low End Theory'}],
- u'id': u'329847293',
- u'name': u'A Tribe Called Quest'},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getMusicDirectory'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName, {'id': mid})
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def search(self, artist=None, album=None, title=None, any=None,
- count=20, offset=0, newerThan=None):
- """
- since: 1.0.0
-
- DEPRECATED SINCE API 1.4.0! USE search2() INSTEAD!
-
- Returns a listing of files matching the given search criteria.
- Supports paging with offset
-
- artist:str Search for artist
- album:str Search for album
- title:str Search for title of song
- any:str Search all fields
- count:int Max number of results to return [default: 20]
- offset:int Search result offset. For paging [default: 0]
- newerThan:int Return matches newer than this timestamp
- """
- if artist == album == title == any == None:
- raise ArgumentError('Invalid search. You must supply search '
- 'criteria')
- methodName = 'search'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'artist': artist, 'album': album,
- 'title': title, 'any': any, 'count': count, 'offset': offset,
- 'newerThan': self._ts2milli(newerThan)})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def search2(self, query, artistCount=20, artistOffset=0, albumCount=20,
- albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
- """
- since: 1.4.0
-
- Returns albums, artists and songs matching the given search criteria.
- Supports paging through the result.
-
- query:str The search query
- artistCount:int Max number of artists to return [default: 20]
- artistOffset:int Search offset for artists (for paging) [default: 0]
- albumCount:int Max number of albums to return [default: 20]
- albumOffset:int Search offset for albums (for paging) [default: 0]
- songCount:int Max number of songs to return [default: 20]
- songOffset:int Search offset for songs (for paging) [default: 0]
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
-
- Returns a dict like the following:
-
- {u'searchResult2': {u'album': [{u'artist': u'A Tribe Called Quest',
- u'coverArt': u'289347',
- u'id': u'32487298',
- u'isDir': True,
- u'parent': u'98374289',
- u'title': u'The Love Movement'}],
- u'artist': [{u'id': u'2947839',
- u'name': u'A Tribe Called Quest'},
- {u'id': u'239847239',
- u'name': u'Tribe'}],
- u'song': [{u'album': u'Beats, Rhymes And Life',
- u'artist': u'A Tribe Called Quest',
- u'bitRate': 224,
- u'contentType': u'audio/mpeg',
- u'coverArt': u'329847',
- u'duration': 148,
- u'genre': u'default',
- u'id': u'3928472893',
- u'isDir': False,
- u'isVideo': False,
- u'parent': u'23984728394',
- u'path': u'A Tribe Called Quest/Beats, Rhymes And Life/A Tribe Called Quest - Beats, Rhymes And Life - 03 - Motivators.mp3',
- u'size': 4171913,
- u'suffix': u'mp3',
- u'title': u'Motivators',
- u'track': 3}]},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'search2'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'query': query, 'artistCount': artistCount,
- 'artistOffset': artistOffset, 'albumCount': albumCount,
- 'albumOffset': albumOffset, 'songCount': songCount,
- 'songOffset': songOffset, 'musicFolderId': musicFolderId})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def search3(self, query, artistCount=20, artistOffset=0, albumCount=20,
- albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
- """
- since: 1.8.0
-
- Works the same way as search2, but uses ID3 tags for
- organization
-
- query:str The search query
- artistCount:int Max number of artists to return [default: 20]
- artistOffset:int Search offset for artists (for paging) [default: 0]
- albumCount:int Max number of albums to return [default: 20]
- albumOffset:int Search offset for albums (for paging) [default: 0]
- songCount:int Max number of songs to return [default: 20]
- songOffset:int Search offset for songs (for paging) [default: 0]
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
-
- Returns a dict like the following (search for "Tune Yards":
- {u'searchResult3': {u'album': [{u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'coverArt': u'al-7',
- u'created': u'2012-01-30T12:35:33',
- u'duration': 3229,
- u'id': 7,
- u'name': u'Bird-Brains',
- u'songCount': 13},
- {u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'coverArt': u'al-8',
- u'created': u'2011-03-22T15:08:00',
- u'duration': 2531,
- u'id': 8,
- u'name': u'W H O K I L L',
- u'songCount': 10}],
- u'artist': {u'albumCount': 2,
- u'coverArt': u'ar-1',
- u'id': 1,
- u'name': u'Tune-Yards'},
- u'song': [{u'album': u'Bird-Brains',
- u'albumId': 7,
- u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'bitRate': 160,
- u'contentType': u'audio/mpeg',
- u'coverArt': 105,
- u'created': u'2012-01-30T12:35:33',
- u'duration': 328,
- u'genre': u'Lo-Fi',
- u'id': 107,
- u'isDir': False,
- u'isVideo': False,
- u'parent': 105,
- u'path': u'Tune Yards/Bird-Brains/10-tune-yards-fiya.mp3',
- u'size': 6588498,
- u'suffix': u'mp3',
- u'title': u'Fiya',
- u'track': 10,
- u'type': u'music',
- u'year': 2009}]},
-
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'search3'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'query': query, 'artistCount': artistCount,
- 'artistOffset': artistOffset, 'albumCount': albumCount,
- 'albumOffset': albumOffset, 'songCount': songCount,
- 'songOffset': songOffset, 'musicFolderId': musicFolderId})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getPlaylists(self, username=None):
- """
- since: 1.0.0
-
- Returns the ID and name of all saved playlists
- The "username" option was added in 1.8.0.
-
- username:str If specified, return playlists for this user
- rather than for the authenticated user. The
- authenticated user must have admin role
- if this parameter is used
-
- Returns a dict like the following:
-
- {u'playlists': {u'playlist': [{u'id': u'62656174732e6d3375',
- u'name': u'beats'},
- {u'id': u'766172696574792e6d3375',
- u'name': u'variety'}]},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getPlaylists'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'username': username})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getPlaylist(self, pid):
- """
- since: 1.0.0
-
- Returns a listing of files in a saved playlist
-
- id:str The ID of the playlist as returned in getPlaylists()
-
- Returns a dict like the following:
-
- {u'playlist': {u'entry': {u'album': u'The Essential Bob Dylan',
- u'artist': u'Bob Dylan',
- u'bitRate': 32,
- u'contentType': u'audio/mpeg',
- u'coverArt': u'2983478293',
- u'duration': 984,
- u'genre': u'Classic Rock',
- u'id': u'982739428',
- u'isDir': False,
- u'isVideo': False,
- u'parent': u'98327428974',
- u'path': u"Bob Dylan/Essential Bob Dylan Disc 1/Bob Dylan - The Essential Bob Dylan - 03 - The Times They Are A-Changin'.mp3",
- u'size': 3921899,
- u'suffix': u'mp3',
- u'title': u"The Times They Are A-Changin'",
- u'track': 3},
- u'id': u'44796c616e2e6d3375',
- u'name': u'Dylan'},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getPlaylist'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName, {'id': pid})
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def createPlaylist(self, playlistId=None, name=None, songIds=[]):
- """
- since: 1.2.0
-
- Creates OR updates a playlist. If updating the list, the
- playlistId is required. If creating a list, the name is required.
-
- playlistId:str The ID of the playlist to UPDATE
- name:str The name of the playlist to CREATE
- songIds:list The list of songIds to populate the list with in
- either create or update mode. Note that this
- list will replace the existing list if updating
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'createPlaylist'
- viewName = '%s.view' % methodName
-
- if playlistId == name == None:
- raise ArgumentError('You must supply either a playlistId or a name')
- if playlistId is not None and name is not None:
- raise ArgumentError('You can only supply either a playlistId '
- 'OR a name, not both')
-
- q = self._getQueryDict({'playlistId': playlistId, 'name': name})
-
- req = self._getRequestWithList(viewName, 'songId', songIds, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deletePlaylist(self, pid):
- """
- since: 1.2.0
-
- Deletes a saved playlist
-
- pid:str ID of the playlist to delete, as obtained by getPlaylists
-
- Returns a dict like the following:
-
- """
- methodName = 'deletePlaylist'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName, {'id': pid})
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def download(self, sid):
- """
- since: 1.0.0
-
- Downloads a given music file.
-
- sid:str The ID of the music file to download.
-
- Returns the file-like object for reading or raises an exception
- on error
- """
- methodName = 'download'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName, {'id': sid})
- res = self._doBinReq(req)
- if isinstance(res, dict):
- self._checkStatus(res)
- return res
-
- def stream(self, sid, maxBitRate=0, tformat=None, timeOffset=None,
- size=None, estimateContentLength=False, converted=False):
- """
- since: 1.0.0
-
- Downloads a given music file.
-
- sid:str The ID of the music file to download.
- maxBitRate:int (since: 1.2.0) If specified, the server will
- attempt to limit the bitrate to this value, in
- kilobits per second. If set to zero (default), no limit
- is imposed. Legal values are: 0, 32, 40, 48, 56, 64,
- 80, 96, 112, 128, 160, 192, 224, 256 and 320.
- tformat:str (since: 1.6.0) Specifies the target format
- (e.g. "mp3" or "flv") in case there are multiple
- applicable transcodings (since: 1.9.0) You can use
- the special value "raw" to disable transcoding
- timeOffset:int (since: 1.6.0) Only applicable to video
- streaming. Start the stream at the given
- offset (in seconds) into the video
- size:str (since: 1.6.0) The requested video size in
- WxH, for instance 640x480
- estimateContentLength:bool (since: 1.8.0) If set to True,
- the HTTP Content-Length header
- will be set to an estimated
- value for trancoded media
- converted:bool (since: 1.14.0) Only applicable to video streaming.
- Subsonic can optimize videos for streaming by
- converting them to MP4. If a conversion exists for
- the video in question, then setting this parameter
- to "true" will cause the converted video to be
- returned instead of the original.
-
- Returns the file-like object for reading or raises an exception
- on error
- """
- methodName = 'stream'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': sid, 'maxBitRate': maxBitRate,
- 'format': tformat, 'timeOffset': timeOffset, 'size': size,
- 'estimateContentLength': estimateContentLength,
- 'converted': converted})
-
- req = self._getRequest(viewName, q)
- res = self._doBinReq(req)
- if isinstance(res, dict):
- self._checkStatus(res)
- return res
-
- def getCoverArt(self, aid, size=None):
- """
- since: 1.0.0
-
- Returns a cover art image
-
- aid:str ID string for the cover art image to download
- size:int If specified, scale image to this size
-
- Returns the file-like object for reading or raises an exception
- on error
- """
- methodName = 'getCoverArt'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': aid, 'size': size})
-
- req = self._getRequest(viewName, q)
- res = self._doBinReq(req)
- if isinstance(res, dict):
- self._checkStatus(res)
- return res
-
- def scrobble(self, sid, submission=True, listenTime=None):
- """
- since: 1.5.0
-
- "Scrobbles" a given music file on last.fm. Requires that the user
- has set this up.
-
- Since 1.8.0 you may specify multiple id (and optionally time)
- parameters to scrobble multiple files.
-
- Since 1.11.0 this method will also update the play count and
- last played timestamp for the song and album. It will also make
- the song appear in the "Now playing" page in the web app, and
- appear in the list of songs returned by getNowPlaying
-
- sid:str The ID of the file to scrobble
- submission:bool Whether this is a "submission" or a "now playing"
- notification
- listenTime:int (Since 1.8.0) The time (unix timestamp) at
- which the song was listened to.
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'scrobble'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': sid, 'submission': submission,
- 'time': self._ts2milli(listenTime)})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def changePassword(self, username, password):
- """
- since: 1.1.0
-
- Changes the password of an existing Subsonic user. Note that the
- user performing this must have admin privileges
-
- username:str The username whose password is being changed
- password:str The new password of the user
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'changePassword'
- viewName = '%s.view' % methodName
- hexPass = 'enc:%s' % self._hexEnc(password)
-
- # There seems to be an issue with some subsonic implementations
- # not recognizing the "enc:" precursor to the encoded password and
- # encodes the whole "enc:" as the password. Weird.
- #q = {'username': username, 'password': hexPass.lower()}
- q = {'username': username, 'password': password}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getUser(self, username):
- """
- since: 1.3.0
-
- Get details about a given user, including which auth roles it has.
- Can be used to enable/disable certain features in the client, such
- as jukebox control
-
- username:str The username to retrieve. You can only retrieve
- your own user unless you have admin privs.
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'user': {u'adminRole': False,
- u'commentRole': False,
- u'coverArtRole': False,
- u'downloadRole': True,
- u'jukeboxRole': False,
- u'playlistRole': True,
- u'podcastRole': False,
- u'settingsRole': True,
- u'streamRole': True,
- u'uploadRole': True,
- u'username': u'test'},
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getUser'
- viewName = '%s.view' % methodName
-
- q = {'username': username}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getUsers(self):
- """
- since 1.8.0
-
- Gets a list of users
-
- returns a dict like the following
-
- {u'status': u'ok',
- u'users': {u'user': [{u'adminRole': True,
- u'commentRole': True,
- u'coverArtRole': True,
- u'downloadRole': True,
- u'jukeboxRole': True,
- u'playlistRole': True,
- u'podcastRole': True,
- u'scrobblingEnabled': True,
- u'settingsRole': True,
- u'shareRole': True,
- u'streamRole': True,
- u'uploadRole': True,
- u'username': u'user1'},
- ...
- ...
- ]},
- u'version': u'1.10.2',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getUsers'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def createUser(self, username, password, email,
- ldapAuthenticated=False, adminRole=False, settingsRole=True,
- streamRole=True, jukeboxRole=False, downloadRole=False,
- uploadRole=False, playlistRole=False, coverArtRole=False,
- commentRole=False, podcastRole=False, shareRole=False,
- musicFolderId=None):
- """
- since: 1.1.0
-
- Creates a new subsonic user, using the parameters defined. See the
- documentation at http://subsonic.org for more info on all the roles.
-
- username:str The username of the new user
- password:str The password for the new user
- email:str The email of the new user
-
- musicFolderId:int These are the only folders the user has access to
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'createUser'
- viewName = '%s.view' % methodName
- hexPass = 'enc:%s' % self._hexEnc(password)
-
- q = self._getQueryDict({
- 'username': username, 'password': hexPass, 'email': email,
- 'ldapAuthenticated': ldapAuthenticated, 'adminRole': adminRole,
- 'settingsRole': settingsRole, 'streamRole': streamRole,
- 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
- 'uploadRole': uploadRole, 'playlistRole': playlistRole,
- 'coverArtRole': coverArtRole, 'commentRole': commentRole,
- 'podcastRole': podcastRole, 'shareRole': shareRole,
- 'musicFolderId': musicFolderId
- })
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def updateUser(self, username, password=None, email=None,
- ldapAuthenticated=False, adminRole=False, settingsRole=True,
- streamRole=True, jukeboxRole=False, downloadRole=False,
- uploadRole=False, playlistRole=False, coverArtRole=False,
- commentRole=False, podcastRole=False, shareRole=False,
- musicFolderId=None, maxBitRate=0):
- """
- since 1.10.1
-
- Modifies an existing Subsonic user.
-
- username:str The username of the user to update.
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
- maxBitRate:int The max bitrate for the user. 0 is unlimited
-
- All other args are the same as create user and you can update
- whatever item you wish to update for the given username.
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'updateUser'
- viewName = '%s.view' % methodName
- if password is not None:
- password = 'enc:%s' % self._hexEnc(password)
- q = self._getQueryDict({'username': username, 'password': password,
- 'email': email, 'ldapAuthenticated': ldapAuthenticated,
- 'adminRole': adminRole,
- 'settingsRole': settingsRole, 'streamRole': streamRole,
- 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
- 'uploadRole': uploadRole, 'playlistRole': playlistRole,
- 'coverArtRole': coverArtRole, 'commentRole': commentRole,
- 'podcastRole': podcastRole, 'shareRole': shareRole,
- 'musicFolderId': musicFolderId, 'maxBitRate': maxBitRate
- })
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deleteUser(self, username):
- """
- since: 1.3.0
-
- Deletes an existing Subsonic user. Of course, you must have admin
- rights for this.
-
- username:str The username of the user to delete
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'deleteUser'
- viewName = '%s.view' % methodName
-
- q = {'username': username}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getChatMessages(self, since=1):
- """
- since: 1.2.0
-
- Returns the current visible (non-expired) chat messages.
-
- since:int Only return messages newer than this timestamp
-
- NOTE: All times returned are in MILLISECONDS since the Epoch, not
- seconds!
-
- Returns a dict like the following:
- {u'chatMessages': {u'chatMessage': {u'message': u'testing 123',
- u'time': 1303411919872L,
- u'username': u'admin'}},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getChatMessages'
- viewName = '%s.view' % methodName
-
- q = {'since': self._ts2milli(since)}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def addChatMessage(self, message):
- """
- since: 1.2.0
-
- Adds a message to the chat log
-
- message:str The message to add
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'addChatMessage'
- viewName = '%s.view' % methodName
-
- q = {'message': message}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAlbumList(self, ltype, size=10, offset=0, fromYear=None,
- toYear=None, genre=None, musicFolderId=None):
- """
- since: 1.2.0
-
- Returns a list of random, newest, highest rated etc. albums.
- Similar to the album lists on the home page of the Subsonic
- web interface
-
- ltype:str The list type. Must be one of the following: random,
- newest, highest, frequent, recent,
- (since 1.8.0 -> )starred, alphabeticalByName,
- alphabeticalByArtist
- Since 1.10.1 you can use byYear and byGenre to
- list albums in a given year range or genre.
- size:int The number of albums to return. Max 500
- offset:int The list offset. Use for paging. Max 5000
- fromYear:int If you specify the ltype as "byYear", you *must*
- specify fromYear
- toYear:int If you specify the ltype as "byYear", you *must*
- specify toYear
- genre:str The name of the genre e.g. "Rock". You must specify
- genre if you set the ltype to "byGenre"
- musicFolderId:str Only return albums in the music folder with
- the given ID. See getMusicFolders()
-
- Returns a dict like the following:
-
- {u'albumList': {u'album': [{u'artist': u'Hank Williams',
- u'id': u'3264928374',
- u'isDir': True,
- u'parent': u'9238479283',
- u'title': u'The Original Singles Collection...Plus'},
- {u'artist': u'Freundeskreis',
- u'coverArt': u'9823749823',
- u'id': u'23492834',
- u'isDir': True,
- u'parent': u'9827492374',
- u'title': u'Quadratur des Kreises'}]},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getAlbumList'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'type': ltype, 'size': size,
- 'offset': offset, 'fromYear': fromYear, 'toYear': toYear,
- 'genre': genre, 'musicFolderId': musicFolderId})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAlbumList2(self, ltype, size=10, offset=0, fromYear=None,
- toYear=None, genre=None):
- """
- since 1.8.0
-
- Returns a list of random, newest, highest rated etc. albums.
- This is similar to getAlbumList, but uses ID3 tags for
- organization
-
- ltype:str The list type. Must be one of the following: random,
- newest, highest, frequent, recent,
- (since 1.8.0 -> )starred, alphabeticalByName,
- alphabeticalByArtist
- Since 1.10.1 you can use byYear and byGenre to
- list albums in a given year range or genre.
- size:int The number of albums to return. Max 500
- offset:int The list offset. Use for paging. Max 5000
- fromYear:int If you specify the ltype as "byYear", you *must*
- specify fromYear
- toYear:int If you specify the ltype as "byYear", you *must*
- specify toYear
- genre:str The name of the genre e.g. "Rock". You must specify
- genre if you set the ltype to "byGenre"
-
- Returns a dict like the following:
- {u'albumList2': {u'album': [{u'artist': u'Massive Attack',
- u'artistId': 0,
- u'coverArt': u'al-0',
- u'created': u'2009-08-28T10:00:44',
- u'duration': 3762,
- u'id': 0,
- u'name': u'100th Window',
- u'songCount': 9},
- {u'artist': u'Massive Attack',
- u'artistId': 0,
- u'coverArt': u'al-5',
- u'created': u'2003-11-03T22:00:00',
- u'duration': 2715,
- u'id': 5,
- u'name': u'Blue Lines',
- u'songCount': 9}]},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getAlbumList2'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'type': ltype, 'size': size,
- 'offset': offset, 'fromYear': fromYear, 'toYear': toYear,
- 'genre': genre})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getRandomSongs(self, size=10, genre=None, fromYear=None,
- toYear=None, musicFolderId=None):
- """
- since 1.2.0
-
- Returns random songs matching the given criteria
-
- size:int The max number of songs to return. Max 500
- genre:str Only return songs from this genre
- fromYear:int Only return songs after or in this year
- toYear:int Only return songs before or in this year
- musicFolderId:str Only return songs in the music folder with the
- given ID. See getMusicFolders
-
- Returns a dict like the following:
-
- {u'randomSongs': {u'song': [{u'album': u'1998 EP - Airbag (How Am I Driving)',
- u'artist': u'Radiohead',
- u'bitRate': 320,
- u'contentType': u'audio/mpeg',
- u'duration': 129,
- u'id': u'9284728934',
- u'isDir': False,
- u'isVideo': False,
- u'parent': u'983249823',
- u'path': u'Radiohead/1998 EP - Airbag (How Am I Driving)/06 - Melatonin.mp3',
- u'size': 5177469,
- u'suffix': u'mp3',
- u'title': u'Melatonin'},
- {u'album': u'Mezmerize',
- u'artist': u'System Of A Down',
- u'bitRate': 214,
- u'contentType': u'audio/mpeg',
- u'coverArt': u'23849372894',
- u'duration': 176,
- u'id': u'28937492834',
- u'isDir': False,
- u'isVideo': False,
- u'parent': u'92837492837',
- u'path': u'System Of A Down/Mesmerize/10 - System Of A Down - Old School Hollywood.mp3',
- u'size': 4751360,
- u'suffix': u'mp3',
- u'title': u'Old School Hollywood',
- u'track': 10}]},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getRandomSongs'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'size': size, 'genre': genre,
- 'fromYear': fromYear, 'toYear': toYear,
- 'musicFolderId': musicFolderId})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getLyrics(self, artist=None, title=None):
- """
- since: 1.2.0
-
- Searches for and returns lyrics for a given song
-
- artist:str The artist name
- title:str The song title
-
- Returns a dict like the following for
- getLyrics('Bob Dylan', 'Blowin in the wind'):
-
- {u'lyrics': {u'artist': u'Bob Dylan',
- u'content': u"How many roads must a man walk down",
- u'title': u"Blowin' in the Wind"},
- u'status': u'ok',
- u'version': u'1.5.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getLyrics'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'artist': artist, 'title': title})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def jukeboxControl(self, action, index=None, sids=[], gain=None,
- offset=None):
- """
- since: 1.2.0
-
- NOTE: Some options were added as of API version 1.7.0
-
- Controls the jukebox, i.e., playback directly on the server's
- audio hardware. Note: The user must be authorized to control
- the jukebox
-
- action:str The operation to perform. Must be one of: get,
- start, stop, skip, add, clear, remove, shuffle,
- setGain, status (added in API 1.7.0),
- set (added in API 1.7.0)
- index:int Used by skip and remove. Zero-based index of the
- song to skip to or remove.
- sids:str Used by "add" and "set". ID of song to add to the
- jukebox playlist. Use multiple id parameters to
- add many songs in the same request. Whether you
- are passing one song or many into this, this
- parameter MUST be a list
- gain:float Used by setGain to control the playback volume.
- A float value between 0.0 and 1.0
- offset:int (added in API 1.7.0) Used by "skip". Start playing
- this many seconds into the track.
- """
- methodName = 'jukeboxControl'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'action': action, 'index': index,
- 'gain': gain, 'offset': offset})
-
- req = None
- if action == 'add':
- # We have to deal with the sids
- if not (isinstance(sids, list) or isinstance(sids, tuple)):
- raise ArgumentError('If you are adding songs, "sids" must '
- 'be a list or tuple!')
- req = self._getRequestWithList(viewName, 'id', sids, q)
- else:
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getPodcasts(self, incEpisodes=True, pid=None):
- """
- since: 1.6.0
-
- Returns all podcast channels the server subscribes to and their
- episodes.
-
- incEpisodes:bool (since: 1.9.0) Whether to include Podcast
- episodes in the returned result.
- pid:str (since: 1.9.0) If specified, only return
- the Podcast channel with this ID.
-
- Returns a dict like the following:
- {u'status': u'ok',
- u'version': u'1.6.0',
- u'xmlns': u'http://subsonic.org/restapi',
- u'podcasts': {u'channel': {u'description': u"Dr Chris Smith...",
- u'episode': [{u'album': u'Dr Karl and the Naked Scientist',
- u'artist': u'BBC Radio 5 live',
- u'bitRate': 64,
- u'contentType': u'audio/mpeg',
- u'coverArt': u'2f6f7074',
- u'description': u'Dr Karl answers all your science related questions.',
- u'duration': 2902,
- u'genre': u'Podcast',
- u'id': 0,
- u'isDir': False,
- u'isVideo': False,
- u'parent': u'2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e74697374',
- u'publishDate': u'2011-08-17 22:06:00.0',
- u'size': 23313059,
- u'status': u'completed',
- u'streamId': u'2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e746973742f64726b61726c5f32303131303831382d30343036612e6d7033',
- u'suffix': u'mp3',
- u'title': u'DrKarl: Peppermints, Chillies & Receptors',
- u'year': 2011},
- {u'description': u'which is warmer, a bath with bubbles in it or one without? Just one of the stranger science stories tackled this week by Dr Chris Smith and the Naked Scientists!',
- u'id': 1,
- u'publishDate': u'2011-08-14 21:05:00.0',
- u'status': u'skipped',
- u'title': u'DrKarl: how many bubbles in your bath? 15 AUG 11'},
- ...
- {u'description': u'Dr Karl joins Rhod to answer all your science questions',
- u'id': 9,
- u'publishDate': u'2011-07-06 22:12:00.0',
- u'status': u'skipped',
- u'title': u'DrKarl: 8 Jul 11 The Strange Sound of the MRI Scanner'}],
- u'id': 0,
- u'status': u'completed',
- u'title': u'Dr Karl and the Naked Scientist',
- u'url': u'http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml'}}
- }
-
- See also: http://subsonic.svn.sourceforge.net/viewvc/subsonic/trunk/subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml?view=markup
- """
- methodName = 'getPodcasts'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'includeEpisodes': incEpisodes,
- 'id': pid})
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getShares(self):
- """
- since: 1.6.0
-
- Returns information about shared media this user is allowed to manage
-
- Note that entry can be either a single dict or a list of dicts
-
- Returns a dict like the following:
-
- {u'status': u'ok',
- u'version': u'1.6.0',
- u'xmlns': u'http://subsonic.org/restapi',
- u'shares': {u'share': [
- {u'created': u'2011-08-18T10:01:35',
- u'entry': {u'artist': u'Alice In Chains',
- u'coverArt': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e732f636f7665722e6a7067',
- u'id': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e73',
- u'isDir': True,
- u'parent': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e73',
- u'title': u'Alice In Chains'},
- u'expires': u'2012-08-18T10:01:35',
- u'id': 0,
- u'url': u'http://crustymonkey.subsonic.org/share/BuLbF',
- u'username': u'admin',
- u'visitCount': 0
- }]}
- }
- """
- methodName = 'getShares'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def createShare(self, shids=[], description=None, expires=None):
- """
- since: 1.6.0
-
- Creates a public URL that can be used by anyone to stream music
- or video from the Subsonic server. The URL is short and suitable
- for posting on Facebook, Twitter etc. Note: The user must be
- authorized to share (see Settings > Users > User is allowed to
- share files with anyone).
-
- shids:list[str] A list of ids of songs, albums or videos
- to share.
- description:str A description that will be displayed to
- people visiting the shared media
- (optional).
- expires:float A timestamp pertaining to the time at
- which this should expire (optional)
-
- This returns a structure like you would get back from getShares()
- containing just your new share.
- """
- methodName = 'createShare'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'description': description,
- 'expires': self._ts2milli(expires)})
- req = self._getRequestWithList(viewName, 'id', shids, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def updateShare(self, shid, description=None, expires=None):
- """
- since: 1.6.0
-
- Updates the description and/or expiration date for an existing share
-
- shid:str The id of the share to update
- description:str The new description for the share (optional).
- expires:float The new timestamp for the expiration time of this
- share (optional).
- """
- methodName = 'updateShare'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': shid, 'description': description,
- expires: self._ts2milli(expires)})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deleteShare(self, shid):
- """
- since: 1.6.0
-
- Deletes an existing share
-
- shid:str The id of the share to delete
-
- Returns a standard response dict
- """
- methodName = 'deleteShare'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': shid})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def setRating(self, id, rating):
- """
- since: 1.6.0
-
- Sets the rating for a music file
-
- id:str The id of the item (song/artist/album) to rate
- rating:int The rating between 1 and 5 (inclusive), or 0 to remove
- the rating
-
- Returns a standard response dict
- """
- methodName = 'setRating'
- viewName = '%s.view' % methodName
-
- try:
- rating = int(rating)
- except:
- raise ArgumentError('Rating must be an integer between 0 and 5: '
- '%r' % rating)
- if rating < 0 or rating > 5:
- raise ArgumentError('Rating must be an integer between 0 and 5: '
- '%r' % rating)
-
- q = self._getQueryDict({'id': id, 'rating': rating})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getArtists(self):
- """
- since 1.8.0
-
- Similar to getIndexes(), but this method uses the ID3 tags to
- determine the artist
-
- Returns a dict like the following:
- {u'artists': {u'index': [{u'artist': {u'albumCount': 7,
- u'coverArt': u'ar-0',
- u'id': 0,
- u'name': u'Massive Attack'},
- u'name': u'M'},
- {u'artist': {u'albumCount': 2,
- u'coverArt': u'ar-1',
- u'id': 1,
- u'name': u'Tune-Yards'},
- u'name': u'T'}]},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getArtists'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getArtist(self, id):
- """
- since 1.8.0
-
- Returns the info (albums) for an artist. This method uses
- the ID3 tags for organization
-
- id:str The artist ID
-
- Returns a dict like the following:
-
- {u'artist': {u'album': [{u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'coverArt': u'al-7',
- u'created': u'2012-01-30T12:35:33',
- u'duration': 3229,
- u'id': 7,
- u'name': u'Bird-Brains',
- u'songCount': 13},
- {u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'coverArt': u'al-8',
- u'created': u'2011-03-22T15:08:00',
- u'duration': 2531,
- u'id': 8,
- u'name': u'W H O K I L L',
- u'songCount': 10}],
- u'albumCount': 2,
- u'coverArt': u'ar-1',
- u'id': 1,
- u'name': u'Tune-Yards'},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getArtist'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': id})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAlbum(self, id):
- """
- since 1.8.0
-
- Returns the info and songs for an album. This method uses
- the ID3 tags for organization
-
- id:str The album ID
-
- Returns a dict like the following:
-
- {u'album': {u'artist': u'Massive Attack',
- u'artistId': 0,
- u'coverArt': u'al-0',
- u'created': u'2009-08-28T10:00:44',
- u'duration': 3762,
- u'id': 0,
- u'name': u'100th Window',
- u'song': [{u'album': u'100th Window',
- u'albumId': 0,
- u'artist': u'Massive Attack',
- u'artistId': 0,
- u'bitRate': 192,
- u'contentType': u'audio/mpeg',
- u'coverArt': 2,
- u'created': u'2009-08-28T10:00:57',
- u'duration': 341,
- u'genre': u'Rock',
- u'id': 14,
- u'isDir': False,
- u'isVideo': False,
- u'parent': 2,
- u'path': u'Massive Attack/100th Window/01 - Future Proof.mp3',
- u'size': 8184445,
- u'suffix': u'mp3',
- u'title': u'Future Proof',
- u'track': 1,
- u'type': u'music',
- u'year': 2003}],
- u'songCount': 9},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getAlbum'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': id})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getSong(self, id):
- """
- since 1.8.0
-
- Returns the info for a song. This method uses the ID3
- tags for organization
-
- id:str The song ID
-
- Returns a dict like the following:
- {u'song': {u'album': u'W H O K I L L',
- u'albumId': 8,
- u'artist': u'Tune-Yards',
- u'artistId': 1,
- u'bitRate': 320,
- u'contentType': u'audio/mpeg',
- u'coverArt': 106,
- u'created': u'2011-03-22T15:08:00',
- u'discNumber': 1,
- u'duration': 192,
- u'genre': u'Indie Rock',
- u'id': 120,
- u'isDir': False,
- u'isVideo': False,
- u'parent': 106,
- u'path': u'Tune Yards/Who Kill/10 Killa.mp3',
- u'size': 7692656,
- u'suffix': u'mp3',
- u'title': u'Killa',
- u'track': 10,
- u'type': u'music',
- u'year': 2011},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getSong'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': id})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getVideos(self):
- """
- since 1.8.0
-
- Returns all video files
-
- Returns a dict like the following:
- {u'status': u'ok',
- u'version': u'1.8.0',
- u'videos': {u'video': {u'bitRate': 384,
- u'contentType': u'video/x-matroska',
- u'created': u'2012-08-26T13:36:44',
- u'duration': 1301,
- u'id': 130,
- u'isDir': False,
- u'isVideo': True,
- u'path': u'South Park - 16x07 - Cartman Finds Love.mkv',
- u'size': 287309613,
- u'suffix': u'mkv',
- u'title': u'South Park - 16x07 - Cartman Finds Love',
- u'transcodedContentType': u'video/x-flv',
- u'transcodedSuffix': u'flv'}},
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getVideos'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getStarred(self, musicFolderId=None):
- """
- since 1.8.0
-
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
-
- Returns starred songs, albums and artists
-
- Returns a dict like the following:
- {u'starred': {u'album': {u'album': u'Bird-Brains',
- u'artist': u'Tune-Yards',
- u'coverArt': 105,
- u'created': u'2012-01-30T13:16:58',
- u'id': 105,
- u'isDir': True,
- u'parent': 104,
- u'starred': u'2012-08-26T13:18:34',
- u'title': u'Bird-Brains'},
- u'song': [{u'album': u'Mezzanine',
- u'albumId': 4,
- u'artist': u'Massive Attack',
- u'artistId': 0,
- u'bitRate': 256,
- u'contentType': u'audio/mpeg',
- u'coverArt': 6,
- u'created': u'2009-06-15T07:48:28',
- u'duration': 298,
- u'genre': u'Dub',
- u'id': 72,
- u'isDir': False,
- u'isVideo': False,
- u'parent': 6,
- u'path': u'Massive Attack/Mezzanine/Massive Attack_02_mezzanine.mp3',
- u'size': 9564160,
- u'starred': u'2012-08-26T13:19:26',
- u'suffix': u'mp3',
- u'title': u'Risingson',
- u'track': 2,
- u'type': u'music'},
- {u'album': u'Mezzanine',
- u'albumId': 4,
- u'artist': u'Massive Attack',
- u'artistId': 0,
- u'bitRate': 256,
- u'contentType': u'audio/mpeg',
- u'coverArt': 6,
- u'created': u'2009-06-15T07:48:25',
- u'duration': 380,
- u'genre': u'Dub',
- u'id': 71,
- u'isDir': False,
- u'isVideo': False,
- u'parent': 6,
- u'path': u'Massive Attack/Mezzanine/Massive Attack_01_mezzanine.mp3',
- u'size': 12179456,
- u'starred': u'2012-08-26T13:19:03',
- u'suffix': u'mp3',
- u'title': u'Angel',
- u'track': 1,
- u'type': u'music'}]},
- u'status': u'ok',
- u'version': u'1.8.0',
- u'xmlns': u'http://subsonic.org/restapi'}
- """
- methodName = 'getStarred'
- viewName = '%s.view' % methodName
-
- q = {}
- if musicFolderId:
- q['musicFolderId'] = musicFolderId
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getStarred2(self, musicFolderId=None):
- """
- since 1.8.0
-
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
-
- Returns starred songs, albums and artists like getStarred(),
- but this uses ID3 tags for organization
-
- Returns a dict like the following:
-
- **See the output from getStarred()**
- """
- methodName = 'getStarred2'
- viewName = '%s.view' % methodName
-
- q = {}
- if musicFolderId:
- q['musicFolderId'] = musicFolderId
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def updatePlaylist(self, lid, name=None, comment=None, songIdsToAdd=[],
- songIndexesToRemove=[]):
- """
- since 1.8.0
-
- Updates a playlist. Only the owner of a playlist is allowed to
- update it.
-
- lid:str The playlist id
- name:str The human readable name of the playlist
- comment:str The playlist comment
- songIdsToAdd:list A list of song IDs to add to the playlist
- songIndexesToRemove:list Remove the songs at the
- 0 BASED INDEXED POSITIONS in the
- playlist, NOT the song ids. Note that
- this is always a list.
-
- Returns a normal status response dict
- """
- methodName = 'updatePlaylist'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'playlistId': lid, 'name': name,
- 'comment': comment})
- if not isinstance(songIdsToAdd, list) or isinstance(songIdsToAdd,
- tuple):
- songIdsToAdd = [songIdsToAdd]
- if not isinstance(songIndexesToRemove, list) or isinstance(
- songIndexesToRemove, tuple):
- songIndexesToRemove = [songIndexesToRemove]
- listMap = {'songIdToAdd': songIdsToAdd,
- 'songIndexToRemove': songIndexesToRemove}
- req = self._getRequestWithLists(viewName, listMap, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAvatar(self, username):
- """
- since 1.8.0
-
- Returns the avatar for a user or None if the avatar does not exist
-
- username:str The user to retrieve the avatar for
-
- Returns the file-like object for reading or raises an exception
- on error
- """
- methodName = 'getAvatar'
- viewName = '%s.view' % methodName
-
- q = {'username': username}
-
- req = self._getRequest(viewName, q)
- try:
- res = self._doBinReq(req)
- except urllib2.HTTPError:
- # Avatar is not set/does not exist, return None
- return None
- if isinstance(res, dict):
- self._checkStatus(res)
- return res
-
- def star(self, sids=[], albumIds=[], artistIds=[]):
- """
- since 1.8.0
-
- Attaches a star to songs, albums or artists
-
- sids:list A list of song IDs to star
- albumIds:list A list of album IDs to star. Use this rather than
- "sids" if the client access the media collection
- according to ID3 tags rather than file
- structure
- artistIds:list The ID of an artist to star. Use this rather
- than sids if the client access the media
- collection according to ID3 tags rather
- than file structure
-
- Returns a normal status response dict
- """
- methodName = 'star'
- viewName = '%s.view' % methodName
-
- if not isinstance(sids, list) or isinstance(sids, tuple):
- sids = [sids]
- if not isinstance(albumIds, list) or isinstance(albumIds, tuple):
- albumIds = [albumIds]
- if not isinstance(artistIds, list) or isinstance(artistIds, tuple):
- artistIds = [artistIds]
- listMap = {'id': sids,
- 'albumId': albumIds,
- 'artistId': artistIds}
- req = self._getRequestWithLists(viewName, listMap)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def unstar(self, sids=[], albumIds=[], artistIds=[]):
- """
- since 1.8.0
-
- Removes a star to songs, albums or artists. Basically, the
- same as star in reverse
-
- sids:list A list of song IDs to star
- albumIds:list A list of album IDs to star. Use this rather than
- "sids" if the client access the media collection
- according to ID3 tags rather than file
- structure
- artistIds:list The ID of an artist to star. Use this rather
- than sids if the client access the media
- collection according to ID3 tags rather
- than file structure
-
- Returns a normal status response dict
- """
- methodName = 'unstar'
- viewName = '%s.view' % methodName
-
- if not isinstance(sids, list) or isinstance(sids, tuple):
- sids = [sids]
- if not isinstance(albumIds, list) or isinstance(albumIds, tuple):
- albumIds = [albumIds]
- if not isinstance(artistIds, list) or isinstance(artistIds, tuple):
- artistIds = [artistIds]
- listMap = {'id': sids,
- 'albumId': albumIds,
- 'artistId': artistIds}
- req = self._getRequestWithLists(viewName, listMap)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getGenres(self):
- """
- since 1.9.0
-
- Returns all genres
- """
- methodName = 'getGenres'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getSongsByGenre(self, genre, count=10, offset=0, musicFolderId=None):
- """
- since 1.9.0
-
- Returns songs in a given genre
-
- genre:str The genre, as returned by getGenres()
- count:int The maximum number of songs to return. Max is 500
- default: 10
- offset:int The offset if you are paging. default: 0
- musicFolderId:int Only return results from the music folder
- with the given ID. See getMusicFolders
- """
- methodName = 'getGenres'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'genre': genre,
- 'count': count,
- 'offset': offset,
- 'musicFolderId': musicFolderId,
- })
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def hls (self, mid, bitrate=None):
- """
- since 1.8.0
-
- Creates an HTTP live streaming playlist for streaming video or
- audio HLS is a streaming protocol implemented by Apple and
- works by breaking the overall stream into a sequence of small
- HTTP-based file downloads. It's supported by iOS and newer
- versions of Android. This method also supports adaptive
- bitrate streaming, see the bitRate parameter.
-
- mid:str The ID of the media to stream
- bitrate:str If specified, the server will attempt to limit the
- bitrate to this value, in kilobits per second. If
- this parameter is specified more than once, the
- server will create a variant playlist, suitable
- for adaptive bitrate streaming. The playlist will
- support streaming at all the specified bitrates.
- The server will automatically choose video dimensions
- that are suitable for the given bitrates.
- (since: 1.9.0) you may explicitly request a certain
- width (480) and height (360) like so:
- bitRate=1000@480x360
-
- Returns the raw m3u8 file as a string
- """
- methodName = 'hls'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': mid, 'bitrate': bitrate})
- req = self._getRequest(viewName, q)
- try:
- res = self._doBinReq(req)
- except urllib2.HTTPError:
- # Avatar is not set/does not exist, return None
- return None
- if isinstance(res, dict):
- self._checkStatus(res)
- return res.read()
-
- def refreshPodcasts(self):
- """
- since: 1.9.0
-
- Tells the server to check for new Podcast episodes. Note: The user
- must be authorized for Podcast administration
- """
- methodName = 'refreshPodcasts'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def createPodcastChannel(self, url):
- """
- since: 1.9.0
-
- Adds a new Podcast channel. Note: The user must be authorized
- for Podcast administration
-
- url:str The URL of the Podcast to add
- """
- methodName = 'createPodcastChannel'
- viewName = '%s.view' % methodName
-
- q = {'url': url}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deletePodcastChannel(self, pid):
- """
- since: 1.9.0
-
- Deletes a Podcast channel. Note: The user must be authorized
- for Podcast administration
-
- pid:str The ID of the Podcast channel to delete
- """
- methodName = 'deletePodcastChannel'
- viewName = '%s.view' % methodName
-
- q = {'id': pid}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deletePodcastEpisode(self, pid):
- """
- since: 1.9.0
-
- Deletes a Podcast episode. Note: The user must be authorized
- for Podcast administration
-
- pid:str The ID of the Podcast episode to delete
- """
- methodName = 'deletePodcastEpisode'
- viewName = '%s.view' % methodName
-
- q = {'id': pid}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def downloadPodcastEpisode(self, pid):
- """
- since: 1.9.0
-
- Tells the server to start downloading a given Podcast episode.
- Note: The user must be authorized for Podcast administration
-
- pid:str The ID of the Podcast episode to download
- """
- methodName = 'downloadPodcastEpisode'
- viewName = '%s.view' % methodName
-
- q = {'id': pid}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getInternetRadioStations(self):
- """
- since: 1.9.0
-
- Returns all internet radio stations
- """
- methodName = 'getInternetRadioStations'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getBookmarks(self):
- """
- since: 1.9.0
-
- Returns all bookmarks for this user. A bookmark is a position
- within a media file
- """
- methodName = 'getBookmarks'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def createBookmark(self, mid, position, comment=None):
- """
- since: 1.9.0
-
- Creates or updates a bookmark (position within a media file).
- Bookmarks are personal and not visible to other users
-
- mid:str The ID of the media file to bookmark. If a bookmark
- already exists for this file, it will be overwritten
- position:int The position (in milliseconds) within the media file
- comment:str A user-defined comment
- """
- methodName = 'createBookmark'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': mid, 'position': position,
- 'comment': comment})
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def deleteBookmark(self, mid):
- """
- since: 1.9.0
-
- Deletes the bookmark for a given file
-
- mid:str The ID of the media file to delete the bookmark from.
- Other users' bookmarks are not affected
- """
- methodName = 'deleteBookmark'
- viewName = '%s.view' % methodName
-
- q = {'id': mid}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getArtistInfo(self, aid, count=20, includeNotPresent=False):
- """
- since: 1.11.0
-
- Returns artist info with biography, image URLS and similar artists
- using data from last.fm
-
- aid:str The ID of the artist, album or song
- count:int The max number of similar artists to return
- includeNotPresent:bool Whether to return artists that are not
- present in the media library
- """
- methodName = 'getArtistInfo'
- viewName = '%s.view' % methodName
-
- q = {'id': aid, 'count': count,
- 'includeNotPresent': includeNotPresent}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getArtistInfo2(self, aid, count=20, includeNotPresent=False):
- """
- since: 1.11.0
-
- Similar to getArtistInfo(), but organizes music according to ID3 tags
-
- aid:str The ID of the artist, album or song
- count:int The max number of similar artists to return
- includeNotPresent:bool Whether to return artists that are not
- present in the media library
- """
- methodName = 'getArtistInfo2'
- viewName = '%s.view' % methodName
-
- q = {'id': aid, 'count': count,
- 'includeNotPresent': includeNotPresent}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getSimilarSongs(self, iid, count=50):
- """
- since 1.11.0
-
- Returns a random collection of songs from the given artist and
- similar artists, using data from last.fm. Typically used for
- artist radio features.
-
- iid:str The artist, album, or song ID
- count:int Max number of songs to return
- """
- methodName = 'getSimilarSongs'
- viewName = '%s.view' % methodName
-
- q = {'id': iid, 'count': count}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getSimilarSongs2(self, iid, count=50):
- """
- since 1.11.0
-
- Similar to getSimilarSongs(), but organizes music according to
- ID3 tags
-
- iid:str The artist, album, or song ID
- count:int Max number of songs to return
- """
- methodName = 'getSimilarSongs2'
- viewName = '%s.view' % methodName
-
- q = {'id': iid, 'count': count}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def savePlayQueue(self, qids, current=None, position=None):
- """
- since 1.12.0
-
- qid:list[int] The list of song ids in the play queue
- current:int The id of the current playing song
- position:int The position, in milliseconds, within the current
- playing song
-
- Saves the state of the play queue for this user. This includes
- the tracks in the play queue, the currently playing track, and
- the position within this track. Typically used to allow a user to
- move between different clients/apps while retaining the same play
- queue (for instance when listening to an audio book).
- """
- methodName = 'savePlayQueue'
- viewName = '%s.view' % methodName
- if not isinstance(qids, (tuple, list)):
- qids = [qids]
-
- q = self._getQueryDict({'current': current, 'position': position})
-
- req = self._getRequestWithLists(viewName, {'id': qids}, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getPlayQueue(self):
- """
- since 1.12.0
-
- Returns the state of the play queue for this user (as set by
- savePlayQueue). This includes the tracks in the play queue,
- the currently playing track, and the position within this track.
- Typically used to allow a user to move between different
- clients/apps while retaining the same play queue (for instance
- when listening to an audio book).
- """
- methodName = 'getPlayQueue'
- viewName = '%s.view' % methodName
-
- req = self._getRequest(viewName)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getTopSongs(self, artist, count=50):
- """
- since 1.13.0
-
- Returns the top songs for a given artist
-
- artist:str The artist to get songs for
- count:int The number of songs to return
- """
- methodName = 'getTopSongs'
- viewName = '%s.view' % methodName
-
- q = {'artist': artist, 'count': count}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getNewestPodcasts(self, count=20):
- """
- since 1.13.0
-
- Returns the most recently published Podcast episodes
-
- count:int The number of episodes to return
- """
- methodName = 'getNewestPodcasts'
- viewName = '%s.view' % methodName
-
- q = {'count': count}
-
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def scanMediaFolders(self):
- """
- This is not an officially supported method of the API
-
- Same as selecting 'Settings' > 'Scan media folders now' with
- Subsonic web GUI
-
- Returns True if refresh successful, False otherwise
- """
- methodName = 'scanNow'
- return self._unsupportedAPIFunction(methodName)
-
- def cleanupDatabase(self):
- """
- This is not an officially supported method of the API
-
- Same as selecting 'Settings' > 'Clean-up Database' with Subsonic
- web GUI
-
- Returns True if cleanup initiated successfully, False otherwise
-
- Subsonic stores information about all media files ever encountered.
- By cleaning up the database, information about files that are
- no longer in your media collection is permanently removed.
- """
- methodName = 'expunge'
- return self._unsupportedAPIFunction(methodName)
-
- def getVideoInfo(self, vid):
- """
- since 1.14.0
-
- Returns details for a video, including information about available
- audio tracks, subtitles (captions) and conversions.
-
- vid:int The video ID
- """
- methodName = 'getVideoInfo'
- viewName = '%s.view' % methodName
-
- q = {'id': int(vid)}
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAlbumInfo(self, aid):
- """
- since 1.14.0
-
- Returns the album notes, image URLs, etc., using data from last.fm
-
- aid:int The album ID
- """
- methodName = 'getAlbumInfo'
- viewName = '%s.view' % methodName
-
- q = {'id': int(aid)}
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getAlbumInfo2(self, aid):
- """
- since 1.14.0
-
- Same as getAlbumInfo, but uses ID3 tags
-
- aid:int The album ID
- """
- methodName = 'getAlbumInfo2'
- viewName = '%s.view' % methodName
-
- q = {'id': int(aid)}
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def getCaptions(self, vid, fmt=None):
- """
- since 1.14.0
-
- Returns captions (subtitles) for a video. Use getVideoInfo for a list
- of captions.
-
- vid:int The ID of the video
- fmt:str Preferred captions format ("srt" or "vtt")
- """
- methodName = 'getCaptions'
- viewName = '%s.view' % methodName
-
- q = self._getQueryDict({'id': int(vid), 'format': fmt})
- req = self._getRequest(viewName, q)
- res = self._doInfoReq(req)
- self._checkStatus(res)
- return res
-
- def _unsupportedAPIFunction(self, methodName):
- """
- base function to call unsupported API methods
-
- Returns True if refresh successful, False otherwise
- :rtype : boolean
- """
- baseMethod = 'musicFolderSettings'
- viewName = '%s.view' % baseMethod
-
- url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port,
- self._separateServerPath(), viewName, methodName)
- req = urllib2.Request(url)
- res = self._opener.open(req)
- res_msg = res.msg.lower()
- return res_msg == 'ok'
-
- #
- # Private internal methods
- #
- def _getOpener(self, username, passwd):
- # Context is only relevent in >= python 2.7.9
- https_chain = HTTPSHandlerChain()
- if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
- https_chain = HTTPSHandlerChain(
- context=ssl._create_unverified_context())
- opener = urllib2.build_opener(PysHTTPRedirectHandler, https_chain)
- return opener
-
- def _getQueryDict(self, d):
- """
- Given a dictionary, it cleans out all the values set to None
- """
- for k, v in d.items():
- if v is None:
- del d[k]
- return d
-
- def _getBaseQdict(self):
- qdict = {
- 'f': 'json',
- 'v': self._apiVersion,
- 'c': self._appName,
- 'u': self._username,
- }
-
- if self._legacyAuth:
- qdict['p'] = 'enc:%s' % self._hexEnc(self._rawPass)
- else:
- salt = self._getSalt()
- token = md5(self._rawPass + salt).hexdigest()
- qdict.update({
- 's': salt,
- 't': token,
- })
-
- return qdict
-
- def _getRequest(self, viewName, query={}):
- qdict = self._getBaseQdict()
- qdict.update(query)
- url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
- viewName)
- req = urllib2.Request(url, urlencode(qdict))
-
- if self._useGET:
- url += '?%s' % urlencode(qdict)
- req = urllib2.Request(url)
-
- return req
-
- def _getRequestWithList(self, viewName, listName, alist, query={}):
- """
- Like _getRequest, but allows appending a number of items with the
- same key (listName). This bypasses the limitation of urlencode()
- """
- qdict = self._getBaseQdict()
- qdict.update(query)
- url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
- viewName)
- data = StringIO()
- data.write(urlencode(qdict))
- for i in alist:
- data.write('&%s' % urlencode({listName: i}))
- req = urllib2.Request(url, data.getvalue())
-
- if self._useGET:
- url += '?%s' % data.getvalue()
- req = urllib2.Request(url)
-
- return req
-
- def _getRequestWithLists(self, viewName, listMap, query={}):
- """
- Like _getRequestWithList(), but you must pass a dictionary
- that maps the listName to the list. This allows for multiple
- list parameters to be used, like in updatePlaylist()
-
- viewName:str The name of the view
- listMap:dict A mapping of listName to a list of entries
- query:dict The normal query dict
- """
- qdict = self._getBaseQdict()
- qdict.update(query)
- url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
- viewName)
- data = StringIO()
- data.write(urlencode(qdict))
- for k, l in listMap.iteritems():
- for i in l:
- data.write('&%s' % urlencode({k: i}))
- req = urllib2.Request(url, data.getvalue())
-
- if self._useGET:
- url += '?%s' % data.getvalue()
- req = urllib2.Request(url)
-
- return req
-
- def _doInfoReq(self, req):
- # Returns a parsed dictionary version of the result
- res = self._opener.open(req)
- dres = json.loads(res.read())
- return dres['subsonic-response']
-
- def _doBinReq(self, req):
- res = self._opener.open(req)
- contType = res.info().getheader('Content-Type')
- if contType:
- if contType.startswith('text/html') or \
- contType.startswith('application/json'):
- dres = json.loads(res.read())
- return dres['subsonic-response']
- return res
-
- def _checkStatus(self, result):
- if result['status'] == 'ok':
- return True
- elif result['status'] == 'failed':
- exc = getExcByCode(result['error']['code'])
- raise exc(result['error']['message'])
-
- def _hexEnc(self, raw):
- """
- Returns a "hex encoded" string per the Subsonic api docs
-
- raw:str The string to hex encode
- """
- ret = ''
- for c in raw:
- ret += '%02X' % ord(c)
- return ret
-
- def _ts2milli(self, ts):
- """
- For whatever reason, Subsonic uses timestamps in milliseconds since
- the unix epoch. I have no idea what need there is of this precision,
- but this will just multiply the timestamp times 1000 and return the int
- """
- if ts is None:
- return None
- return int(ts * 1000)
-
- def _separateServerPath(self):
- """
- separate REST portion of URL from base server path.
- """
- return urllib2.splithost(self._serverPath)[1].split('/')[0]
-
- def _fixLastModified(self, data):
- """
- This will recursively walk through a data structure and look for
- a dict key/value pair where the key is "lastModified" and change
- the shitty java millisecond timestamp to a real unix timestamp
- of SECONDS since the unix epoch. JAVA SUCKS!
- """
- if isinstance(data, dict):
- for k, v in data.items():
- if k == 'lastModified':
- data[k] = long(v) / 1000.0
- return
- elif isinstance(v, (tuple, list, dict)):
- return self._fixLastModified(v)
- elif isinstance(data, (list, tuple)):
- for item in data:
- if isinstance(item, (list, tuple, dict)):
- return self._fixLastModified(item)
-
- def _process_netrc(self, use_netrc):
- """
- The use_netrc var is either a boolean, which means we should use
- the user's default netrc, or a string specifying a path to a
- netrc formatted file
-
- use_netrc:bool|str Either set to True to use the user's default
- netrc file or a string specifying a specific
- netrc file to use
- """
- if not use_netrc:
- raise CredentialError('useNetrc must be either a boolean "True" '
- 'or a string representing a path to a netrc file, '
- 'not {0}'.format(repr(use_netrc)))
- if isinstance(use_netrc, bool) and use_netrc:
- self._netrc = netrc()
- else:
- # This should be a string specifying a path to a netrc file
- self._netrc = netrc(os.path.expanduser(use_netrc))
- auth = self._netrc.authenticators(self._hostname)
- if not auth:
- raise CredentialError('No machine entry found for {0} in '
- 'your netrc file'.format(self._hostname))
-
- # If we get here, we have credentials
- self._username = auth[0]
- self._rawPass = auth[2]
-
- def _getSalt(self, length=12):
- salt = md5(os.urandom(100)).hexdigest()
- return salt[:length]
diff --git a/lib/py-sonic/libsonic/errors.py b/lib/py-sonic/libsonic/errors.py
deleted file mode 100644
index a000c57..0000000
--- a/lib/py-sonic/libsonic/errors.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-This file is part of py-sonic.
-
-py-sonic is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-py-sonic 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 py-sonic. If not, see
-"""
-
-class SonicError(Exception):
- pass
-
-class ParameterError(SonicError):
- pass
-
-class VersionError(SonicError):
- pass
-
-class CredentialError(SonicError):
- pass
-
-class AuthError(SonicError):
- pass
-
-class LicenseError(SonicError):
- pass
-
-class DataNotFoundError(SonicError):
- pass
-
-class ArgumentError(SonicError):
- pass
-
-# This maps the error code numbers from the Subsonic server to their
-# appropriate Exceptions
-ERR_CODE_MAP = {
- 0: SonicError ,
- 10: ParameterError ,
- 20: VersionError ,
- 30: VersionError ,
- 40: CredentialError ,
- 50: AuthError ,
- 60: LicenseError ,
- 70: DataNotFoundError ,
-}
-
-def getExcByCode(code):
- code = int(code)
- if code in ERR_CODE_MAP:
- return ERR_CODE_MAP[code]
- return SonicError
diff --git a/lib/py-sonic/setup.py b/lib/py-sonic/setup.py
deleted file mode 100644
index fb4ce5a..0000000
--- a/lib/py-sonic/setup.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python
-
-"""
-This file is part of py-sonic.
-
-py-sonic is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-py-sonic 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 py-sonic. If not, see
-"""
-
-from distutils.core import setup
-from libsonic import __version__ as version
-
-setup(name='py-sonic',
- version=version,
- author='Jay Deiman',
- author_email='admin@splitstreams.com',
- url='http://stuffivelearned.org',
- description='A python wrapper library for the Subsonic REST API. '
- 'http://subsonic.org',
- long_description='This is a basic wrapper library for the Subsonic '
- 'REST API. This will allow you to connect to your server and retrieve '
- 'information and have it returned in basic Python types.',
- packages=['libsonic'],
- package_dir={'libsonic': 'libsonic'},
- classifiers=[
- 'Development Status :: 4 - Beta',
- 'Intended Audience :: System Administrators',
- 'Intended Audience :: Information Technology',
- 'License :: OSI Approved :: GNU General Public License (GPL)',
- 'Natural Language :: English',
- 'Operating System :: POSIX',
- 'Programming Language :: Python',
- 'Topic :: System :: Systems Administration',
- 'Topic :: Internet :: WWW/HTTP',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- 'Topic :: Software Development :: Libraries',
- 'Topic :: System',
- ]
-)
diff --git a/lib/simpleplugin/__init__.py b/lib/simpleplugin/__init__.py
new file mode 100644
index 0000000..337f20e
--- /dev/null
+++ b/lib/simpleplugin/__init__.py
@@ -0,0 +1,2 @@
+#v2.0.1
+#https://github.com/romanvm/script.module.simpleplugin/releases
\ No newline at end of file
diff --git a/lib/simpleplugin/simpleplugin.py b/lib/simpleplugin/simpleplugin.py
new file mode 100644
index 0000000..8fc6975
--- /dev/null
+++ b/lib/simpleplugin/simpleplugin.py
@@ -0,0 +1,915 @@
+# -*- coding: utf-8 -*-
+# Created on: 03.06.2015
+"""
+SimplePlugin micro-framework for Kodi content plugins
+
+**Author**: Roman Miroshnychenko aka Roman V.M.
+
+**License**: `GPL v.3 `_
+"""
+
+import os
+import sys
+import re
+from datetime import datetime, timedelta
+import cPickle as pickle
+from urlparse import parse_qs
+from urllib import urlencode
+from functools import wraps
+from collections import MutableMapping, namedtuple
+from copy import deepcopy
+from types import GeneratorType
+from hashlib import md5
+from shutil import move
+import xbmcaddon
+import xbmc
+import xbmcplugin
+import xbmcgui
+
+__all__ = ['SimplePluginError', 'Storage', 'Addon', 'Plugin', 'Params']
+
+ListContext = namedtuple('ListContext', ['listing', 'succeeded', 'update_listing', 'cache_to_disk',
+ 'sort_methods', 'view_mode', 'content'])
+PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
+
+
+class SimplePluginError(Exception):
+ """Custom exception"""
+ pass
+
+
+class Params(dict):
+ """
+ Params(**kwargs)
+
+ A class that stores parsed plugin call parameters
+
+ Parameters can be accessed both through :class:`dict` keys and
+ instance properties.
+
+ Example:
+
+ .. code-block:: python
+
+ @plugin.action('foo')
+ def action(params):
+ foo = params['foo'] # Access by key
+ bar = params.bar # Access through property. Both variants are equal
+ """
+ def __getattr__(self, item):
+ if item not in self:
+ raise AttributeError('Invalid parameter: "{0}"!'.format(item))
+ return self[item]
+
+ def __str__(self):
+ return ''.format(super(Params, self).__repr__())
+
+ def __repr__(self):
+ return ''.format(super(Params, self).__repr__())
+
+
+class Storage(MutableMapping):
+ """
+ Persistent storage for arbitrary data with a dictionary-like interface
+
+ It is designed as a context manager and better be used
+ with 'with' statement.
+
+ :param storage_dir: directory for storage
+ :type storage_dir: str
+ :param filename: the name of a storage file (optional)
+ :type filename: str
+
+ Usage::
+
+ with Storage('/foo/bar/storage/') as storage:
+ storage['key1'] = value1
+ value2 = storage['key2']
+
+ .. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
+ Storage contents are saved to disk only for a new storage or if the contents have been changed.
+ """
+ def __init__(self, storage_dir, filename='storage.pcl'):
+ """
+ Class constructor
+ """
+ self._storage = {}
+ self._hash = None
+ self._filename = os.path.join(storage_dir, filename)
+ try:
+ with open(self._filename, 'rb') as fo:
+ contents = fo.read()
+ self._storage = pickle.loads(contents)
+ self._hash = md5(contents).hexdigest()
+ except (IOError, pickle.PickleError, EOFError):
+ pass
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.flush()
+
+ def __getitem__(self, key):
+ return self._storage[key]
+
+ def __setitem__(self, key, value):
+ self._storage[key] = value
+
+ def __delitem__(self, key):
+ del self._storage[key]
+
+ def __iter__(self):
+ return self._storage.__iter__()
+
+ def __len__(self):
+ return len(self._storage)
+
+ def __str__(self):
+ return ''.format(self._storage)
+
+ def __repr__(self):
+ return ''.format(self._storage)
+
+ def flush(self):
+ """
+ Save storage contents to disk
+
+ This method saves new and changed :class:`Storage` contents to disk
+ and invalidates the Storage instance. Unchanged Storage is not saved
+ but simply invalidated.
+ """
+ contents = pickle.dumps(self._storage)
+ if self._hash is None or md5(contents).hexdigest() != self._hash:
+ tmp = self._filename + '.tmp'
+ try:
+ with open(tmp, 'wb') as fo:
+ fo.write(contents)
+ except:
+ os.remove(tmp)
+ raise
+ move(tmp, self._filename) # Atomic save
+ del self._storage
+
+ def copy(self):
+ """
+ Make a copy of storage contents
+
+ .. note:: this method performs a *deep* copy operation.
+
+ :return: a copy of storage contents
+ :rtype: dict
+ """
+ return deepcopy(self._storage)
+
+
+class Addon(object):
+ """
+ Base addon class
+
+ Provides access to basic addon parameters
+
+ :param id_: addon id, e.g. 'plugin.video.foo' (optional)
+ :type id_: str
+ """
+ def __init__(self, id_=''):
+ """
+ Class constructor
+ """
+ self._addon = xbmcaddon.Addon(id_)
+ self._configdir = xbmc.translatePath(self._addon.getAddonInfo('profile')).decode('utf-8')
+ self._ui_strings_map = None
+ if not os.path.exists(self._configdir):
+ os.mkdir(self._configdir)
+
+ def __getattr__(self, item):
+ """
+ Get addon setting as an Addon instance attribute
+
+ E.g. addon.my_setting is equal to addon.get_setting('my_setting')
+
+ :param item:
+ :type item: str
+ """
+ return self.get_setting(item)
+
+ def __str__(self):
+ return ''.format(self.id)
+
+ def __repr__(self):
+ return ''.format(self.id)
+
+ @property
+ def addon(self):
+ """
+ Kodi Addon instance that represents this Addon
+
+ :return: Addon instance
+ :rtype: xbmcaddon.Addon
+ """
+ return self._addon
+
+ @property
+ def id(self):
+ """
+ Addon ID
+
+ :return: Addon ID, e.g. 'plugin.video.foo'
+ :rtype: str
+ """
+ return self._addon.getAddonInfo('id')
+
+ @property
+ def path(self):
+ """
+ Addon path
+
+ :return: path to the addon folder
+ :rtype: str
+ """
+ return self._addon.getAddonInfo('path').decode('utf-8')
+
+ @property
+ def icon(self):
+ """
+ Addon icon
+
+ :return: path to the addon icon image
+ :rtype: str
+ """
+ icon = os.path.join(self.path, 'icon.png')
+ if os.path.exists(icon):
+ return icon
+ else:
+ return ''
+
+ @property
+ def fanart(self):
+ """
+ Addon fanart
+
+ :return: path to the addon fanart image
+ :rtype: str
+ """
+ fanart = os.path.join(self.path, 'fanart.jpg')
+ if os.path.exists(fanart):
+ return fanart
+ else:
+ return ''
+
+ @property
+ def config_dir(self):
+ """
+ Addon config dir
+
+ :return: path to the addon config dir
+ :rtype: str
+ """
+ return self._configdir
+
+ def get_localized_string(self, id_):
+ """
+ Get localized UI string
+
+ :param id_: UI string ID
+ :type id_: int
+ :return: UI string in the current language
+ :rtype: unicode
+ """
+ return self._addon.getLocalizedString(id_).encode('utf-8')
+
+ def get_setting(self, id_, convert=True):
+ """
+ Get addon setting
+
+ If ``convert=True``, 'bool' settings are converted to Python :class:`bool` values,
+ and numeric strings to Python :class:`long` or :class:`float` depending on their format.
+
+ .. note:: Settings can also be read via :class:`Addon` instance poperties named as the respective settings.
+ I.e. ``addon.foo`` is equal to ``addon.get_setting('foo')``.
+
+ :param id_: setting ID
+ :type id_: str
+ :param convert: try to guess and convert the setting to an appropriate type
+ E.g. ``'1.0'`` will be converted to float ``1.0`` number, ``'true'`` to ``True`` and so on.
+ :type convert: bool
+ :return: setting value
+ """
+ setting = self._addon.getSetting(id_)
+ if convert:
+ if setting == 'true':
+ return True # Convert boolean strings to bool
+ elif setting == 'false':
+ return False
+ elif re.search(r'^-?\d+$', setting) is not None:
+ return long(setting) # Convert numeric strings to long
+ elif re.search(r'^-?\d+\.\d+$', setting) is not None:
+ return float(setting) # Convert numeric strings with a dot to float
+ return setting
+
+ def set_setting(self, id_, value):
+ """
+ Set addon setting
+
+ Python :class:`bool` type are converted to ``'true'`` or ``'false'``
+ Non-string/non-unicode values are converted to strings.
+
+ .. warning:: Setting values via :class:`Addon` instance properties is not supported!
+ Values can only be set using :meth:`Addon.set_setting` method.
+
+ :param id_: setting ID
+ :type id_: str
+ :param value: setting value
+ """
+ if isinstance(value, bool):
+ value = 'true' if value else 'false'
+ elif not isinstance(value, basestring):
+ value = str(value)
+ self._addon.setSetting(id_, value)
+
+ def log(self, message, level=0):
+ """
+ Add message to Kodi log starting with Addon ID
+
+ :param message: message to be written into the Kodi log
+ :type message: str
+ :param level: log level. :mod:`xbmc` module provides the necessary symbolic constants.
+ Default: ``xbmc.LOGDEBUG``
+ :type level: int
+ """
+ if isinstance(message, unicode):
+ message = message.encode('utf-8')
+ xbmc.log('{0}: {1}'.format(self.id, message), level)
+
+ def log_notice(self, message):
+ """
+ Add NOTICE message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGINFO)
+
+ def log_warning(self, message):
+ """
+ Add WARNING message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGWARNING)
+
+ def log_error(self, message):
+ """
+ Add ERROR message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGERROR)
+
+ def log_debug(self, message):
+ """
+ Add debug message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGDEBUG)
+
+ def get_storage(self, filename='storage.pcl'):
+ """
+ Get a persistent :class:`Storage` instance for storing arbitrary values between addon calls.
+
+ A :class:`Storage` instance can be used as a context manager.
+
+ Example::
+
+ with plugin.get_storage() as storage:
+ storage['param1'] = value1
+ value2 = storage['param2']
+
+ .. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
+
+ :param filename: the name of a storage file (optional)
+ :type filename: str
+ :return: Storage object
+ :rtype: Storage
+ """
+ return Storage(self.config_dir, filename)
+
+ def cached(self, duration=10):
+ """
+ Cached decorator
+
+ Used to cache function return data
+
+ Usage::
+
+ @plugin.cached(30)
+ def my_func(*args, **kwargs):
+ # Do some stuff
+ return value
+
+ :param duration: caching duration in min (positive values only)
+ :type duration: int
+ :raises ValueError: if duration is zero or negative
+ """
+ def outer_wrapper(func):
+ @wraps(func)
+ def inner_wrapper(*args, **kwargs):
+ with self.get_storage('__cache__.pcl') as cache:
+ current_time = datetime.now()
+ key = func.__name__ + str(args) + str(kwargs)
+ try:
+ data, timestamp = cache[key]
+ if duration > 0 and current_time - timestamp > timedelta(minutes=duration):
+ raise KeyError
+ elif duration <= 0:
+ raise ValueError('Caching duration cannot be zero or negative!')
+ self.log_debug('Cache hit: {0}'.format(key))
+ except KeyError:
+ self.log_debug('Cache miss: {0}'.format(key))
+ data = func(*args, **kwargs)
+ cache[key] = (data, current_time)
+ return data
+ return inner_wrapper
+ return outer_wrapper
+
+ def gettext(self, ui_string):
+ """
+ Get a translated UI string from addon localization files.
+
+ This function emulates GNU Gettext for more convenient access
+ to localized addon UI strings. To reduce typing this method object
+ can be assigned to a ``_`` (single underscore) variable.
+
+ For using gettext emulation :meth:`Addon.initialize_gettext` method
+ needs to be called first. See documentation for that method for more info
+ about Gettext emulation.
+
+ :param ui_string: a UI string from English :file:`strings.po`.
+ :type ui_string: str
+ :return: a UI string from translated :file:`strings.po`.
+ :rtype: unicode
+ :raises simpleplugin.SimplePluginError: if :meth:`Addon.initialize_gettext` wasn't called first
+ or if a string is not found in English :file:`strings.po`.
+ """
+ if self._ui_strings_map is not None:
+ try:
+ return self.get_localized_string(self._ui_strings_map['strings'][ui_string])
+ except KeyError:
+ raise SimplePluginError('UI string "{0}" is not found in strings.po!'.format(ui_string))
+ else:
+ raise SimplePluginError('Addon localization is not initialized!')
+
+ def initialize_gettext(self):
+ """
+ Initialize GNU gettext emulation in addon
+
+ Kodi localization system for addons is not very convenient
+ because you need to operate with numeric string codes instead
+ of UI strings themselves which reduces code readability and
+ may lead to errors. The :class:`Addon` class provides facilities
+ for emulating GNU Gettext localization system.
+
+ This allows to use UI strings from addon's English :file:`strings.po`
+ file instead of numeric codes to return localized strings from
+ respective localized :file:`.po` files.
+
+ This method returns :meth:`Addon.gettext` method object that
+ can be assigned to a short alias to reduce typing. Traditionally,
+ ``_`` (a single underscore) is used for this purpose.
+
+ Example::
+
+ addon = simpleplugin.Addon()
+ _ = addon.initialize_gettext()
+
+ xbmcgui.Dialog().notification(_('Warning!'), _('Something happened'))
+
+ In the preceding example the notification strings will be replaced
+ with localized versions if these strings are translated.
+
+ :return: :meth:`Addon.gettext` method object
+ :raises simpleplugin.SimplePluginError: if the addon's English :file:`strings.po` file is missing
+ """
+ strings_po = os.path.join(self.path, 'resources', 'language', 'English', 'strings.po')
+ if os.path.exists(strings_po):
+ with open(strings_po, 'rb') as fo:
+ raw_strings = fo.read()
+ raw_strings_hash = md5(raw_strings).hexdigest()
+ gettext_pcl = '__gettext__.pcl'
+ with self.get_storage(gettext_pcl) as ui_strings_map:
+ if (not os.path.exists(os.path.join(self._configdir, gettext_pcl)) or
+ raw_strings_hash != ui_strings_map['hash']):
+ ui_strings = self._parse_po(raw_strings.split('\n'))
+ self._ui_strings_map = {
+ 'hash': raw_strings_hash,
+ 'strings': ui_strings
+ }
+ ui_strings_map['hash'] = raw_strings_hash
+ ui_strings_map['strings'] = ui_strings.copy()
+ else:
+ self._ui_strings_map = deepcopy(ui_strings_map)
+ else:
+ raise SimplePluginError('Unable to initialize localization because of missing English strings.po!')
+ return self.gettext
+
+ def _parse_po(self, strings):
+ """
+ Parses ``strings.po`` file into a dict of {'string': id} items.
+ """
+ ui_strings = {}
+ string_id = None
+ for string in strings:
+ if string_id is None and 'msgctxt' in string:
+ string_id = int(re.search(r'"#(\d+)"', string).group(1))
+ elif string_id is not None and 'msgid' in string:
+ ui_strings[re.search(r'"(.*?)"', string, re.U).group(1)] = string_id
+ string_id = None
+ return ui_strings
+
+
+class Plugin(Addon):
+ """
+ Plugin class
+
+ :param id_: plugin's id, e.g. 'plugin.video.foo' (optional)
+ :type id_: str
+
+ This class provides a simplified API to create virtual directories of playable items
+ for Kodi content plugins.
+ :class:`simpleplugin.Plugin` uses a concept of callable plugin actions (functions or methods)
+ that are defined using :meth:`Plugin.action` decorator.
+ A Plugin instance must have at least one action that is named ``'root'``.
+
+ Minimal example:
+
+ .. code-block:: python
+
+ from simpleplugin import Plugin
+
+ plugin = Plugin()
+
+ @plugin.action()
+ def root(params): # Mandatory item!
+ return [{'label': 'Foo',
+ 'url': plugin.get_url(action='some_action', param='Foo')},
+ {'label': 'Bar',
+ 'url': plugin.get_url(action='some_action', param='Bar')}]
+
+ @plugin.action()
+ def some_action(params):
+ return [{'label': params['param']}]
+
+ plugin.run()
+
+ An action callable receives 1 parameter -- params.
+ params is a dict-like object containing plugin call parameters (including action string)
+ The action callable can return
+ either a list/generator of dictionaries representing Kodi virtual directory items
+ or a resolved playable path (:class:`str` or :obj:`unicode`) for Kodi to play.
+
+ Example 1::
+
+ @plugin.action()
+ def list_action(params):
+ listing = get_listing(params) # Some external function to create listing
+ return listing
+
+ The ``listing`` variable is a Python list/generator of dict items.
+ Example 2::
+
+ @plugin.action()
+ def play_action(params):
+ path = get_path(params) # Some external function to get a playable path
+ return path
+
+ Each dict item can contain the following properties:
+
+ - label -- item's label (default: ``''``).
+ - label2 -- item's label2 (default: ``''``).
+ - thumb -- item's thumbnail (default: ``''``).
+ - icon -- item's icon (default: ``''``).
+ - path -- item's path (default: ``''``).
+ - fanart -- item's fanart (optional).
+ - art -- a dict containing all item's graphic (see :meth:`xbmcgui.ListItem.setArt` for more info) -- optional.
+ - stream_info -- a dictionary of ``{stream_type: {param: value}}`` items
+ (see :meth:`xbmcgui.ListItem.addStreamInfo`) -- optional.
+ - info -- a dictionary of ``{media: {param: value}}`` items
+ (see :meth:`xbmcgui.ListItem.setInfo`) -- optional
+ - context_menu - a list that contains 2-item tuples ``('Menu label', 'Action')``.
+ The items from the tuples are added to the item's context menu.
+ - url -- a callback URL for this list item.
+ - is_playable -- if ``True``, then this item is playable and must return a playable path or
+ be resolved via :meth:`Plugin.resolve_url` (default: ``False``).
+ - is_folder -- if ``True`` then the item will open a lower-level sub-listing. if ``False``,
+ the item either is a playable media or a general-purpose script
+ which neither creates a virtual folder nor points to a playable media (default: C{True}).
+ if ``'is_playable'`` is set to ``True``, then ``'is_folder'`` value automatically assumed to be ``False``.
+ - subtitles -- the list of paths to subtitle files (optional).
+ - mime -- item's mime type (optional).
+ - list_item -- an 'class:`xbmcgui.ListItem` instance (optional).
+ It is used when you want to set all list item properties by yourself.
+ If ``'list_item'`` property is present, all other properties,
+ except for ``'url'`` and ``'is_folder'``, are ignored.
+ - properties -- a dictionary of list item properties
+ (see :meth:`xbmcgui.ListItem.setProperty`) -- optional.
+
+ Example 3::
+
+ listing = [{ 'label': 'Label',
+ 'label2': 'Label 2',
+ 'thumb': 'thumb.png',
+ 'icon': 'icon.png',
+ 'fanart': 'fanart.jpg',
+ 'art': {'clearart': 'clearart.png'},
+ 'stream_info': {'video': {'codec': 'h264', 'duration': 1200},
+ 'audio': {'codec': 'ac3', 'language': 'en'}},
+ 'info': {'video': {'genre': 'Comedy', 'year': 2005}},
+ 'context_menu': [('Menu Item', 'Action')],
+ 'url': 'plugin:/plugin.video.test/?action=play',
+ 'is_playable': True,
+ 'is_folder': False,
+ 'subtitles': ['/path/to/subtitles.en.srt', '/path/to/subtitles.uk.srt'],
+ 'mime': 'video/mp4'
+ }]
+
+ Alternatively, an action callable can use :meth:`Plugin.create_listing` and :meth:`Plugin.resolve_url`
+ static methods to pass additional parameters to Kodi.
+
+ Example 4::
+
+ @plugin.action()
+ def list_action(params):
+ listing = get_listing(params) # Some external function to create listing
+ return Plugin.create_listing(listing, sort_methods=(2, 10, 17), view_mode=500)
+
+ Example 5::
+
+ @plugin.action()
+ def play_action(params):
+ path = get_path(params) # Some external function to get a playable path
+ return Plugin.resolve_url(path, succeeded=True)
+
+ If an action callable performs any actions other than creating a listing or
+ resolving a playable URL, it must return ``None``.
+ """
+ def __init__(self, id_=''):
+ """
+ Class constructor
+ """
+ super(Plugin, self).__init__(id_)
+ self._url = 'plugin://{0}/'.format(self.id)
+ self._handle = None
+ self.actions = {}
+
+ def __str__(self):
+ return ''.format(sys.argv)
+
+ def __repr__(self):
+ return ''.format(sys.argv)
+
+ @staticmethod
+ def get_params(paramstring):
+ """
+ Convert a URL-encoded paramstring to a Python dict
+
+ :param paramstring: URL-encoded paramstring
+ :type paramstring: str
+ :return: parsed paramstring
+ :rtype: Params
+ """
+ raw_params = parse_qs(paramstring)
+ params = Params()
+ for key, value in raw_params.iteritems():
+ params[key] = value[0] if len(value) == 1 else value
+ return params
+
+ def get_url(self, plugin_url='', **kwargs):
+ """
+ Construct a callable URL for a virtual directory item
+
+ If plugin_url is empty, a current plugin URL is used.
+ kwargs are converted to a URL-encoded string of plugin call parameters
+ To call a plugin action, 'action' parameter must be used,
+ if 'action' parameter is missing, then the plugin root action is called
+ If the action is not added to :class:`Plugin` actions, :class:`PluginError` will be raised.
+
+ :param plugin_url: plugin URL with trailing / (optional)
+ :type plugin_url: str
+ :param kwargs: pairs of key=value items
+ :return: a full plugin callback URL
+ :rtype: str
+ """
+ url = plugin_url or self._url
+ if kwargs:
+ return '{0}?{1}'.format(url, urlencode(kwargs, doseq=True))
+ return url
+
+ def action(self, name=None):
+ """
+ Action decorator
+
+ Defines plugin callback action. If action's name is not defined explicitly,
+ then the action is named after the decorated function.
+
+ .. warning:: Action's name must be unique.
+
+ A plugin must have at least one action named ``'root'`` implicitly or explicitly.
+
+ Example:
+
+ .. code-block:: python
+
+ @plugin.action() # The action is implicitly named 'root' after the decorated function
+ def root(params):
+ pass
+
+ @plugin.action('foo') # The action name is set explicitly
+ def foo_action(params):
+ pass
+
+ :param name: action's name (optional).
+ :type name: str
+ :raises simpleplugin.SimplePluginError: if the action with such name is already defined.
+ """
+ def wrap(func, name=name):
+ if name is None:
+ name = func.__name__
+ if name in self.actions:
+ raise SimplePluginError('Action "{0}" already defined!'.format(name))
+ self.actions[name] = func
+ return func
+ return wrap
+
+ def run(self, category=''):
+ """
+ Run plugin
+
+ :param category: str - plugin sub-category, e.g. 'Comedy'.
+ See :func:`xbmcplugin.setPluginCategory` for more info.
+ :type category: str
+ :raises simpleplugin.SimplePluginError: if unknown action string is provided.
+ """
+ self._handle = int(sys.argv[1])
+ if category:
+ xbmcplugin.setPluginCategory(self._handle, category)
+ params = self.get_params(sys.argv[2][1:])
+ action = params.get('action', 'root')
+ self.log_debug(str(self))
+ self.log_debug('Actions: {0}'.format(str(self.actions.keys())))
+ self.log_debug('Called action "{0}" with params "{1}"'.format(action, str(params)))
+ try:
+ action_callable = self.actions[action]
+ except KeyError:
+ raise SimplePluginError('Invalid action: "{0}"!'.format(action))
+ else:
+ result = action_callable(params)
+ self.log_debug('Action return value: {0}'.format(str(result)))
+ if isinstance(result, (list, GeneratorType)):
+ self._add_directory_items(self.create_listing(result))
+ elif isinstance(result, basestring):
+ self._set_resolved_url(self.resolve_url(result))
+ elif isinstance(result, tuple) and hasattr(result, 'listing'):
+ self._add_directory_items(result)
+ elif isinstance(result, tuple) and hasattr(result, 'path'):
+ self._set_resolved_url(result)
+ else:
+ self.log_debug('The action "{0}" has not returned any valid data to process.'.format(action))
+
+ @staticmethod
+ def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,
+ view_mode=None, content=None):
+ """
+ Create and return a context dict for a virtual folder listing
+
+ :param listing: the list of the plugin virtual folder items
+ :type listing: :class:`list` or :class:`types.GeneratorType`
+ :param succeeded: if ``False`` Kodi won't open a new listing and stays on the current level.
+ :type succeeded: bool
+ :param update_listing: if ``True``, Kodi won't open a sub-listing but refresh the current one.
+ :type update_listing: bool
+ :param cache_to_disk: cache this view to disk.
+ :type cache_to_disk: bool
+ :param sort_methods: the list of integer constants representing virtual folder sort methods.
+ :type sort_methods: tuple
+ :param view_mode: a numeric code for a skin view mode.
+ View mode codes are different in different skins except for ``50`` (basic listing).
+ :type view_mode: int
+ :param content: string - current plugin content, e.g. 'movies' or 'episodes'.
+ See :func:`xbmcplugin.setContent` for more info.
+ :type content: str
+ :return: context object containing necessary parameters
+ to create virtual folder listing in Kodi UI.
+ :rtype: ListContext
+ """
+ return ListContext(listing, succeeded, update_listing, cache_to_disk, sort_methods, view_mode, content)
+
+ @staticmethod
+ def resolve_url(path='', play_item=None, succeeded=True):
+ """
+ Create and return a context dict to resolve a playable URL
+
+ :param path: the path to a playable media.
+ :type path: str or unicode
+ :param play_item: a dict of item properties as described in the class docstring.
+ It allows to set additional properties for the item being played, like graphics, metadata etc.
+ if ``play_item`` parameter is present, then ``path`` value is ignored, and the path must be set via
+ ``'path'`` property of a ``play_item`` dict.
+ :type play_item: dict
+ :param succeeded: if ``False``, Kodi won't play anything
+ :type succeeded: bool
+ :return: context object containing necessary parameters
+ for Kodi to play the selected media.
+ :rtype: PlayContext
+ """
+ return PlayContext(path, play_item, succeeded)
+
+ @staticmethod
+ def create_list_item(item):
+ """
+ Create an :class:`xbmcgui.ListItem` instance from an item dict
+
+ :param item: a dict of ListItem properties
+ :type item: dict
+ :return: ListItem instance
+ :rtype: xbmcgui.ListItem
+ """
+ list_item = xbmcgui.ListItem(label=item.get('label', ''),
+ label2=item.get('label2', ''),
+ path=item.get('path', ''))
+ if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 16:
+ art = item.get('art', {})
+ art['thumb'] = item.get('thumb', '')
+ art['icon'] = item.get('icon', '')
+ art['fanart'] = item.get('fanart', '')
+ item['art'] = art
+ else:
+ list_item.setThumbnailImage(item.get('thumb', ''))
+ list_item.setIconImage(item.get('icon', ''))
+ list_item.setProperty('fanart_image', item.get('fanart', ''))
+ if item.get('art'):
+ list_item.setArt(item['art'])
+ if item.get('stream_info'):
+ for stream, stream_info in item['stream_info'].iteritems():
+ list_item.addStreamInfo(stream, stream_info)
+ if item.get('info'):
+ for media, info in item['info'].iteritems():
+ list_item.setInfo(media, info)
+ if item.get('context_menu') is not None:
+ list_item.addContextMenuItems(item['context_menu'])
+ if item.get('subtitles'):
+ list_item.setSubtitles(item['subtitles'])
+ if item.get('mime'):
+ list_item.setMimeType(item['mime'])
+ if item.get('properties'):
+ for key, value in item['properties'].iteritems():
+ list_item.setProperty(key, value)
+ return list_item
+
+ def _add_directory_items(self, context):
+ """
+ Create a virtual folder listing
+
+ :param context: context object
+ :type context: ListContext
+ """
+ self.log_debug('Creating listing from {0}'.format(str(context)))
+ if context.content is not None:
+ xbmcplugin.setContent(self._handle, context.content) # This must be at the beginning
+ listing = []
+ for item in context.listing:
+ is_folder = item.get('is_folder', True)
+ if item.get('list_item') is not None:
+ list_item = item['list_item']
+ else:
+ list_item = self.create_list_item(item)
+ if item.get('is_playable'):
+ list_item.setProperty('IsPlayable', 'true')
+ is_folder = False
+ listing.append((item['url'], list_item, is_folder))
+ xbmcplugin.addDirectoryItems(self._handle, listing, len(listing))
+ if context.sort_methods is not None:
+ [xbmcplugin.addSortMethod(self._handle, method) for method in context.sort_methods]
+ xbmcplugin.endOfDirectory(self._handle,
+ context.succeeded,
+ context.update_listing,
+ context.cache_to_disk)
+ if context.view_mode is not None:
+ xbmc.executebuiltin('Container.SetViewMode({0})'.format(context.view_mode))
+
+ def _set_resolved_url(self, context):
+ """
+ Resolve a playable URL
+
+ :param context: context object
+ :type context: PlayContext
+ """
+ self.log_debug('Resolving URL from {0}'.format(str(context)))
+ if context.play_item is None:
+ list_item = xbmcgui.ListItem(path=context.path)
+ else:
+ list_item = self.create_list_item(context.play_item)
+ xbmcplugin.setResolvedUrl(self._handle, context.succeeded, list_item)
diff --git a/main.py b/main.py
index 843fbea..5004a65 100644
--- a/main.py
+++ b/main.py
@@ -6,40 +6,31 @@
# Created on: 04.10.2016
# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html
-
+import os
+import xbmcaddon
import xbmcgui
import json
-import os
import shutil
import dateutil.parser
from datetime import datetime
+# Add the /lib folder to sys
+sys.path.append(xbmc.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
-#check for Simpleplugin. Official repos are not up to date so let's do this nasty trick.
-#TO FIX : version check. https://github.com/romanvm/script.module.simpleplugin/issues/4
-try:
- from simpleplugin import Plugin
- from simpleplugin import Addon
-except:
- xbmcgui.Dialog().ok('SimplePlugin 2.0.1 required', "The Subsonic Addon requires SimplePlugin 2.0.1 framework.", "Please download and install it !", "https://github.com/romanvm/script.module.simpleplugin/releases")
- sys.exit()
-
+import libsonic
+import libsonic_extra
+from simpleplugin import Plugin
+from simpleplugin import Addon
# Create plugin instance
plugin = Plugin()
-# Make sure library folder is on the path
-sys.path.append(xbmc.translatePath(
- os.path.join(plugin.addon.getAddonInfo('path'), 'lib')))
-
# initialize_gettext
#_ = plugin.initialize_gettext()
connection = None
cachetime = int(Addon().get_setting('cachetime'))
-import libsonic_extra
-
def popup(text, time=5000, image=None):
title = plugin.addon.getAddonInfo('name')
icon = plugin.addon.getAddonInfo('icon')