diff --git a/script.service.kodi.callbacks/LICENSE.txt b/script.service.kodi.callbacks/LICENSE.txt
new file mode 100644
index 0000000000..23e7ed4954
--- /dev/null
+++ b/script.service.kodi.callbacks/LICENSE.txt
@@ -0,0 +1,682 @@
+
+THE PATHTOOLS MODULE IS UTILIZED UNDER AN MIT LICENSE. SEE THE PATHTOOLS SUBDIRECTORY FOR A COPY OF THE LICENSE
+
+THE WATCHDOG MODULE IS UTILIZED UNDER AN APACHE LICENSE. SEE THE WATCHDOG SUBDIRECTORY FOR A COPY OF THE LICENSE
+
+ALL OTHER CODE IS PROVIDED UNDER THE FOLLOWING LICENSE:
+
+
+ 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/script.service.kodi.callbacks/README.md b/script.service.kodi.callbacks/README.md
new file mode 100644
index 0000000000..11f65bb5b3
--- /dev/null
+++ b/script.service.kodi.callbacks/README.md
@@ -0,0 +1,9 @@
+script.service.kodi.callbacks
+=======================
+Props to pilluli for initial idea
+
+This is an all new version using a Publisher-Subscriber model.
+
+For usage details, please see the wiki: http://kodi.wiki/view/Add-on:Kodi_Callbacks
+
+Please report problems on the kodi forum at: http://forum.kodi.tv/showthread.php?tid=256170
diff --git a/script.service.kodi.callbacks/addon.xml b/script.service.kodi.callbacks/addon.xml
new file mode 100644
index 0000000000..849fe64372
--- /dev/null
+++ b/script.service.kodi.callbacks/addon.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+ Callbacks for Kodi
+ Provides user definable actions for specific events within Kodi. Credit to Yesudeep Mangalapilly (gorakhargosh on github) and contributors for watchdog and pathtools modules.
+
+ For bugs, requests or general questions visit the Kodi forums.
+ all
+ https://github.com/KenV99/script.service.kodi.callbacks
+ http://kodi.wiki/view/Add-on:Kodi_Callbacks
+ http://forum.kodi.tv/showthread.php?tid=256170
+ GNU GENERAL PUBLIC LICENSE. Version 3, June 2007
+ en
+
+
+
diff --git a/script.service.kodi.callbacks/changelog.txt b/script.service.kodi.callbacks/changelog.txt
new file mode 100644
index 0000000000..beac064c79
--- /dev/null
+++ b/script.service.kodi.callbacks/changelog.txt
@@ -0,0 +1,67 @@
+v0.9.9 (2016-03-22)
+Version changed to meet official repo rules
+
+v0.9.8.5 (2016-03-15)
+Fixed log bug and testWatchdogStartup bugs
+
+v0.9.8.4 (2016-03-09)
+- Added JSON Notify task
+- Corrected settings bug where one could not select tasks properly
+
+v0.9.8.3 (2016-03-07)
+- Removed github code from master branch
+
+v0.9.8.2 (2016-03-05)
+- Refactored settings xml generation
+- Made unconfigured tasks unselectable in settings
+
+v0.9.8 (2016-02-26)
+- Merged nonrepo branch onto master
+- Change xml to be official repo compliant
+- Added ability to write settings to log - useful if need to put log on pastebin
+- Hide unused tasks and events in settings
+
+v0.9.7.6 (2016-02-22)
+- Refactored update, detects paths with spaces
+
+v0.9.7.5 (2016-02-16)
+- Fix schedule import
+
+v0.9.7.4 (2016-02-16)
+- Added Scheduler publisher to schedule tasks to run daily or at a given interval
+
+v0.9.7.3 (2016-02-15)
+- Non-repo version added ability to update from Github
+
+v0.9.7.2 (2016-02-14)
+- Added ability to load from zip in settings and backup current
+
+v0.9.7.2 (2016-02-13)
+- Implement kodipathtools. Publisher tests passing on Linux, OSX, Windows.
+
+v0.9.7.1 (2016-02-04)
+- Android path fix. Added %mt for pause and resume. Changed settings to allow for special// paths.
+
+v0.9.7 (2016-02-03)
+- Watchdog startup file change detection added. Several bugs corrected.
+
+v0.9.6 (2016-01-31)
+- Publisher and Subscriber Factories. Readied for offical repo submission.
+
+v0.9.5 (2016-01-29)
+- Minor changes to onIdle and afterIdle
+
+v0.9.4 (2016-01-27)
+- Added after Idle event. Bug fix with string handling from settings.xml
+
+v0.9.3 (2016-01-25)
+- Provided string localization and po handling
+
+v0.9.2 (2016-01-22)
+- Added basic auth for http, added task testing to settings, bug fixes, removed dependency on service.watchdog
+
+v0.9.1 (2016-01-16)
+- Bug fixes, file watchdog added
+
+v0.9.0 (2016-01-12)
+- Initial version
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/default.py b/script.service.kodi.callbacks/default.py
new file mode 100644
index 0000000000..9c310af3ca
--- /dev/null
+++ b/script.service.kodi.callbacks/default.py
@@ -0,0 +1,167 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+
+debug = False # TODO: check
+testdebug = False # TODO: check
+testTasks = False # TODO: check
+branch = 'master'
+build = '1006'
+
+from resources.lib.utils.debugger import startdebugger
+
+if debug:
+ startdebugger()
+
+import os
+import sys
+import threading
+import xbmc
+import xbmcaddon
+import resources.lib.pubsub as PubSub_Threaded
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.publisherfactory import PublisherFactory
+from resources.lib.subscriberfactory import SubscriberFactory
+from resources.lib.settings import Settings
+from resources.lib.utils.poutil import KodiPo
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+log = KodiLogger.log
+
+try:
+ _addonversion_ = xbmcaddon.Addon().getAddonInfo('version')
+except RuntimeError:
+ try:
+ _addonversion_ = xbmcaddon.Addon('script.service.kodi.callbacks').getAddonInfo('version')
+ except RuntimeError:
+ _addonversion_ = 'ERROR getting version'
+
+
+class Cache(object):
+ publishers = None
+ dispatcher = None
+
+
+class MainMonitor(xbmc.Monitor):
+ def __init__(self):
+ super(MainMonitor, self).__init__()
+
+ def onSettingsChanged(self):
+ log(msg=_('Settings change detected - attempting to restart'))
+ abortall()
+ start()
+
+
+def abortall():
+ for p in Cache.publishers:
+ try:
+ p.abort()
+ except threading.ThreadError as e:
+ log(msg=_('Error aborting: %s - Error: %s') % (str(p), str(e)))
+ Cache.dispatcher.abort()
+ for p in Cache.publishers:
+ p.join(0.5)
+ Cache.dispatcher.join(0.5)
+ if len(threading.enumerate()) > 1:
+ main_thread = threading.current_thread()
+ log(msg=_('Enumerating threads to kill others than main (%i)') % main_thread.ident)
+ for t in threading.enumerate():
+ if t is not main_thread and t.is_alive():
+ log(msg=_('Attempting to kill thread: %i: %s') % (t.ident, t.name))
+ try:
+ t.abort(0.5)
+ except (threading.ThreadError, AttributeError):
+ log(msg=_('Error killing thread'))
+ else:
+ if not t.is_alive():
+ log(msg=_('Thread killed succesfully'))
+ else:
+ log(msg=_('Error killing thread'))
+
+
+def start():
+ global log
+ settings = Settings()
+ settings.getSettings()
+ kl = KodiLogger()
+ if settings.general['elevate_loglevel'] is True:
+ kl.setLogLevel(xbmc.LOGNOTICE)
+ else:
+ kl.setLogLevel(xbmc.LOGDEBUG)
+ log = kl.log
+ log(msg=_('Settings read'))
+ Cache.dispatcher = PubSub_Threaded.Dispatcher(interval=settings.general['TaskFreq'], sleepfxn=xbmc.sleep)
+ log(msg=_('Dispatcher initialized'))
+
+ subscriberfactory = SubscriberFactory(settings, kl)
+ subscribers = subscriberfactory.createSubscribers()
+ for subscriber in subscribers:
+ Cache.dispatcher.addSubscriber(subscriber)
+ publisherfactory = PublisherFactory(settings, subscriberfactory.topics, Cache.dispatcher, kl, debug)
+ publisherfactory.createPublishers()
+ Cache.publishers = publisherfactory.ipublishers
+
+ Cache.dispatcher.start()
+ log(msg=_('Dispatcher started'))
+
+ for p in Cache.publishers:
+ try:
+ p.start()
+ except threading.ThreadError:
+ raise
+ log(msg=_('Publisher(s) started'))
+
+
+def main():
+ xbmc.log(msg=_('$$$ [kodi.callbacks] - Staring kodi.callbacks ver: %s (build %s)') % (str(_addonversion_), build),
+ level=xbmc.LOGNOTICE)
+ if branch != 'master':
+ xbmcaddon.Addon().setSetting('installed branch', branch)
+ start()
+ Cache.dispatcher.q_message(PubSub_Threaded.Message(PubSub_Threaded.Topic('onStartup')))
+ monitor = MainMonitor()
+ log(msg=_('Entering wait loop'))
+ monitor.waitForAbort()
+
+ # Shutdown tasks
+ Cache.dispatcher.q_message(PubSub_Threaded.Message(PubSub_Threaded.Topic('onShutdown'), pid=os.getpid()))
+ log(msg=_('Shutdown started'))
+ abortall()
+ log(msg='Shutdown complete')
+
+
+if __name__ == '__main__':
+
+ if testTasks:
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ startdebugger()
+ from resources.lib.tests.testTasks import testTasks
+
+ tt = testTasks()
+ tt.runTests()
+ else:
+ if branch != 'master':
+ try:
+ from resources.lib.utils.githubtools import processargs
+ except ImportError:
+ pass
+ else:
+ processargs(sys.argv)
+ main()
diff --git a/script.service.kodi.callbacks/fanart.jpg b/script.service.kodi.callbacks/fanart.jpg
new file mode 100644
index 0000000000..9652460b66
Binary files /dev/null and b/script.service.kodi.callbacks/fanart.jpg differ
diff --git a/script.service.kodi.callbacks/icon.png b/script.service.kodi.callbacks/icon.png
new file mode 100644
index 0000000000..5de843e3a0
Binary files /dev/null and b/script.service.kodi.callbacks/icon.png differ
diff --git a/script.service.kodi.callbacks/resources/__init__.py b/script.service.kodi.callbacks/resources/__init__.py
new file mode 100644
index 0000000000..6483cfc09b
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
diff --git a/script.service.kodi.callbacks/resources/language/English/strings.po b/script.service.kodi.callbacks/resources/language/English/strings.po
new file mode 100644
index 0000000000..d91c5eedfa
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/language/English/strings.po
@@ -0,0 +1,1220 @@
+# Kodi Media Center language file
+# Addon Name: Kodi Callbacks
+# Addon id: script.service.kodi.callbacks
+# Addon Provider: KenV99
+msgid ""
+msgstr ""
+"Project-Id-Version: XBMC Addons\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n""Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Addon Summary"
+msgid "Callbacks for Kodi"
+msgstr ""
+
+msgctxt "Addon Description"
+msgid "Provides user definable actions for specific events within Kodi. Credit to Yesudeep Mangalapilly (gorakhargosh on github) and contributors for watchdog and pathtools modules."
+msgstr ""
+
+msgctxt "Addon Disclaimer"
+msgid "For bugs, requests or general questions visit the Kodi forums."
+msgstr ""
+
+#Add-on messages id=32001 to 32298
+
+msgctxt "#32001"
+msgid "Task 1"
+msgstr ""
+
+msgctxt "#32002"
+msgid "Task 2"
+msgstr ""
+
+msgctxt "#32003"
+msgid "Task 3"
+msgstr ""
+
+msgctxt "#32004"
+msgid "Task 4"
+msgstr ""
+
+msgctxt "#32005"
+msgid "Task 5"
+msgstr ""
+
+msgctxt "#32006"
+msgid "Task 6"
+msgstr ""
+
+msgctxt "#32007"
+msgid "Task 7"
+msgstr ""
+
+msgctxt "#32008"
+msgid "Task 8"
+msgstr ""
+
+msgctxt "#32009"
+msgid "Task 9"
+msgstr ""
+
+msgctxt "#32010"
+msgid "Task 10"
+msgstr ""
+
+msgctxt "#32011"
+msgid "Event 1"
+msgstr ""
+
+msgctxt "#32012"
+msgid "Event 2"
+msgstr ""
+
+msgctxt "#32013"
+msgid "Event 3"
+msgstr ""
+
+msgctxt "#32014"
+msgid "Event 4"
+msgstr ""
+
+msgctxt "#32015"
+msgid "Event 5"
+msgstr ""
+
+msgctxt "#32016"
+msgid "Event 6"
+msgstr ""
+
+msgctxt "#32017"
+msgid "Event 7"
+msgstr ""
+
+msgctxt "#32018"
+msgid "Event 8"
+msgstr ""
+
+msgctxt "#32019"
+msgid "Event 9"
+msgstr ""
+
+msgctxt "#32020"
+msgid "Event 10"
+msgstr ""
+
+msgctxt "#32021"
+msgid "Task received: %s: %s"
+msgstr ""
+
+msgctxt "#32022"
+msgid "Kodi Callbacks"
+msgstr ""
+
+msgctxt "#32023"
+msgid "Settings change detected - attempting to restart"
+msgstr ""
+
+msgctxt "#32024"
+msgid "Command for Task %s, Event %s completed succesfully!"
+msgstr ""
+
+msgctxt "#32025"
+msgid "\nThe following message was returned: %s"
+msgstr ""
+
+msgctxt "#32026"
+msgid "ERROR encountered for Task %s, Event %s\nERROR mesage: %s"
+msgstr ""
+
+msgctxt "#32027"
+msgid "Settings read"
+msgstr ""
+
+msgctxt "#32028"
+msgid "Dispatcher initialized"
+msgstr ""
+
+msgctxt "#32029"
+msgid "Subscriber for event: %s, task: %s created"
+msgstr ""
+
+msgctxt "#32030"
+msgid "Subscriber for event: %s, task: %s NOT created due to errors"
+msgstr ""
+
+msgctxt "#32031"
+msgid "Loop Publisher initialized"
+msgstr ""
+
+msgctxt "#32032"
+msgid "Player Publisher initialized"
+msgstr ""
+
+msgctxt "#32033"
+msgid "Monitor Publisher initialized"
+msgstr ""
+
+msgctxt "#32034"
+msgid "Log Publisher initialized"
+msgstr ""
+
+msgctxt "#32035"
+msgid "Watchdog Publisher initialized"
+msgstr ""
+
+msgctxt "#32036"
+msgid "Dispatcher started"
+msgstr ""
+
+msgctxt "#32037"
+msgid "Publisher(s) started"
+msgstr ""
+
+msgctxt "#32038"
+msgid "$$$ [kodi.callbacks] - Staring kodi.callbacks ver: %s (build %s)"
+msgstr ""
+
+msgctxt "#32039"
+msgid "Entering wait loop"
+msgstr ""
+
+msgctxt "#32040"
+msgid "Shutdown started"
+msgstr ""
+
+msgctxt "#32041"
+msgid "Error aborting: %s - Error: %s"
+msgstr ""
+
+msgctxt "#32042"
+msgid "Enumerating threads to kill others than main (%i)"
+msgstr ""
+
+msgctxt "#32043"
+msgid "Attempting to kill thread: %i: %s"
+msgstr ""
+
+msgctxt "#32044"
+msgid "Error killing thread"
+msgstr ""
+
+msgctxt "#32045"
+msgid "Thread killed succesfully"
+msgstr ""
+
+msgctxt "#32046"
+msgid "Running Test for Event: %s"
+msgstr ""
+
+msgctxt "#32047"
+msgid "Settings for test read"
+msgstr ""
+
+msgctxt "#32048"
+msgid "Creating subscriber for test"
+msgstr ""
+
+msgctxt "#32049"
+msgid "Test subscriber created successfully"
+msgstr ""
+
+msgctxt "#32050"
+msgid "Running test"
+msgstr ""
+
+msgctxt "#32051"
+msgid "Unspecified error during testing"
+msgstr ""
+
+msgctxt "#32052"
+msgid "Test subscriber creation failed due to errors"
+msgstr ""
+
+msgctxt "#32053"
+msgid "Native Task Testing Complete - see log for results"
+msgstr ""
+
+msgctxt "#32054"
+msgid "Task not run because task count exceeded"
+msgstr ""
+
+msgctxt "#32055"
+msgid "Task not run because task already running"
+msgstr ""
+
+msgctxt "#32056"
+msgid "Tasks"
+msgstr ""
+
+msgctxt "#32057"
+msgid "Task"
+msgstr ""
+
+msgctxt "#32058"
+msgid "Max num of this task running simultaneously (-1=no limit)"
+msgstr ""
+
+msgctxt "#32059"
+msgid "Max num of times this task runs (-1=no limit)"
+msgstr ""
+
+msgctxt "#32060"
+msgid "Refractory period in secs (-1=none)"
+msgstr ""
+
+msgctxt "#32061"
+msgid "Events"
+msgstr ""
+
+msgctxt "#32062"
+msgid "Type"
+msgstr ""
+
+msgctxt "#32063"
+msgid "Hint - variables can be subbed (%%=%, _%=space, _%%=,): "
+msgstr ""
+
+msgctxt "#32064"
+msgid "Var subbed arg string"
+msgstr ""
+
+msgctxt "#32065"
+msgid "Test Command (click OK to save changes first)"
+msgstr ""
+
+msgctxt "#32066"
+msgid "General"
+msgstr ""
+
+msgctxt "#32067"
+msgid "Display Notifications when Tasks Run?"
+msgstr ""
+
+msgctxt "#32068"
+msgid "Loop Pooling Frequency (ms)"
+msgstr ""
+
+msgctxt "#32069"
+msgid "Log Polling Frequency (ms)"
+msgstr ""
+
+msgctxt "#32070"
+msgid "Task Polling Frequency (ms)"
+msgstr ""
+
+msgctxt "#32071"
+msgid "Show debugging info in normal log?"
+msgstr ""
+
+msgctxt "#32072"
+msgid "Regenerate settings.xml (Developers Only)"
+msgstr ""
+
+msgctxt "#32073"
+msgid "Test addon native tasks (see log for output)"
+msgstr ""
+
+msgctxt "#32074"
+msgid "Settings.xml rewritten"
+msgstr ""
+
+msgctxt "#32075"
+msgid "Kodi Builtin Function"
+msgstr ""
+
+msgctxt "#32076"
+msgid "Task %s launching for event: %s"
+msgstr ""
+
+msgctxt "#32077"
+msgid "HTTP string (without parameters)"
+msgstr ""
+
+msgctxt "#32078"
+msgid "user for Basic Auth (optional)"
+msgstr ""
+
+msgctxt "#32079"
+msgid "password for Basic Auth (optional)"
+msgstr ""
+
+msgctxt "#32080"
+msgid "Invalid url: %s"
+msgstr ""
+
+msgctxt "#32081"
+msgid "Error on url read"
+msgstr ""
+
+msgctxt "#32082"
+msgid "Requests Connection Error"
+msgstr ""
+
+msgctxt "#32083"
+msgid "Requests HTTPError"
+msgstr ""
+
+msgctxt "#32084"
+msgid "Requests URLRequired Error"
+msgstr ""
+
+msgctxt "#32085"
+msgid "Requests Timeout Error"
+msgstr ""
+
+msgctxt "#32086"
+msgid "Generic Requests Error"
+msgstr ""
+
+msgctxt "#32087"
+msgid "HTTPError = "
+msgstr ""
+
+msgctxt "#32088"
+msgid "URLError\n"
+msgstr ""
+
+msgctxt "#32089"
+msgid "Http Bad Status Line caught and passed"
+msgstr ""
+
+msgctxt "#32090"
+msgid "HTTPException"
+msgstr ""
+
+msgctxt "#32091"
+msgid "The request timed out, host unreachable"
+msgstr ""
+
+msgctxt "#32092"
+msgid "Python file"
+msgstr ""
+
+msgctxt "#32093"
+msgid "Import and call run() (default=no)?"
+msgstr ""
+
+msgctxt "#32094"
+msgid "Error - not a python script: %s"
+msgstr ""
+
+msgctxt "#32095"
+msgid "Error - File not found: %s"
+msgstr ""
+
+msgctxt "#32096"
+msgid "Script executable file"
+msgstr ""
+
+msgctxt "#32097"
+msgid "Requires shell?"
+msgstr ""
+
+msgctxt "#32098"
+msgid "Wait for script to complete?"
+msgstr ""
+
+msgctxt "#32099"
+msgid "Failed to set execute bit on script: %s"
+msgstr ""
+
+msgctxt "#32100"
+msgid "Process returned data: %s\n"
+msgstr ""
+
+msgctxt "#32101"
+msgid "Process returned error: %s"
+msgstr ""
+
+msgctxt "#32102"
+msgid "sender"
+msgstr ""
+
+msgctxt "#32103"
+msgid "method"
+msgstr ""
+
+msgctxt "#32104"
+msgid "data"
+msgstr ""
+
+msgctxt "#32105"
+msgid "windowIdO"
+msgstr ""
+
+msgctxt "#32106"
+msgid "windowIdC"
+msgstr ""
+
+msgctxt "#32107"
+msgid "idleTime"
+msgstr ""
+
+msgctxt "#32108"
+msgid "matchIf"
+msgstr ""
+
+msgctxt "#32109"
+msgid "rejectIf"
+msgstr ""
+
+msgctxt "#32110"
+msgid "folder"
+msgstr ""
+
+msgctxt "#32111"
+msgid "patterns"
+msgstr ""
+
+msgctxt "#32112"
+msgid "ignore_patterns"
+msgstr ""
+
+msgctxt "#32113"
+msgid "ignore_directories"
+msgstr ""
+
+msgctxt "#32114"
+msgid "recursive"
+msgstr ""
+
+msgctxt "#32115"
+msgid "%sp=speed"
+msgstr ""
+
+msgctxt "#32116"
+msgid "%ar=aspectRatio,%mt=mediaType,%ti=title,%fn=fileName"
+msgstr ""
+
+msgctxt "#32117"
+msgid "%rs=resolution"
+msgstr ""
+
+msgctxt "#32118"
+msgid "%ti=time"
+msgstr ""
+
+msgctxt "#32119"
+msgid "%pi=pid"
+msgstr ""
+
+msgctxt "#32120"
+msgid "%ch=chapter"
+msgstr ""
+
+msgctxt "#32121"
+msgid "%li=library"
+msgstr ""
+
+msgctxt "#32122"
+msgid "%wi=windowId"
+msgstr ""
+
+msgctxt "#32123"
+msgid "%pp=profilePath"
+msgstr ""
+
+msgctxt "#32124"
+msgid "%ev=event,%pa=path"
+msgstr ""
+
+msgctxt "#32125"
+msgid "%mt=mediaType,%ti=title,%pp=percentPlayed,%fn=fileName"
+msgstr ""
+
+msgctxt "#32126"
+msgid "%me=method,%se=sender,%da=data"
+msgstr ""
+
+msgctxt "#32127"
+msgid "%sm=stereoMode"
+msgstr ""
+
+msgctxt "#32128"
+msgid "%ll=logLine"
+msgstr ""
+
+msgctxt "#32129"
+msgid "none"
+msgstr ""
+
+msgctxt "#32130"
+msgid "builtin"
+msgstr ""
+
+msgctxt "#32131"
+msgid "http"
+msgstr ""
+
+msgctxt "#32132"
+msgid "python"
+msgstr ""
+
+msgctxt "#32133"
+msgid "script"
+msgstr ""
+
+msgctxt "#32134"
+msgid "Task not run because task is in refractory period"
+msgstr ""
+
+msgctxt "#32135"
+msgid "Task starting for %s"
+msgstr ""
+
+msgctxt "#32136"
+msgid "Task finalized for %s"
+msgstr ""
+
+msgctxt "#32137"
+msgid "Could not stop LogMonitor T:%i"
+msgstr ""
+
+msgctxt "#32138"
+msgid "Could not stop LogPublisher T:%i"
+msgstr ""
+
+msgctxt "#32139"
+msgid "Could not stop LogCheckSimple T:%i"
+msgstr ""
+
+msgctxt "#32140"
+msgid "Could not stop LogCheckRegex T:%i"
+msgstr ""
+
+msgctxt "#32141"
+msgid "Could not stop LoopPublisher T:%i"
+msgstr ""
+
+msgctxt "#32142"
+msgid "Could not stop MonitorPublisher T:%i"
+msgstr ""
+
+msgctxt "#32143"
+msgid "Could not stop PlayerPublisher T:%i"
+msgstr ""
+
+msgctxt "#32144"
+msgid "Testing for task type: %s"
+msgstr ""
+
+msgctxt "#32145"
+msgid "Settings: %s"
+msgstr ""
+
+msgctxt "#32146"
+msgid "Runtime kwargs: %s"
+msgstr ""
+
+msgctxt "#32147"
+msgid "The following message was returned: %s"
+msgstr ""
+
+msgctxt "#32148"
+msgid "testHttp returned with an error: %s"
+msgstr ""
+
+msgctxt "#32149"
+msgid "Http test failed"
+msgstr ""
+
+msgctxt "#32150"
+msgid "Builtin test failed"
+msgstr ""
+
+msgctxt "#32151"
+msgid "testScriptNoShell returned with an error: %s"
+msgstr ""
+
+msgctxt "#32152"
+msgid "Script without shell test failed"
+msgstr ""
+
+msgctxt "#32153"
+msgid "testScriptShell returned with an error: %s"
+msgstr ""
+
+msgctxt "#32154"
+msgid "Script with shell test failed"
+msgstr ""
+
+msgctxt "#32155"
+msgid "testPythonImport returned with an error: %s"
+msgstr ""
+
+msgctxt "#32156"
+msgid "Python import test failed"
+msgstr ""
+
+msgctxt "#32157"
+msgid "testPythonExternal returned with an error: %s"
+msgstr ""
+
+msgctxt "#32158"
+msgid "Python external test failed"
+msgstr ""
+
+msgctxt "#32159"
+msgid "Error: %s"
+msgstr ""
+
+msgctxt "#32160"
+msgid "Error testing %s\n"
+msgstr ""
+
+msgctxt "#32161"
+msgid "Test passed for task %s"
+msgstr ""
+
+msgctxt "#32162"
+msgid "afterIdleTime"
+msgstr ""
+
+msgctxt "#32163"
+msgid "Process returned no data\n"
+msgstr ""
+
+msgctxt "#32164"
+msgid "Localized string added to po for: [%s]"
+msgstr ""
+
+msgctxt "#32165"
+msgid "Localized string not found for: [%s]"
+msgstr ""
+
+msgctxt "#32166"
+msgid "%li=listOfChanges"
+msgstr ""
+
+msgctxt "#32167"
+msgid "Watchdog Startup Publisher initialized"
+msgstr ""
+
+msgctxt "#32168"
+msgid "String not found"
+msgstr ""
+
+msgctxt "#32169"
+msgid "ws_folder"
+msgstr ""
+
+msgctxt "#32170"
+msgid "ws_patterns"
+msgstr ""
+
+msgctxt "#32171"
+msgid "ws_ignore_patterns"
+msgstr ""
+
+msgctxt "#32172"
+msgid "ws_ignore_directories"
+msgstr ""
+
+msgctxt "#32173"
+msgid "ws_recursive"
+msgstr ""
+
+msgctxt "#32174"
+msgid "Watchdog Startup ould not load pickle"
+msgstr ""
+
+msgctxt "#32175"
+msgid "Watchdog Startup unpickling error"
+msgstr ""
+
+msgctxt "#32176"
+msgid "Watchdog Startup folder not found: %s"
+msgstr ""
+
+msgctxt "#32177"
+msgid "Watchdog startup pickling error on exit"
+msgstr ""
+
+msgctxt "#32178"
+msgid "Watchdog startup OSError on pickle attempt"
+msgstr ""
+
+msgctxt "#32179"
+msgid "Watchdog startup pickle saved"
+msgstr ""
+
+msgctxt "#32180"
+msgid "Watchdog startup could not clear pickle"
+msgstr ""
+
+msgctxt "#32181"
+msgid "Localized string id not found for: [%s]"
+msgstr ""
+
+msgctxt "#32182"
+msgid "%mt=mediaType"
+msgstr ""
+
+msgctxt "#32183"
+msgid "%mt=mediaType,%ti=time"
+msgstr ""
+
+msgctxt "#32184"
+msgid "Error importing Watchdog: %s"
+msgstr ""
+
+msgctxt "#32185"
+msgid "Non existent string"
+msgstr ""
+
+msgctxt "#32186"
+msgid "testHttp never returned"
+msgstr ""
+
+msgctxt "#32187"
+msgid "Timed out waiting for return"
+msgstr ""
+
+msgctxt "#32188"
+msgid "Cannot fully test pythonExternal on Android"
+msgstr ""
+
+msgctxt "#32189"
+msgid "Error testing %s: %s"
+msgstr ""
+
+msgctxt "#32190"
+msgid "Update"
+msgstr ""
+
+msgctxt "#32191"
+msgid "Update from downloaded zip"
+msgstr ""
+
+msgctxt "#32192"
+msgid "Restore from previous back up"
+msgstr ""
+
+msgctxt "#32193"
+msgid "Locate zip file"
+msgstr ""
+
+msgctxt "#32194"
+msgid "Locate backup zip file"
+msgstr ""
+
+msgctxt "#32195"
+msgid "Watchdog Startup could not load pickle"
+msgstr ""
+
+msgctxt "#32196"
+msgid "A new version of %s is available\nDownload and install?"
+msgstr ""
+
+msgctxt "#32197"
+msgid "Downloaded file could not be extracted"
+msgstr ""
+
+msgctxt "#32198"
+msgid "Backup failed, update aborted"
+msgstr ""
+
+msgctxt "#32199"
+msgid "Error encountered copying to addon directory: %s"
+msgstr ""
+
+msgctxt "#32200"
+msgid "New version installed"
+msgstr ""
+
+msgctxt "#32201"
+msgid "\nPrevious installation backed up"
+msgstr ""
+
+msgctxt "#32202"
+msgid "Attempt to restart addon now?"
+msgstr ""
+
+msgctxt "#32203"
+msgid "All files are current"
+msgstr ""
+
+msgctxt "#32204"
+msgid "Could not find addon.xml\nInstallation aborted"
+msgstr ""
+
+msgctxt "#32205"
+msgid "Before any installation, the current is backed up to userdata/addon_data"
+msgstr ""
+
+msgctxt "#32206"
+msgid "Automatically download/install latest from GitHub on startup?"
+msgstr ""
+
+msgctxt "#32207"
+msgid "Install without prompts?"
+msgstr ""
+
+msgctxt "#32208"
+msgid "Check for update on GitHub"
+msgstr ""
+
+msgctxt "#32209"
+msgid "Download/install latest from Github"
+msgstr ""
+
+msgctxt "#32210"
+msgid "GitHub Download Error - Error reading file"
+msgstr ""
+
+msgctxt "#32211"
+msgid "Downloading %s bytes %s"
+msgstr ""
+
+msgctxt "#32212"
+msgid "Download Cancelled"
+msgstr ""
+
+msgctxt "#32213"
+msgid "GitHub Download Error: %s"
+msgstr ""
+
+msgctxt "#32214"
+msgid "Unknown GitHub Download Error"
+msgstr ""
+
+msgctxt "#32215"
+msgid "hour"
+msgstr ""
+
+msgctxt "#32216"
+msgid "minute"
+msgstr ""
+
+msgctxt "#32217"
+msgid "hours"
+msgstr ""
+
+msgctxt "#32218"
+msgid "minutes"
+msgstr ""
+
+msgctxt "#32219"
+msgid "seconds"
+msgstr ""
+
+msgctxt "#32220"
+msgid "onIntervalAlarm interval cannot be zero"
+msgstr ""
+
+msgctxt "#32221"
+msgid "Could not stop SchedulePublisher T:%i"
+msgstr ""
+
+msgctxt "#32222"
+msgid "Schedule Publisher initialized"
+msgstr ""
+
+msgctxt "#32223"
+msgid "Repository branch name"
+msgstr ""
+
+msgctxt "#32224"
+msgid "Currently installed branch"
+msgstr ""
+
+msgctxt "#32225"
+msgid "Repository branch name for downloads"
+msgstr ""
+
+msgctxt "#32226"
+msgid "Backup restored"
+msgstr ""
+
+msgctxt "#32227"
+msgid "The following files were updated: %s"
+msgstr ""
+
+msgctxt "#32228"
+msgid "$$$ [kodi.callbacks] - Staring kodi.callbacks ver: %s"
+msgstr ""
+
+msgctxt "#32229"
+msgid "None"
+msgstr ""
+
+msgctxt "#32230"
+msgid "on Clean Finished"
+msgstr ""
+
+msgctxt "#32231"
+msgid "on Clean Started"
+msgstr ""
+
+msgctxt "#32232"
+msgid "on DPMS Activated"
+msgstr ""
+
+msgctxt "#32233"
+msgid "on DPMS Deactivated"
+msgstr ""
+
+msgctxt "#32234"
+msgid "on Daily Alarm"
+msgstr ""
+
+msgctxt "#32235"
+msgid "on File System Change"
+msgstr ""
+
+msgctxt "#32236"
+msgid "on File System Change at Startup"
+msgstr ""
+
+msgctxt "#32237"
+msgid "on Idle [secs]"
+msgstr ""
+
+msgctxt "#32238"
+msgid "on Interval Alarm"
+msgstr ""
+
+msgctxt "#32239"
+msgid "on JSON Notification"
+msgstr ""
+
+msgctxt "#32240"
+msgid "on Log Regex"
+msgstr ""
+
+msgctxt "#32241"
+msgid "on Log Simple"
+msgstr ""
+
+msgctxt "#32242"
+msgid "on Playback Ended"
+msgstr ""
+
+msgctxt "#32243"
+msgid "on Playback Paused"
+msgstr ""
+
+msgctxt "#32244"
+msgid "on Playback Resumed"
+msgstr ""
+
+msgctxt "#32245"
+msgid "on Playback Seek"
+msgstr ""
+
+msgctxt "#32246"
+msgid "on Playback Seek Chapter"
+msgstr ""
+
+msgctxt "#32247"
+msgid "on Playback Speed Changed"
+msgstr ""
+
+msgctxt "#32248"
+msgid "on Playback Started"
+msgstr ""
+
+msgctxt "#32249"
+msgid "on Profile Change"
+msgstr ""
+
+msgctxt "#32250"
+msgid "on Queue Next Item"
+msgstr ""
+
+msgctxt "#32251"
+msgid "on Resume After Idle [secs]"
+msgstr ""
+
+msgctxt "#32252"
+msgid "on Scan Finished"
+msgstr ""
+
+msgctxt "#32253"
+msgid "on Scan Started"
+msgstr ""
+
+msgctxt "#32254"
+msgid "on Screensaver Activated"
+msgstr ""
+
+msgctxt "#32255"
+msgid "on Screensaver Deactivated"
+msgstr ""
+
+msgctxt "#32256"
+msgid "on Shutdown"
+msgstr ""
+
+msgctxt "#32257"
+msgid "on Startup"
+msgstr ""
+
+msgctxt "#32258"
+msgid "on Stereoscopic Mode Change "
+msgstr ""
+
+msgctxt "#32259"
+msgid "on Window Closed"
+msgstr ""
+
+msgctxt "#32260"
+msgid "on Window Opened"
+msgstr ""
+
+msgctxt "#32261"
+msgid "Choose event type (click here)"
+msgstr ""
+
+msgctxt "#32262"
+msgid "Event:"
+msgstr ""
+
+msgctxt "#32263"
+msgid "Write Settings into Kodi log"
+msgstr ""
+
+msgctxt "#32264"
+msgid "Settings Regenerated"
+msgstr ""
+
+msgctxt "#32265"
+msgid "Incorrect path"
+msgstr ""
+
+msgctxt "#32266"
+msgid "New version available for branch: %s"
+msgstr ""
+
+msgctxt "#32267"
+msgid "Current version: %s"
+msgstr ""
+
+msgctxt "#32268"
+msgid "Available version: %s"
+msgstr ""
+
+msgctxt "#32269"
+msgid "Download and install?"
+msgstr ""
+
+msgctxt "#32270"
+msgid "A new version is not available for branch: %s"
+msgstr ""
+
+msgctxt "#32271"
+msgid "Download and install anyway?"
+msgstr ""
+
+msgctxt "#32272"
+msgid "Settings written to log"
+msgstr ""
+
+msgctxt "#32273"
+msgid "Process returned data: [%s]\n"
+msgstr ""
+
+msgctxt "#32274"
+msgid "Script executable file - browse"
+msgstr ""
+
+msgctxt "#32275"
+msgid "Script executable file - edit"
+msgstr ""
+
+msgctxt "#32276"
+msgid ""
+msgstr ""
+
+msgctxt "#32277"
+msgid "Choose event type (click here)"
+msgstr ""
+
+msgctxt "#32278"
+msgid "Note: variables can be subbed (%%=%, _%=space, __%=,):"
+msgstr ""
+
+msgctxt "#32279"
+msgid "ws_folder - browse"
+msgstr ""
+
+msgctxt "#32280"
+msgid "ws_folder - edit"
+msgstr ""
+
+msgctxt "#32281"
+msgid "folder - browse"
+msgstr ""
+
+msgctxt "#32282"
+msgid "folder - edit"
+msgstr ""
+
+msgctxt "#32283"
+msgid "Upload Log"
+msgstr ""
+
+msgctxt "#32284"
+msgid "Choose event type"
+msgstr ""
+
+msgctxt "#32285"
+msgid "JsonNotify"
+msgstr ""
+
+msgctxt "#32286"
+msgid "Sender string"
+msgstr ""
+
+msgctxt "#32287"
+msgid "Request Type"
+msgstr ""
+
+msgctxt "#32288"
+msgid "json_rpc_notify"
+msgstr ""
+
+msgctxt "#32289"
+msgid "%al=album,%wi=width,%ar=aspectRatio,%at=artist,%ht=height"
+msgstr ""
+
+msgctxt "#32290"
+msgid "%st=showtitle,%ep=episode,%mt=mediaType,%se=season"
+msgstr ""
+
+msgctxt "#32291"
+msgid "%fn=fileName,%sm=stereomode,%ti=title"
+msgstr ""
+
+msgctxt "#32292"
+msgid "%st=showtitle,%sp=speed,%ep=episode,%mt=mediaType"
+msgstr ""
+
+msgctxt "#32293"
+msgid "%se=season,%fn=fileName,%sm=stereomode,%ti=title"
+msgstr ""
+
+msgctxt "#32294"
+msgid "%fn=fileName,%sm=stereomode,%ti=title,%tm=time"
+msgstr ""
+
+msgctxt "#32295"
+msgid "%al=album,%wi=width,%ch=chapter,%ar=aspectRatio,%at=artist"
+msgstr ""
+
+msgctxt "#32296"
+msgid "%ht=height,%st=showtitle,%ep=episode,%mt=mediaType"
+msgstr ""
+
+msgctxt "#32297"
+msgid "Content-Type (for POST or PUT only)"
+msgstr ""
+
+msgctxt "#32298"
+msgid "No actions run for this addon from Programs"
+msgstr ""
+
diff --git a/script.service.kodi.callbacks/resources/lib/__init__.py b/script.service.kodi.callbacks/resources/lib/__init__.py
new file mode 100644
index 0000000000..c23aacea2f
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/__init__.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+import os
+import pkgutil
+import sys
+
+import tasks
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.taskABC import AbstractTask
+from resources.lib.utils.kodipathtools import translatepath, setPathExecuteRW, setPathRW
+
+KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+log = KodiLogger.log
+
+
+def createUserTasks():
+ paths = [translatepath('special://addondata')]
+ try:
+ setPathRW(paths[0])
+ except OSError:
+ pass
+ paths.append(os.path.join(paths[0], 'lib'))
+ paths.append(os.path.join(paths[1], 'usertasks'))
+ for path in paths:
+ if not os.path.isdir(path):
+ try:
+ os.mkdir(path)
+ setPathExecuteRW(path)
+ except OSError:
+ pass
+ for path in paths[1:]:
+ fn = os.path.join(path, '__init__.py')
+ if not os.path.isfile(fn):
+ try:
+ with open(fn, mode='w') as f:
+ f.writelines('')
+ setPathExecuteRW(fn)
+ except (OSError, IOError):
+ pass
+
+
+dirn = translatepath(r'special://addondata/lib')
+usertasks = None
+createUserTasks()
+sys.path.insert(0, dirn)
+try:
+ import usertasks
+except ImportError:
+ usertasks = None
+ log(msg='Failed importing usertasks from addondata')
+if usertasks is None:
+ packages = [tasks]
+else:
+ packages = [tasks, usertasks]
+taskdict = {}
+tasktypes = []
+for package in packages:
+ prefix = package.__name__ + "."
+ for importer, modname, ispkg in pkgutil.iter_modules(package.__path__, prefix):
+ module = __import__(modname, fromlist="dummy")
+ for name, cls in module.__dict__.items():
+ try:
+ if issubclass(cls, AbstractTask):
+ if cls.tasktype != 'abstract':
+ if cls.tasktype not in tasktypes:
+ try:
+ taskdict[cls.tasktype] = {'class': cls, 'variables': cls.variables}
+ tasktypes.append(cls.tasktype)
+ except:
+ raise Exception('Error loading class for %s' % cls.tasktype)
+ except TypeError:
+ pass
diff --git a/script.service.kodi.callbacks/resources/lib/dialogtb.py b/script.service.kodi.callbacks/resources/lib/dialogtb.py
new file mode 100644
index 0000000000..d1303f467d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/dialogtb.py
@@ -0,0 +1,83 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import xbmc
+import xbmcgui
+import xbmcaddon
+import textwrap
+
+class MessageDialog(xbmcgui.WindowXMLDialog):
+
+ MESSAGE_ACTION_OK = 110
+ MESSAGE_EXIT = 111
+ MESSAGE_TITLE = 101
+ MESSAGE_TEXT = 105
+
+ def __init__(self, *args, **kwargs):
+ super(MessageDialog, self).__init__(*args, **kwargs)
+ self.msg = ''
+ self.title = ''
+
+ def set_text(self, title, msg):
+ self.msg = msg
+ self.title = title
+
+ def onInit(self):
+ self.getControl(self.MESSAGE_TITLE).setLabel(self.title)
+ # noinspection PyBroadException
+ try:
+ self.getControl(self.MESSAGE_TEXT).setText(self.msg)
+ except Exception:
+ pass
+
+ def onAction(self, action):
+ if action == 1010:
+ self.close()
+
+ def onClick(self, controlID):
+ if controlID == self.MESSAGE_ACTION_OK or controlID == self.MESSAGE_EXIT:
+ self.onAction(1010)
+
+ def onFocus(self, controlID):
+ pass
+
+
+def show_textbox(title, msg):
+ _addon_ = xbmcaddon.Addon('script.service.kodi.callbacks')
+ _cwd_ = xbmc.translatePath(_addon_.getAddonInfo('path'))
+ msgbox = MessageDialog("DialogTextBox.xml", _cwd_, "Default")
+ xt = type(msg)
+ if xt is str or xt is unicode:
+ wmsg = '\n'.join(textwrap.wrap(msg, 62))
+ elif xt is list:
+ tmsg = []
+ for i in msg:
+ omsg = textwrap.wrap(i, width=62, break_long_words=True)
+ l1 = []
+ for i1 in omsg:
+ l1.append('i=%s, len=%s' % (i1, len(i1)))
+ nmsg = '\n'.join(omsg) + '\n'
+ tmsg.append(nmsg)
+ wmsg = ''.join(tmsg)
+ else:
+ wmsg = ''
+ msgbox.set_text(title, wmsg)
+ msgbox.doModal()
+ del msg
+
diff --git a/script.service.kodi.callbacks/resources/lib/events.py b/script.service.kodi.callbacks/resources/lib/events.py
new file mode 100644
index 0000000000..b6ad67398a
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/events.py
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+def requires_subtopic():
+ return ['onFileSystemChange', 'onLogSimple', 'onLogRegex', 'onIdle', 'afterIdle', 'onWindowOpen', 'onWindowClose',
+ 'onNotification', 'onFileSystemChange', 'onStartupFileChanges', 'onDailyAlarm', 'onIntervalAlarm']
+
+
+class Events(object):
+ Player = {
+ 'onPlayBackStarted': {
+ 'text': 'on Playback Started',
+ 'reqInfo': [],
+ 'optArgs': ['mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode', 'season',
+ 'episode', 'showtitle'],
+ 'varArgs': {'%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio', '%ht': 'height',
+ '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode', '%st': 'showtitle',
+ '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'mediaType': 'movie', 'fileName': 'G:\\movies\\Star Wars - Episode IV\\movie.mkv',
+ 'title': 'Star Wars Episode IV - A New Hope', 'aspectRatio': '2.35', 'width': '1080'}
+ },
+ 'onPlayBackEnded': {
+ 'text': 'on Playback Ended',
+ 'reqInfo': [],
+ 'optArgs': ['mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode', 'season',
+ 'episode', 'showtitle', 'percentPlayed'],
+ 'varArgs': {'%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio', '%ht': 'height',
+ '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode', '%st': 'showtitle',
+ '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'mediaType': 'movie', 'fileName': 'G:\\movies\\Star Wars - Episode IV\\movie.mkv',
+ 'title': 'Star Wars Episode IV - A New Hope', 'percentPlayed': '26'}
+ },
+ 'onPlayBackPaused': {
+ 'text': 'on Playback Paused',
+ 'reqInfo': [],
+ 'optArgs': ['time', 'mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode',
+ 'season', 'episode', 'showtitle'],
+ 'varArgs': {'%tm': 'time', '%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio',
+ '%ht': 'height', '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode',
+ '%st': 'showtitle', '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'time': '235.026016235', 'mediaType': 'movie'}
+ },
+ 'onPlayBackResumed': {
+ 'text': 'on Playback Resumed',
+ 'reqInfo': [],
+ 'optArgs': ['time', 'mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode',
+ 'season', 'episode', 'showtitle'],
+ 'varArgs': {'%tm': 'time', '%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio',
+ '%ht': 'height', '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode',
+ '%st': 'showtitle', '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'mediaType': 'movie'}
+ },
+ 'onPlayBackSeek': {
+ 'text': 'on Playback Seek',
+ 'reqInfo': [],
+ 'optArgs': ['time', 'mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode',
+ 'season', 'episode', 'showtitle'],
+ 'varArgs': {'%tm': 'time', '%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio',
+ '%ht': 'height', '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode',
+ '%st': 'showtitle', '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'time': '252615'}
+ },
+ 'onPlayBackSeekChapter': {
+ 'text': 'on Playback Seek Chapter',
+ 'reqInfo': [],
+ 'optArgs': ['chapter', 'mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode',
+ 'season', 'episode', 'showtitle'],
+ 'varArgs': {'%ch': 'chapter', '%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio',
+ '%ht': 'height', '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode',
+ '%st': 'showtitle', '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'chapter': '2'}
+ },
+ 'onPlayBackSpeedChanged': {
+ 'text': 'on Playback Speed Changed',
+ 'reqInfo': [],
+ 'optArgs': ['speed', 'mediaType', 'fileName', 'title', 'aspectRatio', 'width', 'height', 'stereomode',
+ 'season', 'episode', 'showtitle'],
+ 'varArgs': {'%sp': 'speed', '%mt': 'mediaType', '%fn': 'fileName', '%ti': 'title', '%ar': 'aspectRatio',
+ '%ht': 'height', '%wi': 'width', '%sm': 'stereomode', '%se': 'season', '%ep': 'episode',
+ '%st': 'showtitle', '%at': 'artist', '%al': 'album'},
+ 'expArgs': {'speed': '2'}
+ },
+ 'onQueueNextItem': {
+ 'text': 'on Queue Next Item',
+ 'reqInfo': [],
+ 'optArgs': []
+ }
+ }
+ Monitor = {
+ 'onCleanFinished': {
+ 'text': 'on Clean Finished',
+ 'reqInfo': [],
+ 'optArgs': ['library'],
+ 'varArgs': {'%li': 'library'},
+ 'expArgs': {'library': 'movies'}
+ },
+ 'onCleanStarted': {
+ 'text': 'on Clean Started',
+ 'reqInfo': [],
+ 'optArgs': ['library'],
+ 'varArgs': {'%li': 'library'},
+ 'expArgs': {'library': 'movies'}
+ },
+ 'onDPMSActivated': {
+ 'text': 'on DPMS Activated',
+ 'reqInfo': [],
+ 'optArgs': []
+ },
+ 'onDMPSDeactivated': {
+ 'text': 'on DPMS Deactivated',
+ 'reqInfo': [],
+ 'optArgs': []
+ },
+ 'onNotification': {
+ 'text': 'on JSON Notification',
+ 'reqInfo': [('sender', 'text', ''), ('method', 'text', ''), ('data', 'text', '')],
+ 'optArgs': ['sender', 'method', 'data'],
+ 'varArgs': {'%se': 'sender', '%me': 'method', '%da': 'data'},
+ 'expArgs': {'sender': 'xbmc', 'method': 'VideoLibrary.OnScanStarted', 'data': 'null'}
+ },
+ 'onScanFinished': {
+ 'text': 'on Scan Finished',
+ 'reqInfo': [],
+ 'optArgs': ['library'],
+ 'varArgs': {'%li': 'library'},
+ 'expArgs': {'library': 'movies'}
+ },
+ 'onScanStarted': {
+ 'text': 'on Scan Started',
+ 'reqInfo': [],
+ 'optArgs': ['library'],
+ 'varArgs': {'%li': 'library'},
+ 'expArgs': {'library': 'movies'}
+ },
+ 'onScreensaverActivated': {
+ 'text': 'on Screensaver Activated',
+ 'reqInfo': [],
+ 'optArgs': []
+ },
+ 'onScreensaverDeactivated': {
+ 'text': 'on Screensaver Deactivated',
+ 'reqInfo': [],
+ 'optArgs': []
+ },
+ }
+ CustomLoop = {
+ 'onStereoModeChange': {
+ 'text': 'on Stereoscopic Mode Change ',
+ 'reqInfo': [],
+ 'optArgs': ['stereoMode'],
+ 'varArgs': {'%sm': 'stereoMode'},
+ 'expArgs': {'stereoMode': 'split_vertical'}
+ },
+ 'onProfileChange': {
+ 'text': 'on Profile Change',
+ 'reqInfo': [],
+ 'optArgs': ['profilePath'],
+ 'varArgs': {'%pp': 'profilePath'},
+ 'expArgs': {'profilePath': 'C:\\Users\\Ken User\\AppData\\Roaming\\Kodi\\userdata\\'}
+ },
+ 'onWindowOpen': {
+ 'text': 'on Window Opened',
+ 'reqInfo': [('windowIdO', 'int', 0)],
+ 'optArgs': ['windowId'],
+ 'varArgs': {'%wi': 'windowId'},
+ 'expArgs': {'windowId': '10000'}
+ },
+ 'onWindowClose': {
+ 'text': 'on Window Closed',
+ 'reqInfo': [('windowIdC', 'int', 0)],
+ 'optArgs': ['windowId'],
+ 'varArgs': {'%wi': 'windowId'},
+ 'expArgs': {'windowId': '10000'}
+ },
+ 'onIdle': {
+ 'text': 'on Idle [secs]',
+ 'reqInfo': [('idleTime', 'int', '60')],
+ 'optArgs': []
+ },
+ 'afterIdle': {
+ 'text': 'on Resume After Idle [secs]',
+ 'reqInfo': [('afterIdleTime', 'int', '60')],
+ 'optArgs': []
+ }
+ }
+ Basic = {
+ 'onStartup': {
+ 'text': 'on Startup',
+ 'reqInfo': [],
+ 'optArgs': []
+ },
+ 'onShutdown': {
+ 'text': 'on Shutdown',
+ 'reqInfo': [],
+ 'optArgs': ['pid'],
+ 'varArgs': {'%pi': 'pid'},
+ 'expArgs': {'pid': '10000'}
+ }
+ }
+ Log = {
+ 'onLogSimple': {
+ 'text': 'on Log Simple',
+ 'reqInfo': [('matchIf', 'text', ''), ('rejectIf', 'text', '')],
+ 'optArgs': ['logLine'],
+ 'varArgs': {'%ll': 'logLine'},
+ 'expArgs': {
+ 'logLine': '16:10:31 T:13092 NOTICE: fps: 23.976024, pwidth: 1916, pheight: 796, dwidth: 1916, dheight: 796'}
+ },
+ 'onLogRegex': {
+ 'text': 'on Log Regex',
+ 'reqInfo': [('matchIf', 'text', ''), ('rejectIf', 'text', '')],
+ 'optArgs': ['logLine'],
+ 'varArgs': {'%ll': 'logLine'},
+ 'expArgs': {
+ 'logLine': '16:10:31 T:13092 NOTICE: fps: 23.976024, pwidth: 1916, pheight: 796, dwidth: 1916, dheight: 796'}
+ }
+ }
+ Watchdog = {
+ 'onFileSystemChange': {
+ 'text': 'on File System Change',
+ 'reqInfo': [('folder', 'sfolder', ''), ('patterns', 'text', ''), ('ignore_patterns', 'text', ''),
+ ('ignore_directories', 'bool', 'false'), ('recursive', 'bool', 'false')],
+ 'optArgs': ['path', 'event'],
+ 'varArgs': {'%pa': 'path', '%ev': 'event'},
+ 'expArgs': {'path': 'C:\\Users\\User\\text.txt', 'event': 'deleted'}
+ }
+ }
+ WatchdogStartup = {
+ 'onStartupFileChanges': {
+ 'text': 'on File System Change at Startup',
+ 'reqInfo': [('ws_folder', 'sfolder', ''), ('ws_patterns', 'text', ''), ('ws_ignore_patterns', 'text', ''),
+ ('ws_ignore_directories', 'bool', 'false'), ('ws_recursive', 'bool', 'false')],
+ 'optArgs': ['listOfChanges'],
+ 'varArgs': {'%li': 'listOfChanges'},
+ 'expArgs': {'listOfChanges': str(
+ {'FilesDeleted': ['C:\\Users\\User\\text.txt'], 'FilesCreated': ['C:\\movies\\Fargo.mp4']})}
+ }
+ }
+ Schedule = {
+ 'onDailyAlarm': {
+ 'text': 'on Daily Alarm',
+ 'reqInfo': [('hour', 'int', '13'), ('minute', 'int', '0')],
+ 'optArgs': []
+ },
+ 'onIntervalAlarm': {
+ 'text': 'on Interval Alarm',
+ 'reqInfo': [('hours', 'int', '0'), ('minutes', 'int', '30'), ('seconds', 'int', '0')],
+ 'optArgs': []
+ },
+ }
+
+ def __init__(self):
+ self.AllEvents = self._AllEvents()
+ self.AllEventsSimple = self._AllEventsSimple()
+
+ @staticmethod
+ def mergedicts(*dicts):
+ result = {}
+ for d in dicts:
+ result.update(d)
+ return result
+
+ @staticmethod
+ def _AllEvents():
+ return Events.mergedicts(Events.Player, Events.Monitor, Events.CustomLoop, Events.Basic, Events.Log,
+ Events.Watchdog, Events.WatchdogStartup, Events.Schedule)
+
+ @staticmethod
+ def _AllEventsSimple():
+ return Events._AllEvents().keys()
diff --git a/script.service.kodi.callbacks/resources/lib/helpers/onQuit.bat b/script.service.kodi.callbacks/resources/lib/helpers/onQuit.bat
new file mode 100644
index 0000000000..4d99059acd
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/helpers/onQuit.bat
@@ -0,0 +1,9 @@
+@echo off
+:loop
+tasklist | find " %1 " >nul
+if not errorlevel 1 (
+ timeout /t 2 >nul
+ goto :loop
+)
+REM Put the code you want to run here
+notepad.exe
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/helpers/onQuit.sh b/script.service.kodi.callbacks/resources/lib/helpers/onQuit.sh
new file mode 100644
index 0000000000..feb387695d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/helpers/onQuit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+while kill -0 $1 > /dev/null 2>&1
+do
+ sleep 0.2
+done
+# insert your code here
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/helpers/sudohelper.sh b/script.service.kodi.callbacks/resources/lib/helpers/sudohelper.sh
new file mode 100644
index 0000000000..4f69919984
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/helpers/sudohelper.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+bash "$*"
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/kodilogging.py b/script.service.kodi.callbacks/resources/lib/kodilogging.py
new file mode 100644
index 0000000000..954a656d88
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/kodilogging.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+import xbmc
+import threading
+
+def log(loglevel=xbmc.LOGNOTICE, msg=''):
+ if isinstance(msg, str):
+ msg = msg.decode("utf-8")
+ message = u"$$$ [%s] - %s" % ('kodi.callbacks', msg)
+ xbmc.log(msg=message.encode("utf-8"), level=loglevel)
+
+class KodiLogger(object):
+ LOGDEBUG = 0
+ LOGERROR = 4
+ LOGFATAL = 6
+ LOGINFO = 1
+ LOGNONE = 7
+ LOGNOTICE = 2
+ LOGSEVERE = 5
+ LOGWARNING = 3
+ _instance = None
+ _lock = threading.Lock()
+ selfloglevel = xbmc.LOGDEBUG
+ kodirunning = True
+
+ def __new__(cls):
+ if xbmc.getFreeMem() == long():
+ KodiLogger.kodirunning = False
+ if KodiLogger._instance is None:
+ with KodiLogger._lock:
+ if KodiLogger._instance is None:
+ KodiLogger._instance = super(KodiLogger, cls).__new__(cls)
+ return KodiLogger._instance
+
+ def __init__(self):
+ KodiLogger._instance = self
+
+ @staticmethod
+ def setLogLevel(arg):
+ KodiLogger.selfloglevel = arg
+
+ @staticmethod
+ def log(loglevel=None, msg=''):
+ if loglevel is None:
+ loglevel = KodiLogger.selfloglevel
+ if isinstance(msg, str):
+ msg = msg.decode("utf-8")
+ if KodiLogger.kodirunning:
+ message = u"$$$ [%s] - %s" % ('kodi.callbacks', msg)
+ xbmc.log(msg=message.encode("utf-8"), level=loglevel)
+ else:
+ print msg
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/kodisettings/__init__.py b/script.service.kodi.callbacks/resources/lib/kodisettings/__init__.py
new file mode 100644
index 0000000000..0463614978
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/kodisettings/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
diff --git a/script.service.kodi.callbacks/resources/lib/kodisettings/generate_xml.py b/script.service.kodi.callbacks/resources/lib/kodisettings/generate_xml.py
new file mode 100644
index 0000000000..a40761e033
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/kodisettings/generate_xml.py
@@ -0,0 +1,317 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import codecs
+import os
+
+import xbmcaddon
+from resources.lib import taskdict
+from resources.lib.events import Events
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.kodisettings import struct
+from resources.lib.utils.poutil import KodiPo, PoDict
+
+kodipo = KodiPo()
+kodipo.updateAlways = True
+glsid = kodipo.getLocalizedStringId
+__ = kodipo.podict.has_msgctxt
+kl = KodiLogger()
+log = kl.log
+
+podict = PoDict()
+from default import branch as branch
+
+pofile = os.path.join(xbmcaddon.Addon('script.service.kodi.callbacks').getAddonInfo('path').decode("utf-8"),
+ 'resources', 'language', 'English', 'strings.po')
+if pofile.startswith('resources'):
+ pofile = r'C:\Users\Ken User\AppData\Roaming\Kodi\addons\script.service.kodi.callbacks\resources\language\English\strings.po'
+
+podict.read_from_file(pofile)
+
+
+def generate_settingsxml(fn=None):
+ taskcontrols, tasks = createTasks()
+ eventcontrols, podirty = createEvents(tasks)
+ generalcontrols = createGeneral()
+ updatecontrols = createUpdate()
+ settings = struct.Settings()
+ mysettings = [('Tasks', taskcontrols), ('Events', eventcontrols), ('General', generalcontrols),
+ ('Update', updatecontrols)]
+ for category, controls in mysettings:
+ settings.addCategory(struct.Category(category))
+ for control in controls:
+ settings.addControl(category, control)
+
+ output = settings.render()
+ writetofile(fn, output)
+ if podirty is True:
+ podict.write_to_file(pofile)
+
+
+def createTasks():
+ taskchoices = ['none']
+ for key in sorted(taskdict.keys()):
+ taskchoices.append(key)
+ tasks = []
+ last_id = None
+ taskcontrols = []
+ for i in xrange(1, 11):
+ tasks.append('Task %i' % i)
+ prefix = "T%s" % str(i)
+ curTaskType = '%s.type' % prefix
+ if i == 1:
+
+ taskcontrols.append(struct.Lsep('%s.div' % prefix, 'Task %i' % i))
+ taskcontrols.append(struct.LabelEnum('%s.type' % prefix, 'Task', default='none', lvalues=taskchoices))
+ else:
+ conditional = struct.Conditional(struct.Conditional.OP_NOT_EQUAL, 'none', last_id)
+ taskcontrols.append(struct.Lsep('%s.div' % prefix, 'Task %i' % i, visible=conditional))
+ taskcontrols.append(
+ struct.LabelEnum('%s.type' % prefix, 'Task', default='none', lvalues=taskchoices, visible=conditional))
+ conditional = struct.Conditional(struct.Conditional.OP_NOT_EQUAL, 'none', curTaskType)
+ taskcontrols.append(
+ struct.Number('%s.maxrunning' % prefix, 'Max num of this task running simultaneously (-1=no limit)',
+ default=-1, visible=conditional))
+ taskcontrols.append(
+ struct.Number('%s.maxruns' % prefix, 'Max num of times this task runs (-1=no limit)', default=-1,
+ visible=conditional))
+ taskcontrols.append(struct.Number('%s.refractory' % prefix, 'Refractory period in secs (-1=none)', default=-1,
+ visible=conditional))
+
+ for key in sorted(taskdict.keys()):
+ for var in taskdict[key]['variables']:
+ varset = var['settings']
+ if varset['type'] == 'sfile':
+ mytype = 'browser'
+ elif varset['type'] == 'file':
+ mytype = 'browser'
+ else:
+ mytype = varset['type']
+ try:
+ option = varset['option']
+ except KeyError:
+ conditionals = struct.Conditional(struct.Conditional.OP_EQUAL, unicode(key), curTaskType)
+ if varset['type'] == 'sfile':
+ labelbrowse = u'%s - browse' % varset['label']
+ labeledit = u'%s - edit' % varset['label']
+ taskcontrols.append(
+ struct.FileBrowser(u'%s.%s' % (prefix, var['id']), labelbrowse, default=varset['default'],
+ fbtype=struct.FileBrowser.TYPE_FILE, visible=conditionals))
+ taskcontrols.append(
+ struct.Text(u'%s.%s' % (prefix, var['id']), labeledit, default=varset['default'],
+ visible=conditionals))
+ elif varset['type'] == 'labelenum':
+ Control = struct.getControlClass[mytype]
+ taskcontrols.append(
+ Control('%s.%s' % (prefix, var['id']), label=varset['label'], values=varset['values'],
+ default=varset['default'],
+ visible=conditionals))
+ else:
+ Control = struct.getControlClass[mytype]
+ taskcontrols.append(
+ Control('%s.%s' % (prefix, var['id']), label=varset['label'],
+ default=varset['default'],
+ visible=conditionals))
+ else:
+ conditionals = struct.Conditional(struct.Conditional.OP_EQUAL, unicode(key), curTaskType)
+ if varset['type'] == 'sfile':
+ labelbrowse = '%s - browse' % varset['label']
+ labeledit = '%s - edit' % varset['label']
+ taskcontrols.append(
+ struct.FileBrowser('%s.%s' % (prefix, var['id']), labelbrowse, default=varset['default'],
+ option=option, fbtype=struct.FileBrowser.TYPE_FILE,
+ visible=conditionals))
+ taskcontrols.append(
+ struct.Text('%s.%s' % (prefix, var['id']), labeledit, default=varset['default'],
+ visible=conditionals))
+ else:
+ Control = struct.getControlClass[mytype]
+ taskcontrols.append(
+ Control('%s.%s' % (prefix, var['id']), varset['label'], default=varset['default'],
+ option=option, visible=conditionals))
+ last_id = curTaskType
+ return taskcontrols, tasks
+
+
+def createEvents(tasks):
+ podirty = False
+ allevts = Events().AllEvents
+ evts = []
+ for evtkey in allevts.keys():
+ evts.append(allevts[evtkey]['text'])
+ evts.sort()
+ evts.insert(0, 'None')
+ levts = []
+ for evt in evts:
+ levts.append(glsid(evt))
+ levts = "|".join(levts)
+ eventcontrols = []
+
+ last_id = None
+ for i in xrange(1, 11):
+ prefix = 'E%s' % str(i)
+ curEvtType = '%s.type' % prefix
+ action_evt = 'RunScript(script.service.kodi.callbacks, lselector, id=%s.type, heading=%s, lvalues=%s)' % (
+ prefix, glsid('Choose event type'), levts)
+ if i == 1:
+ eventcontrols.append(struct.Lsep('%s.lsep' % prefix, 'Event %i' % i))
+ eventcontrols.append(
+ struct.Action('%s.action' % prefix, 'Choose event type (click here)', action=action_evt))
+ eventcontrols.append(
+ struct.Select('%s.type-v' % prefix, 'Event:', default='None', enable=False, lvalues=evts))
+ else:
+ conditionals = struct.Conditional(struct.Conditional.OP_NOT_EQUAL, glsid('None'), last_id)
+ eventcontrols.append(struct.Lsep('%s.lsep' % prefix, 'Event %i' % i, visible=conditionals))
+ eventcontrols.append(
+ struct.Action('%s.action' % prefix, 'Choose event type (click here)', action=action_evt,
+ visible=conditionals))
+ eventcontrols.append(
+ struct.Select('%s.type-v' % prefix, 'Event:', default=glsid('None'), enable=False, lvalues=evts,
+ visible=conditionals))
+ conditionals = struct.Conditional(struct.Conditional.OP_NOT_EQUAL, glsid('None'), curEvtType)
+ eventcontrols.append(struct.Text(curEvtType, '', default=glsid('None'), visible=False))
+
+ eventcontrols.append(
+ struct.LabelEnum('%s.task' % prefix, 'Task', default='Task 1', lvalues=tasks, visible=conditionals))
+
+ for evtkey in allevts.keys():
+ evt = allevts[evtkey]
+ conditionals = struct.Conditional(struct.Conditional.OP_EQUAL, glsid(evt['text']), curEvtType)
+ for req in evt['reqInfo']:
+ r1 = req[1]
+ if r1 in ['float', 'int']:
+ r1 = 'number'
+ if r1 == 'sfolder':
+ mytype = 'browse'
+ else:
+ mytype = r1
+
+ if r1 == 'sfolder':
+ labelbrowse = '%s - browse' % req[0]
+ labeledit = '%s - edit' % req[0]
+ eventcontrols.append(
+ struct.FileBrowser('%s.%s' % (prefix, req[0]), labelbrowse, struct.FileBrowser.TYPE_FOLDER,
+ default=req[2], visible=conditionals))
+ eventcontrols.append(
+ struct.Text('%s.%s' % (prefix, req[0]), labeledit, default=req[2], visible=conditionals))
+ else:
+ Control = struct.getControlClass[mytype]
+ eventcontrols.append(
+ Control(sid='%s.%s' % (prefix, req[0]), label=req[0], default=req[2], visible=conditionals))
+ eventcontrols.append(
+ struct.Lsep(label='Note: variables can be subbed (%%=%, _%=space, __%=,):', visible=conditionals))
+ try:
+ vargs = evt['varArgs']
+ except KeyError:
+ vargs = {}
+ vs = []
+ for key in vargs.keys():
+ vs.append('%s=%s' % (key, vargs[key]))
+ vs = ','.join(vs)
+ brk = 60
+ if len(vs) > 0:
+ if len(vs) < brk:
+ found, strid = podict.has_msgid(vs)
+ if found is False:
+ podict.addentry(strid, vs)
+ podirty = True
+ eventcontrols.append(struct.Lsep(label=vs, visible=conditionals))
+ else:
+ startindex = 0
+ x = 0
+ lines = []
+ while startindex + brk < len(vs):
+ x = vs.rfind(',', startindex, startindex+brk)
+ found, strid = podict.has_msgid(vs[startindex:x])
+ if found is False:
+ podict.addentry(strid, vs[startindex:x])
+ podirty = True
+ lines.append(vs[startindex:x])
+ startindex = x + 1
+ found, strid = podict.has_msgid(vs[x + 1:])
+ if found is False:
+ podict.addentry(strid, vs[x + 1:])
+ podirty = True
+ lines.append(vs[x+1:])
+ for line in lines:
+ eventcontrols.append(struct.Lsep(label=line, visible=conditionals))
+ conditionals = struct.Conditional(struct.Conditional.OP_NOT_EQUAL, unicode(glsid('None')), curEvtType)
+ eventcontrols.append(
+ struct.Text('%s.userargs' % prefix, 'Var subbed arg string', default='', visible=conditionals))
+ eventcontrols.append(struct.Action('%s.test' % prefix, 'Test Command (click OK to save changes first)',
+ action='RunScript(script.service.kodi.callbacks, %s)' % prefix,
+ visible=conditionals))
+ last_id = curEvtType
+ return eventcontrols, podirty
+
+
+def createGeneral():
+ generalcontrols = []
+ generalcontrols.append(struct.Bool('Notify', 'Display Notifications when Tasks Run?', default=False))
+ generalcontrols.append(struct.Number('LoopFreq', 'Loop Pooling Frequency (ms)', default=500))
+ generalcontrols.append(struct.Number('LogFreq', 'Log Polling Frequency (ms)', default=500))
+ generalcontrols.append(struct.Number('TaskFreq', 'Task Polling Frequency (ms)', default=100))
+ generalcontrols.append(struct.Bool('loglevel', 'Show debugging info in normal log?', default=False))
+ generalcontrols.append(struct.Action('logsettings', 'Write Settings into Kodi log',
+ action='RunScript(script.service.kodi.callbacks, logsettings)'))
+ generalcontrols.append(struct.Action('uploadlog', 'Upload Log', action='RunScript(script.xbmc.debug.log)',
+ visible=struct.Conditionals(struct.Conditional(struct.Conditional.OP_HAS_ADDON,
+ 'script.xbmc.debug.log'))))
+ generalcontrols.append(
+ struct.Action('regen', 'Regenerate settings.xml (Developers Only)',
+ action='RunScript(script.service.kodi.callbacks, regen)'))
+ generalcontrols.append(struct.Action('test', 'Test addon native tasks (see log for output)',
+ action='RunScript(script.service.kodi.callbacks, test)'))
+ return generalcontrols
+
+
+def createUpdate():
+ updatecontrols = []
+ updatecontrols.append(struct.Lsep(label='Before any installation, the current is backed up to userdata/addon_data'))
+ if branch != 'master':
+ updatecontrols.append(
+ struct.Text('installed branch', 'Currently installed branch', default='nonrepo', enable=False))
+ updatecontrols.append(struct.Select('repobranchname', 'Repository branch name for downloads', default='nonrepo',
+ values=['master', 'nonrepo']))
+ updatecontrols.append(
+ struct.Bool('autodownload', 'Automatically download/install latest from GitHub on startup?', default=False))
+ updatecontrols.append(struct.Action('checkupdate', 'Check for update on GitHub',
+ action='RunScript(script.service.kodi.callbacks, checkupdate)'))
+ updatecontrols.append(struct.Bool('silent_install', 'Install without prompts?', visible=False, default=False))
+ updatecontrols.append(struct.Action('updatefromzip', 'Update from downloaded zip',
+ action='RunScript(script.service.kodi.callbacks, updatefromzip)'))
+ updatecontrols.append(struct.Action('restorebackup', 'Restore from previous back up',
+ action='RunScript(script.service.kodi.callbacks, restorebackup)'))
+ return updatecontrols
+
+
+def writetofile(fn, output):
+ if fn is None:
+ fn = os.path.join(xbmcaddon.Addon('script.service.kodi.callbacks').getAddonInfo('path').decode("utf-8"),
+ 'resources', 'settings.xml')
+ with codecs.open(fn, 'wb', 'UTF-8') as f:
+ f.writelines(output)
+ try:
+ log(msg='Settings.xml rewritten')
+ except TypeError:
+ pass
+
+
+if __name__ == '__main__':
+ generate_settingsxml(
+ r'C:\Users\Ken User\AppData\Roaming\Kodi\addons\script.service.kodi.callbacks\resources\settings.xml')
diff --git a/script.service.kodi.callbacks/resources/lib/kodisettings/struct.py b/script.service.kodi.callbacks/resources/lib/kodisettings/struct.py
new file mode 100644
index 0000000000..043e5ae58f
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/kodisettings/struct.py
@@ -0,0 +1,1210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import abc
+from resources.lib.utils.poutil import KodiPo
+from resources.lib.kodilogging import KodiLogger
+log = KodiLogger.log
+kodipo = KodiPo()
+kodipo.updateAlways = True
+_ = kodipo.getLocalizedStringId
+
+def getSettingMock(sid):
+ assert isinstance(sid, str) or isinstance(sid, unicode)
+ return 'none'
+
+try:
+ import xbmcaddon
+ getSetting = xbmcaddon.Addon().getSetting
+except ImportError:
+ getSetting = getSettingMock
+
+class Settings(object):
+ """
+ Class representing the top level portion of the settings.xml
+ Contains categories
+ """
+
+ def __init__(self):
+ self._categories = []
+ self._controldict = {}
+ self._controldictbyrefid = {}
+ self.id_position = {}
+ self.duplicateids = []
+ self.values = {}
+
+ def category(self, label):
+ """
+ Retrieves a category by label
+ :param label: English non-localized unicode or string for label
+ :type label: unicode or str
+ :return:
+ :rtype: Category
+ """
+ assert (isinstance(label, unicode) or isinstance(label, str))
+ label = unicode(label)
+ ret = None
+ for c in self._categories:
+ if c.label == label:
+ ret = c
+ break
+ if ret is None:
+ raise KeyError('Category not found for: %s' % label)
+ else:
+ return ret
+
+ def addCategory(self, category):
+ """
+ Add a category. Categories are listed as tabbed pages in settings.
+ :param category:
+ :type category: Category or str or unicode
+ :return:
+ :rtype: Category
+ """
+ if isinstance(category, str) or isinstance(category, unicode):
+ category = Category(unicode(category))
+ self._categories.append(category)
+ elif isinstance(category, Category):
+ self._categories.append(category)
+ else:
+ raise TypeError('Add Category must be string or Category')
+ return category
+
+ def addControl(self, catgorylabel, control):
+ """
+ Add a control to a specific category.
+ :param catgorylabel:
+ :type catgorylabel: unicode or str
+ :param control:
+ :type control: Control
+ """
+ assert isinstance(catgorylabel, unicode) or isinstance(catgorylabel, str)
+ assert isinstance(control, Control)
+ try:
+ c = self.category(catgorylabel)
+ except KeyError:
+ raise KeyError('No category created with that label: %s' % catgorylabel)
+ else:
+ c.addControl(control)
+ if control.internal_ref != u'':
+ if self._controldict.has_key(control.internal_ref):
+ log(msg='Warning - control with duplicate internal reference: %s' % control.internal_ref)
+ else:
+ self._controldict[control.internal_ref] = control
+
+
+ def control(self, internal_ref):
+ """
+ Returns the instance of Control associated with the internal reference id, if the control has a unique id.
+ Unless a specific internal reference id was provided during instantiation, the sid is the internal
+ reference id.
+ :param internal_ref:
+ :type internal_ref: unicode or str
+ :return:
+ :rtype: Control
+ """
+ assert isinstance(internal_ref, unicode) or isinstance(internal_ref, str)
+ internal_ref = unicode(internal_ref)
+ try:
+ return self._controldict[internal_ref]
+ except KeyError:
+ raise KeyError("No control found for sid: %s" % internal_ref)
+
+ @staticmethod
+ def renderHead():
+ line1 = u''
+ line2 = u''
+ return [line1, line2]
+
+ @staticmethod
+ def renderTail():
+ return [u'']
+
+ def render(self):
+ """
+ Renders the xml version of the contained Categories and their Controls.
+ :return: Unicode string representing the xml
+ :rtype: unicode
+ """
+ self.buildIndices()
+ output = self.renderHead()
+ for category in self._categories:
+ output += category.render(self)
+ output += self.renderTail()
+ output = u'\n'.join(output)
+ return output
+
+ def buildIndices(self):
+ """
+ Builds the index of positions of the currently contained categories and controls to be used for referenced
+ conditionals.
+ """
+ curindex = 3
+ for category in self._categories:
+ curindex += 1
+ for control in category._controls:
+ curindex += control.weight
+ if control.internal_ref != u'':
+ if not self.id_position.has_key(control.internal_ref):
+ self.id_position[control.internal_ref] = curindex
+ else:
+ log(msg='Warning duplicate key found: %s [%s, %s]' % (control.internal_ref, control.label, self.control(control.internal_ref).label))
+ self.duplicateids.append(control.internal_ref)
+ control._outputindex = curindex
+ curindex += 1
+
+ def read(self):
+ for category in self._categories:
+ self.values.update(category.read())
+
+
+class Category(object):
+ """
+ Class representing a category.
+ """
+ def __init__(self, label):
+ """
+ :param label:
+ :type label: unicode or str
+ """
+ assert (isinstance(label, unicode) or isinstance(label, str))
+ self.label = unicode(label)
+ self._controls = []
+ self.indent = u' '
+
+ def addControl(self, control):
+ """
+ Adds a control to the category
+ :param control:
+ :type control: Control
+ """
+ assert isinstance(control,Control)
+ self._controls.append(control)
+
+ def control(self, sid):
+ """
+ Retrieves control by sid
+ :param sid:
+ :type sid: unicode
+ :return:
+ :rtype: Control
+ """
+ assert isinstance(sid, unicode)
+ ret = None
+ for c in self._controls:
+ if c.id == sid:
+ ret = c
+ break
+ if ret is None:
+ raise KeyError("Control not found for id: %s" % sid)
+ else:
+ return ret
+
+ def renderHead(self):
+ return [u'%s' % (self.indent, _(self.label))]
+
+ def renderTail(self):
+ return [u'%s' % self.indent]
+
+ def render(self, parent):
+ """
+ Renders the category and its controls to xml
+ :param parent:
+ :type parent: resources.lib.kodisettings.struct.Settings
+ :return:
+ :rtype: unicode
+ """
+ output = self.renderHead()
+ for control in self._controls:
+ assert isinstance(control, Control)
+ tmp = control.render(parent)
+ output += tmp
+ output += self.renderTail()
+ return output
+
+ def read(self):
+ ret = {}
+ for control in self._controls:
+ ret.update(control.read())
+
+class Control(object):
+ """
+ Base class for controls
+ """
+ __metaclass__ = abc.ABCMeta
+ def __init__(self, stype, sid=u'', label=u'', enable=None, visible=None, subsetting=False, weight=1, internal_ref=None):
+ """
+
+ :param stype: The internally used type of control.
+ :type stype: str
+ :param sid: The settings id for the control.
+ :type sid: unicode or str
+ :param label: The label to be displayed. If it is a five digit integer, it will look in the po file for a localized string.
+ :type label: unicode or str
+ :param enable: If evaluates to false, the control is shown as greyed out and disabled.
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible: If evaluates to false, the control is not visible (hidden).
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting: If true, a dash is shown before the label.
+ :type subsetting: bool
+ :param weight: Internally used; the number of lines the control consumes.
+ :type weight: int
+ :param internal_ref: If needed, a separate internal reference id for the control. Needed if more than one control shares the same sid (and values).
+ :type internal_ref: unicode or string
+ """
+ if isinstance(sid, str):
+ sid = unicode(sid)
+ if isinstance(label, str):
+ label = unicode(label)
+ assert isinstance(sid, unicode)
+ assert isinstance(label, unicode)
+ if enable is not None:
+ if isinstance(enable, bool):
+ enable = Conditionals(Conditional(Conditional.OP_BOOLEAN, enable))
+ elif isinstance(enable, Conditional):
+ enable = Conditionals(enable)
+ else:
+ assert isinstance(enable, Conditionals)
+ if visible is not None:
+ if isinstance(visible, bool):
+ visible = Conditionals(Conditional(Conditional.OP_BOOLEAN, visible))
+ elif isinstance(visible, Conditional):
+ visible = Conditionals(visible)
+ else:
+ assert isinstance(visible, Conditionals)
+ if internal_ref is not None:
+ assert isinstance(internal_ref, str) or isinstance(internal_ref, unicode)
+ self.internal_ref = unicode(internal_ref)
+ else:
+ self.internal_ref = sid
+ self.sid = sid
+ self.stype = stype
+ self.label = label
+ self.enable = enable
+ self.visible = visible
+ self.subsetting = subsetting
+ self._outputindex = 0
+ self.indent = u' '
+ self.weight = weight
+
+ @abc.abstractmethod
+ def render(self, parent):
+ pass
+
+ def requiredrenderlist(self, parent):
+ """
+
+ :param parent: A reference to the calling instantiation of Setttings.
+ :type parent: resources.lib.kodisettings.struct.Settings
+ :return: A list of rendered unicode strings.
+ :rtype:list
+ """
+ if self.sid != u'':
+ elements = [u'id="%s"' % self.sid]
+ else:
+ elements = []
+ elements += [u'label="%s"' % _(self.label)]
+ elements += [u'type="%s"' % self.stype]
+ if self.subsetting is True:
+ elements += [u'subsetting="true"']
+ if self.enable is not None:
+ elements += [u'enable="%s"' % self.enable.render(self, parent)]
+ if self.visible is not None:
+ elements += [u'visible="%s"' % self.visible.render(self, parent)]
+ return elements
+
+ def read(self):
+ if self.sid != u'':
+ return {self.sid: getSetting(self.sid)}
+ else:
+ return None
+
+class Sep(Control):
+ """
+ Adds a horizontal separating line between other elements.
+ """
+ def __init__(self):
+ super(Sep, self).__init__('sep')
+
+ def render(self, parent):
+ assert isinstance(parent, Settings)
+ return [u'%s' % self.indent]
+
+class Lsep(Control):
+ """
+ Shows a horizontal line with a text.
+ """
+ def __init__(self, sid=u'', label=u'', visible=None):
+ """
+
+ :param sid: The settings id for the control.
+ :type sid: unicode or str
+ :param label: The label to be displayed. If it is a five digit integer, it will look in the po file for a localized string.
+ :type label: unicode or str
+ :param visible: If evaluates to false, the control is not visible (hidden).
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ """
+ super(Lsep, self).__init__('lsep', sid, label, visible=visible)
+
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+
+class Text(Control):
+ """
+ Text input elements allow a user to input text in various formats.
+ """
+ def __init__(self, sid=u'', label=u'', enable=None, visible=None, subsetting=False, option=None, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param option: "hidden"|"urlencoded" (optional)
+ :type option: unicode or str
+ :param default: (optional) - the default value.
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Text, self).__init__('text', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ self.option = option
+ if option is not None:
+ if option == 'hidden':
+ self.option = unicode(option)
+ elif option == 'urlencoded':
+ self.option = unicode(option)
+ else:
+ raise SyntaxError('Text "option" not defined: %s' % option)
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Ipaddress(Control):
+ """
+
+ """
+ def __init__(self, sid=u'', label=u'', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Ipaddress, self).__init__('ipaddress', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Number(Control):
+ """
+ Allows the user to enter an integer using up/down buttons.
+ """
+ def __init__(self, sid=u'', label=u'', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: int
+ :return:
+ :rtype:
+ """
+ super(Number, self).__init__('number', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ if default is not None:
+ if isinstance(default, int):
+ self.default = unicode(str(default))
+ else:
+ self.default = unicode(default)
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+ def read(self):
+ try:
+ value = int(getSetting(self.sid))
+ except ValueError:
+ value = 0
+ return {self.sid: value}
+
+
+class Slider(Control):
+ """
+ Allows the user to enter a number using a horizontal sliding bar.
+ """
+ def __init__(self, sid=u'', label=u'', srangemin=0, srangemax=100, srangestep=None, option=u'int', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param srangemin:
+ :type srangemin: int or float or str or unicode
+ :param srangemax:
+ :type srangemax: int or float or str or unicode
+ :param srangestep:
+ :type srangestep: int or float or str or unicode
+ :param option:
+ :type option: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting:bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+
+ super(Slider, self).__init__('slider', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ self.srangemin = unicode(srangemin)
+ self.srangemax = unicode(srangemax)
+ if srangestep is not None:
+ self.srangestep = unicode(srangestep)
+ else:
+ self.srangestep = u''
+ assert isinstance(option, unicode) or isinstance(option, str)
+ option = unicode(option)
+ if option == u'int' or option == u'float' or option == u'percent':
+ self.option = option
+ else:
+ raise SyntaxError('Slider option must be "int|float|percent" got: %s' % option)
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+ def read(self):
+ rawvalue = getSetting(self.sid)
+ if self.option == u'int':
+ try:
+ value = int(rawvalue)
+ except ValueError:
+ value = 0
+ else:
+ try:
+ value = float(rawvalue)
+ except ValueError:
+ value = 0.00
+ else:
+ if self.option == u'percent':
+ value *= 100.0
+ return {self.sid:value}
+
+class Date(Control):
+ """
+ Displays a date picker dialog box.
+ """
+ def __init__(self, sid=u'', label=u'', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Date, self).__init__('date', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Time(Control):
+ """
+ Displays a time picker dialog box.
+ """
+ def __init__(self, sid=u'', label=u'', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Time, self).__init__('time', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Bool(Control):
+ """
+ Boolean input elements allow a user to switch a setting on or off.
+ """
+ def __init__(self, sid, label, enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str or bool
+ :return:
+ :rtype:
+ """
+ super(Bool, self).__init__('bool', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ if default is not None:
+ if isinstance(default, unicode) or isinstance(default, str):
+ if default.lower() == 'true':
+ self.default = u'true'
+ else:
+ self.default = u'false'
+ elif isinstance(default, bool):
+ if default is True:
+ self.default = u'true'
+ else:
+ self.default = u'false'
+ elif isinstance(default, int):
+ if default == 0:
+ self.default = u'false'
+ else:
+ self.default = u'true'
+ else:
+ raise TypeError ('Undefined type for default in Bool control')
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+ def read(self):
+ return {self.sid:getSetting(self.sid)=='true'}
+
+class Select(Control):
+ """
+ Will open separate selection window
+ """
+ def __init__(self, sid=u'', label=u'', values=None, lvalues=None, enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param values:
+ :type values: list
+ :param lvalues:
+ :type lvalues: list
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Select, self).__init__('select', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert ((values is not None and lvalues is None) or (lvalues is not None and values is None))
+ if lvalues is not None:
+ self.usinglvalues = True
+ else:
+ self.usinglvalues = False
+ if self.usinglvalues:
+ assert isinstance(lvalues, list)
+ self.values = lvalues
+ else:
+ assert isinstance(values, list)
+ self.values = values
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Addon(Control):
+ """
+ Displays a selection window with a list of addons.
+ """
+ def __init__(self, sid=u'', label=u'', addontype=u'xbmc.metadata.scraper.movies', multiselect=None, enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param addontype:
+ :type addontype: unicode or str
+ :param multiselect:
+ :type multiselect: bool
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Addon, self).__init__('addon', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert (isinstance(addontype, unicode) or isinstance(addontype, str))
+ self.addontype = unicode(addontype)
+ if multiselect is not None:
+ assert isinstance(multiselect, bool)
+ if multiselect is True:
+ self.multiselect = u'true'
+ else:
+ self.multiselect = u'false'
+ else:
+ self.multiselect = None
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Enum(Control):
+ """
+ A rotary selector allows the user to selected from a list of predefined values using the index of the chosen value.
+ """
+ def __init__(self, sid=u'', label=u'', values=None, lvalues=None, enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param values:
+ :type values: list
+ :param lvalues:
+ :type lvalues: list
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(Enum, self).__init__('enum', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert ((values is not None and lvalues is None) or (lvalues is not None and values is None))
+ if lvalues is not None:
+ self.usinglvalues = True
+ else:
+ self.usinglvalues = False
+ if self.usinglvalues:
+ assert isinstance(lvalues, list)
+ self.values = lvalues
+ else:
+ assert (isinstance(values, list) or values == u'$HOURS')
+ self.values = values
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+ def read(self):
+ index = int(getSetting(self.sid))
+ if self.values == u'$HOURS':
+ return {self.sid:index}
+ else:
+ return {self.sid:self.values[index]}
+
+class LabelEnum(Control):
+ """
+ A rotary selector allows the user to selected from a list of predefined values using the actual value of the chosen value.
+ """
+ def __init__(self, sid=u'', label=u'', values=None, lvalues=None, sort=u'no', enable=None, visible=None, subsetting=False, default=None, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param values:
+ :type values: list
+ :param lvalues:
+ :type lvalues: list
+ :param sort:
+ :type sort: unicode or str or bool
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting:
+ :param default: bool
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(LabelEnum, self).__init__('labelenum', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert ((values is not None and lvalues is None) or (lvalues is not None and values is None))
+ if lvalues is not None:
+ self.usinglvalues = True
+ else:
+ self.usinglvalues = False
+ if self.usinglvalues:
+ assert isinstance(lvalues, list)
+ self.values = lvalues
+ else:
+ assert isinstance(values, list)
+ self.values = values
+ assert (sort == u'yes' or sort == u'no' or isinstance(sort, bool))
+ if isinstance(sort, bool):
+ if sort is True:
+ self.sort = u'yes'
+ else:
+ self.sort = u'no'
+ else:
+ self.sort = sort
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class FileBrowser(Control):
+ """
+
+ """
+ TYPE_FILE = 0
+ TYPE_AUDIO = 1
+ TYPE_VIDEO = 2
+ TYPE_IMAGE = 3
+ TYPE_EXECUTABLE = 4
+ TYPE_FOLDER = 5
+ TYPE_FILE_ENUM = 6
+ TYPE_DICT = {TYPE_FILE:'file', TYPE_AUDIO:'audio', TYPE_VIDEO:'video', TYPE_IMAGE:'image', TYPE_EXECUTABLE:'executable', TYPE_FOLDER:'folder', TYPE_FILE_ENUM:'fileenum'}
+
+ def __init__(self, sid=u'', label=u'', fbtype=TYPE_FILE, source=u'auto', option=u'', mask=u'', enable=None, visible=None, subsetting=False, default=u'', internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param fbtype:
+ :type fbtype:
+ :param source:
+ :type source: unicode or str
+ :param option:
+ :type option: unicode or str
+ :param mask:
+ :type mask: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :param default:
+ :type default: unicode or str
+ :return:
+ :rtype:
+ """
+ super(FileBrowser, self).__init__(self.TYPE_DICT[fbtype], sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert (self.TYPE_FILE <= fbtype <= self.TYPE_FILE_ENUM)
+ self.fbtype = fbtype
+ if fbtype == self.TYPE_FOLDER:
+ self.source = source
+ assert (option == u'' or option == 'writeable')
+ self.option = option
+ else:
+ assert (option == u'' or option == u'hideext')
+ self.option = option
+ assert isinstance(mask, unicode) or isinstance(mask, str)
+ self.mask = mask
+ if default is not None:
+ assert (isinstance(default, unicode) or isinstance(default, str))
+ self.default = unicode(default)
+ else:
+ self.default = None
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+class Action(Control):
+ """
+
+ """
+ def __init__(self, sid=u'', label=u'', action=u'', enable=None, visible=None, subsetting=False, internal_ref=None):
+ """
+
+ :param sid:
+ :type sid: unicode or str
+ :param label:
+ :type label: unicode or str
+ :param action:
+ :type action: unicode or str
+ :param enable:
+ :type enable: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param visible:
+ :type visible: NoneType or resources.lib.kodisettings.struct.Conditionals or bool or resources.lib.kodisettings.struct.Conditional
+ :param subsetting:
+ :type subsetting: bool
+ :return:
+ :rtype:
+ """
+ super(Action, self).__init__('action', sid, label, enable, visible, subsetting, internal_ref=internal_ref)
+ assert (isinstance(action, unicode) or isinstance(action, str))
+ self.action = unicode(action)
+
+ def render(self, parent):
+ elements = [u'%s']
+ return [u' '.join(elements)]
+
+
+class Conditionals(object):
+ """
+
+ """
+ COMBINE_AND = 0
+ COMBINE_OR = 1
+ def __init__(self, args, combine_type=COMBINE_AND):
+ """
+
+ :param args:
+ :type args: resources.lib.kodisettings.struct.Conditional or list
+ :param combine_type:
+ :type combine_type: int
+ :return:
+ :rtype:
+ """
+ if isinstance(args, list):
+ for item in args:
+ assert isinstance(item, Conditional)
+ self.conditionals = args
+ else:
+ assert isinstance(args, Conditional)
+ self.conditionals = [args]
+ assert (combine_type==self.COMBINE_AND or combine_type==self.COMBINE_OR)
+ self.combine_type = combine_type
+
+ def addConditional(self, conditional):
+ assert isinstance(conditional, Conditional)
+ self.conditionals.append(conditional)
+
+ def render(self, control, parent):
+ """
+
+ :param control:
+ :type control: resources.lib.kodisettings.struct.Number or resources.lib.kodisettings.struct.Text or resources.lib.kodisettings.struct.FileBrowser or resources.lib.kodisettings.struct.Bool or resources.lib.kodisettings.struct.Lsep or resources.lib.kodisettings.struct.LabelEnum or resources.lib.kodisettings.struct.Select or resources.lib.kodisettings.struct.Action
+ :param parent:
+ :type parent: resources.lib.kodisettings.struct.Settings
+ :return:
+ :rtype:
+ """
+ cond = []
+ for conditional in self.conditionals:
+ cond += conditional.render(control, parent)
+ if self.combine_type == self.COMBINE_AND:
+ cond = u' + '.join(cond)
+ else:
+ cond = u' | '.join(cond)
+ return cond
+
+
+class Conditional(object):
+ """
+
+ """
+ OP_EQUAL = 1
+ OP_NOT_EQUAL = 2
+ OP_GREATER_THAN = 3
+ OP_LESSER_THAN = 5
+ OP_HAS_ADDON = 7
+ OP_BOOLEAN = 8
+ OP_DICT = {OP_EQUAL:u'eq', OP_NOT_EQUAL:u'!eq', OP_GREATER_THAN:u'gt', OP_LESSER_THAN:u'lt'}
+ def __init__(self, operator, value, reference=None):
+ """
+
+ :param operator:
+ :type operator: int
+ :param value:
+ :type value: unicode or str or bool
+ :param reference:
+ :type reference: unicode or str
+ :return:
+ :rtype:
+ """
+ self.operator = operator
+ if operator == Conditional.OP_BOOLEAN:
+ assert isinstance(value, bool)
+ if value is True:
+ self.value = u'true'
+ else:
+ self.value = u'false'
+ elif operator != Conditional.OP_HAS_ADDON:
+ assert isinstance(reference, unicode) or isinstance(reference, str)
+ assert isinstance(value, unicode) or isinstance(value, str)
+ assert reference != u''
+ self.reference = unicode(reference)
+ self.value = unicode(value)
+ else:
+ assert isinstance(value, unicode) or isinstance(value, str)
+ self.addonid = unicode(value)
+
+ def render(self, control, parent):
+ """
+
+ :param control:
+ :type control: resources.lib.kodisettings.struct.Number or resources.lib.kodisettings.struct.Text or resources.lib.kodisettings.struct.FileBrowser or resources.lib.kodisettings.struct.Bool or resources.lib.kodisettings.struct.Lsep or resources.lib.kodisettings.struct.LabelEnum or resources.lib.kodisettings.struct.Select or resources.lib.kodisettings.struct.Action
+ :param parent:
+ :type parent: resources.lib.kodisettings.struct.Settings
+ :return:
+ :rtype:
+ """
+ if self.operator == self.OP_BOOLEAN:
+ return [self.value]
+ elif self.operator == self.OP_HAS_ADDON:
+ return [u'System.HasAddon(%s)' % self.addonid]
+ else:
+ assert isinstance(parent, Settings)
+ if self.reference in parent.duplicateids:
+ raise KeyError('Cannot use control with duplicated id as reference: %s' % self.reference)
+ relindex = parent.control(self.reference)._outputindex - control._outputindex
+ refcontrol = parent.control(self.reference)
+ assert isinstance(refcontrol, Control)
+ if refcontrol.stype == u'enum' or refcontrol.stype == u'labelenum':
+ assert (isinstance(refcontrol, Enum) or isinstance(refcontrol, LabelEnum))
+ index = None
+ for i, item in enumerate(refcontrol.values):
+ if item == self.value:
+ index = i
+ break
+ if index is None:
+ raise KeyError('Conditional Error: match value (%s) not in list of values: %s' % (self.value, refcontrol.values))
+ else:
+ return [u'%s(%s,%s)' % (self.OP_DICT[self.operator], relindex, index)]
+ elif refcontrol.stype == u'labelenum':
+ assert isinstance(refcontrol, LabelEnum)
+ if self.value in refcontrol.values:
+ return [u'%s(%s,%s)' % (self.OP_DICT[self.operator], relindex, self.value)]
+ else:
+ raise KeyError('Conditional Error: match value (%s) not in list of values: %s' % (self.value, refcontrol.values))
+ else:
+ return [u'%s(%s,%s)' % (self.OP_DICT[self.operator], relindex, self.value)]
+
+getControlClass = {'sep': Sep, 'lsep':Lsep, 'text':Text, 'ipaddress':Ipaddress, 'number':Number, 'slider':Slider,
+ 'date':Date, 'time':Time, 'bool':Bool, 'select':Select, 'addon':Addon, 'enum':Enum,
+ 'labelenum':LabelEnum, 'browser':FileBrowser, 'action':Action}
+
+
+
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/AUTHORS b/script.service.kodi.callbacks/resources/lib/pathtools/AUTHORS
new file mode 100644
index 0000000000..e14de1185f
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/AUTHORS
@@ -0,0 +1,2 @@
+Yesudeep Mangalapilly
+Martin Kreichgauer
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/LICENSE.txt b/script.service.kodi.callbacks/resources/lib/pathtools/LICENSE.txt
new file mode 100644
index 0000000000..5e6adc9dad
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright (C) 2010 by Yesudeep Mangalapilly
+
+MIT License
+-----------
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/README b/script.service.kodi.callbacks/resources/lib/pathtools/README
new file mode 100644
index 0000000000..5b5110fef8
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/README
@@ -0,0 +1,6 @@
+pathtools
+=========
+
+Pattern matching and various utilities for file systems paths.
+
+
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/README.rst b/script.service.kodi.callbacks/resources/lib/pathtools/README.rst
new file mode 100644
index 0000000000..100b93820a
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/README.rst
@@ -0,0 +1 @@
+README
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/__init__.py b/script.service.kodi.callbacks/resources/lib/pathtools/__init__.py
new file mode 100644
index 0000000000..c9c373fc07
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# pathtools: File system path tools.
+# Copyright (C) 2010 Yesudeep Mangalapilly
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/path.py b/script.service.kodi.callbacks/resources/lib/pathtools/path.py
new file mode 100644
index 0000000000..20013599aa
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/path.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# path.py: Path functions.
+#
+# Copyright (C) 2010 Yesudeep Mangalapilly
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+:module: pathtools.path
+:synopsis: Directory walking, listing, and path sanitizing functions.
+:author: Yesudeep Mangalapilly
+
+Functions
+---------
+.. autofunction:: get_dir_walker
+.. autofunction:: walk
+.. autofunction:: listdir
+.. autofunction:: list_directories
+.. autofunction:: list_files
+.. autofunction:: absolute_path
+.. autofunction:: real_absolute_path
+.. autofunction:: parent_dir_path
+"""
+
+import os.path
+import os.path
+from functools import partial
+
+
+__all__ = [
+ 'get_dir_walker',
+ 'walk',
+ 'listdir',
+ 'list_directories',
+ 'list_files',
+ 'absolute_path',
+ 'real_absolute_path',
+ 'parent_dir_path',
+]
+
+
+def get_dir_walker(recursive, topdown=True, followlinks=False):
+ """
+ Returns a recursive or a non-recursive directory walker.
+
+ :param recursive:
+ ``True`` produces a recursive walker; ``False`` produces a non-recursive
+ walker.
+ :returns:
+ A walker function.
+ """
+ if recursive:
+ walk = partial(os.walk, topdown=topdown, followlinks=followlinks)
+ else:
+ def walk(path, topdown=topdown, followlinks=followlinks):
+ try:
+ yield next(os.walk(path, topdown=topdown, followlinks=followlinks))
+ except NameError:
+ yield os.walk(path, topdown=topdown, followlinks=followlinks).next() #IGNORE:E1101
+ return walk
+
+
+def walk(dir_pathname, recursive=True, topdown=True, followlinks=False):
+ """
+ Walks a directory tree optionally recursively. Works exactly like
+ :func:`os.walk` only adding the `recursive` argument.
+
+ :param dir_pathname:
+ The directory to traverse.
+ :param recursive:
+ ``True`` for walking recursively through the directory tree;
+ ``False`` otherwise.
+ :param topdown:
+ Please see the documentation for :func:`os.walk`
+ :param followlinks:
+ Please see the documentation for :func:`os.walk`
+ """
+ walk_func = get_dir_walker(recursive, topdown, followlinks)
+ for root, dirnames, filenames in walk_func(dir_pathname):
+ yield (root, dirnames, filenames)
+
+
+def listdir(dir_pathname,
+ recursive=True,
+ topdown=True,
+ followlinks=False):
+ """
+ Enlists all items using their absolute paths in a directory, optionally
+ recursively.
+
+ :param dir_pathname:
+ The directory to traverse.
+ :param recursive:
+ ``True`` for walking recursively through the directory tree;
+ ``False`` otherwise.
+ :param topdown:
+ Please see the documentation for :func:`os.walk`
+ :param followlinks:
+ Please see the documentation for :func:`os.walk`
+ """
+ for root, dirnames, filenames\
+ in walk(dir_pathname, recursive, topdown, followlinks):
+ for dirname in dirnames:
+ yield absolute_path(os.path.join(root, dirname))
+ for filename in filenames:
+ yield absolute_path(os.path.join(root, filename))
+
+
+def list_directories(dir_pathname,
+ recursive=True,
+ topdown=True,
+ followlinks=False):
+ """
+ Enlists all the directories using their absolute paths within the specified
+ directory, optionally recursively.
+
+ :param dir_pathname:
+ The directory to traverse.
+ :param recursive:
+ ``True`` for walking recursively through the directory tree;
+ ``False`` otherwise.
+ :param topdown:
+ Please see the documentation for :func:`os.walk`
+ :param followlinks:
+ Please see the documentation for :func:`os.walk`
+ """
+ for root, dirnames, filenames\
+ in walk(dir_pathname, recursive, topdown, followlinks):
+ for dirname in dirnames:
+ yield absolute_path(os.path.join(root, dirname))
+
+
+def list_files(dir_pathname,
+ recursive=True,
+ topdown=True,
+ followlinks=False):
+ """
+ Enlists all the files using their absolute paths within the specified
+ directory, optionally recursively.
+
+ :param dir_pathname:
+ The directory to traverse.
+ :param recursive:
+ ``True`` for walking recursively through the directory tree;
+ ``False`` otherwise.
+ :param topdown:
+ Please see the documentation for :func:`os.walk`
+ :param followlinks:
+ Please see the documentation for :func:`os.walk`
+ """
+ for root, dirnames, filenames\
+ in walk(dir_pathname, recursive, topdown, followlinks):
+ for filename in filenames:
+ yield absolute_path(os.path.join(root, filename))
+
+
+def absolute_path(path):
+ """
+ Returns the absolute path for the given path and normalizes the path.
+
+ :param path:
+ Path for which the absolute normalized path will be found.
+ :returns:
+ Absolute normalized path.
+ """
+ return os.path.abspath(os.path.normpath(path))
+
+
+def real_absolute_path(path):
+ """
+ Returns the real absolute normalized path for the given path.
+
+ :param path:
+ Path for which the real absolute normalized path will be found.
+ :returns:
+ Real absolute normalized path.
+ """
+ return os.path.realpath(absolute_path(path))
+
+
+def parent_dir_path(path):
+ """
+ Returns the parent directory path.
+
+ :param path:
+ Path for which the parent directory will be obtained.
+ :returns:
+ Parent directory path.
+ """
+ return absolute_path(os.path.dirname(path))
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/patterns.py b/script.service.kodi.callbacks/resources/lib/pathtools/patterns.py
new file mode 100644
index 0000000000..4ecd853745
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/patterns.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# patterns.py: Common wildcard searching/filtering functionality for files.
+#
+# Copyright (C) 2010 Yesudeep Mangalapilly
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+:module: pathtools.patterns
+:synopsis: Wildcard pattern matching and filtering functions for paths.
+:author: Yesudeep Mangalapilly
+
+Functions
+---------
+.. autofunction:: match_path
+.. autofunction:: match_path_against
+.. autofunction:: filter_paths
+"""
+
+from fnmatch import fnmatch, fnmatchcase
+
+__all__ = ['match_path',
+ 'match_path_against',
+ 'match_any_paths',
+ 'filter_paths']
+
+
+def _string_lower(s):
+ """
+ Convenience function to lowercase a string (the :mod:`string` module is
+ deprecated/removed in Python 3.0).
+
+ :param s:
+ The string which will be lowercased.
+ :returns:
+ Lowercased copy of string s.
+ """
+ return s.lower()
+
+
+def match_path_against(pathname, patterns, case_sensitive=True):
+ """
+ Determines whether the pathname matches any of the given wildcard patterns,
+ optionally ignoring the case of the pathname and patterns.
+
+ :param pathname:
+ A path name that will be matched against a wildcard pattern.
+ :param patterns:
+ A list of wildcard patterns to match_path the filename against.
+ :param case_sensitive:
+ ``True`` if the matching should be case-sensitive; ``False`` otherwise.
+ :returns:
+ ``True`` if the pattern matches; ``False`` otherwise.
+
+ Doctests::
+ >>> match_path_against("/home/username/foobar/blah.py", ["*.py", "*.txt"], False)
+ True
+ >>> match_path_against("/home/username/foobar/blah.py", ["*.PY", "*.txt"], True)
+ False
+ >>> match_path_against("/home/username/foobar/blah.py", ["*.PY", "*.txt"], False)
+ True
+ >>> match_path_against("C:\\windows\\blah\\BLAH.PY", ["*.py", "*.txt"], True)
+ False
+ >>> match_path_against("C:\\windows\\blah\\BLAH.PY", ["*.py", "*.txt"], False)
+ True
+ """
+ if case_sensitive:
+ match_func = fnmatchcase
+ pattern_transform_func = (lambda w: w)
+ else:
+ match_func = fnmatch
+ pathname = pathname.lower()
+ pattern_transform_func = _string_lower
+ for pattern in set(patterns):
+ pattern = pattern_transform_func(pattern)
+ if match_func(pathname, pattern):
+ return True
+ return False
+
+
+def _match_path(pathname,
+ included_patterns,
+ excluded_patterns,
+ case_sensitive=True):
+ """Internal function same as :func:`match_path` but does not check arguments.
+
+ Doctests::
+ >>> _match_path("/users/gorakhargosh/foobar.py", ["*.py"], ["*.PY"], True)
+ True
+ >>> _match_path("/users/gorakhargosh/FOOBAR.PY", ["*.py"], ["*.PY"], True)
+ False
+ >>> _match_path("/users/gorakhargosh/foobar/", ["*.py"], ["*.txt"], False)
+ False
+ >>> _match_path("/users/gorakhargosh/FOOBAR.PY", ["*.py"], ["*.PY"], False)
+ Traceback (most recent call last):
+ ...
+ ValueError: conflicting patterns `set(['*.py'])` included and excluded
+ """
+ if not case_sensitive:
+ included_patterns = set(map(_string_lower, included_patterns))
+ excluded_patterns = set(map(_string_lower, excluded_patterns))
+ else:
+ included_patterns = set(included_patterns)
+ excluded_patterns = set(excluded_patterns)
+ common_patterns = included_patterns & excluded_patterns
+ if common_patterns:
+ raise ValueError('conflicting patterns `%s` included and excluded'\
+ % common_patterns)
+ return (match_path_against(pathname, included_patterns, case_sensitive)\
+ and not match_path_against(pathname, excluded_patterns,
+ case_sensitive))
+
+
+def match_path(pathname,
+ included_patterns=None,
+ excluded_patterns=None,
+ case_sensitive=True):
+ """
+ Matches a pathname against a set of acceptable and ignored patterns.
+
+ :param pathname:
+ A pathname which will be matched against a pattern.
+ :param included_patterns:
+ Allow filenames matching wildcard patterns specified in this list.
+ If no pattern is specified, the function treats the pathname as
+ a match_path.
+ :param excluded_patterns:
+ Ignores filenames matching wildcard patterns specified in this list.
+ If no pattern is specified, the function treats the pathname as
+ a match_path.
+ :param case_sensitive:
+ ``True`` if matching should be case-sensitive; ``False`` otherwise.
+ :returns:
+ ``True`` if the pathname matches; ``False`` otherwise.
+ :raises:
+ ValueError if included patterns and excluded patterns contain the
+ same pattern.
+
+ Doctests::
+ >>> match_path("/Users/gorakhargosh/foobar.py")
+ True
+ >>> match_path("/Users/gorakhargosh/foobar.py", case_sensitive=False)
+ True
+ >>> match_path("/users/gorakhargosh/foobar.py", ["*.py"], ["*.PY"], True)
+ True
+ >>> match_path("/users/gorakhargosh/FOOBAR.PY", ["*.py"], ["*.PY"], True)
+ False
+ >>> match_path("/users/gorakhargosh/foobar/", ["*.py"], ["*.txt"], False)
+ False
+ >>> match_path("/users/gorakhargosh/FOOBAR.PY", ["*.py"], ["*.PY"], False)
+ Traceback (most recent call last):
+ ...
+ ValueError: conflicting patterns `set(['*.py'])` included and excluded
+ """
+ included = ["*"] if included_patterns is None else included_patterns
+ excluded = [] if excluded_patterns is None else excluded_patterns
+ return _match_path(pathname, included, excluded, case_sensitive)
+
+
+def filter_paths(pathnames,
+ included_patterns=None,
+ excluded_patterns=None,
+ case_sensitive=True):
+ """
+ Filters from a set of paths based on acceptable patterns and
+ ignorable patterns.
+
+ :param pathnames:
+ A list of path names that will be filtered based on matching and
+ ignored patterns.
+ :param included_patterns:
+ Allow filenames matching wildcard patterns specified in this list.
+ If no pattern list is specified, ["*"] is used as the default pattern,
+ which matches all files.
+ :param excluded_patterns:
+ Ignores filenames matching wildcard patterns specified in this list.
+ If no pattern list is specified, no files are ignored.
+ :param case_sensitive:
+ ``True`` if matching should be case-sensitive; ``False`` otherwise.
+ :returns:
+ A list of pathnames that matched the allowable patterns and passed
+ through the ignored patterns.
+
+ Doctests::
+ >>> pathnames = set(["/users/gorakhargosh/foobar.py", "/var/cache/pdnsd.status", "/etc/pdnsd.conf", "/usr/local/bin/python"])
+ >>> set(filter_paths(pathnames)) == pathnames
+ True
+ >>> set(filter_paths(pathnames, case_sensitive=False)) == pathnames
+ True
+ >>> set(filter_paths(pathnames, ["*.py", "*.conf"], ["*.status"], case_sensitive=True)) == set(["/users/gorakhargosh/foobar.py", "/etc/pdnsd.conf"])
+ True
+ """
+ included = ["*"] if included_patterns is None else included_patterns
+ excluded = [] if excluded_patterns is None else excluded_patterns
+
+ for pathname in pathnames:
+ # We don't call the public match_path because it checks arguments
+ # and sets default values if none are found. We're already doing that
+ # above.
+ if _match_path(pathname, included, excluded, case_sensitive):
+ yield pathname
+
+def match_any_paths(pathnames,
+ included_patterns=None,
+ excluded_patterns=None,
+ case_sensitive=True):
+ """
+ Matches from a set of paths based on acceptable patterns and
+ ignorable patterns.
+
+ :param pathnames:
+ A list of path names that will be filtered based on matching and
+ ignored patterns.
+ :param included_patterns:
+ Allow filenames matching wildcard patterns specified in this list.
+ If no pattern list is specified, ["*"] is used as the default pattern,
+ which matches all files.
+ :param excluded_patterns:
+ Ignores filenames matching wildcard patterns specified in this list.
+ If no pattern list is specified, no files are ignored.
+ :param case_sensitive:
+ ``True`` if matching should be case-sensitive; ``False`` otherwise.
+ :returns:
+ ``True`` if any of the paths matches; ``False`` otherwise.
+
+ Doctests::
+ >>> pathnames = set(["/users/gorakhargosh/foobar.py", "/var/cache/pdnsd.status", "/etc/pdnsd.conf", "/usr/local/bin/python"])
+ >>> match_any_paths(pathnames)
+ True
+ >>> match_any_paths(pathnames, case_sensitive=False)
+ True
+ >>> match_any_paths(pathnames, ["*.py", "*.conf"], ["*.status"], case_sensitive=True)
+ True
+ >>> match_any_paths(pathnames, ["*.txt"], case_sensitive=False)
+ False
+ >>> match_any_paths(pathnames, ["*.txt"], case_sensitive=True)
+ False
+ """
+ included = ["*"] if included_patterns is None else included_patterns
+ excluded = [] if excluded_patterns is None else excluded_patterns
+
+ for pathname in pathnames:
+ # We don't call the public match_path because it checks arguments
+ # and sets default values if none are found. We're already doing that
+ # above.
+ if _match_path(pathname, included, excluded, case_sensitive):
+ return True
+ return False
diff --git a/script.service.kodi.callbacks/resources/lib/pathtools/version.py b/script.service.kodi.callbacks/resources/lib/pathtools/version.py
new file mode 100644
index 0000000000..2a94d01277
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pathtools/version.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# version.py: Version information.
+# Copyright (C) 2010 Yesudeep Mangalapilly
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# When updating this version number, please update the
+# ``docs/source/global.rst.inc`` file as well.
+VERSION_MAJOR = 0
+VERSION_MINOR = 1
+VERSION_BUILD = 1
+VERSION_INFO = (VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD)
+VERSION_STRING = "%d.%d.%d" % VERSION_INFO
+
+__version__ = VERSION_INFO
diff --git a/script.service.kodi.callbacks/resources/lib/publisherfactory.py b/script.service.kodi.callbacks/resources/lib/publisherfactory.py
new file mode 100644
index 0000000000..5a977b6875
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publisherfactory.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+from resources.lib.publishers.log import LogPublisher
+from resources.lib.publishers.loop import LoopPublisher
+from resources.lib.publishers.monitor import MonitorPublisher
+from resources.lib.publishers.player import PlayerPublisher
+from resources.lib.publishers.schedule import SchedulePublisher
+from resources.lib.kodilogging import KodiLogger
+kl = KodiLogger()
+log = kl.log
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+try:
+ from resources.lib.publishers.watchdog import WatchdogPublisher
+except ImportError as e:
+ from resources.lib.publishers.dummy import WatchdogPublisherDummy as WatchdogPublisher
+ log(msg=_('Error importing Watchdog: %s') % str(e), loglevel=KodiLogger.LOGERROR)
+try:
+ from resources.lib.publishers.watchdogStartup import WatchdogStartup
+except ImportError as e:
+ from resources.lib.publishers.dummy import WatchdogPublisherDummy as WatchdogStartup
+ log(msg=_('Error importing Watchdog: %s') % str(e), loglevel=KodiLogger.LOGERROR)
+
+
+
+class PublisherFactory(object):
+
+ def __init__(self, settings, topics, dispatcher, logger, debug=False):
+ self.settings = settings
+ self.logger = logger
+ self.topics = topics
+ self.debug = debug
+ self.dispatcher = dispatcher
+ self.publishers = {LogPublisher:_('Log Publisher initialized'),
+ LoopPublisher:_('Loop Publisher initialized'),
+ MonitorPublisher:_('Monitor Publisher initialized'),
+ PlayerPublisher:_('Player Publisher initialized'),
+ WatchdogPublisher:_('Watchdog Publisher initialized'),
+ WatchdogStartup:_('Watchdog Startup Publisher initialized'),
+ SchedulePublisher:_('Schedule Publisher initialized')
+ }
+ self.ipublishers = []
+
+ def createPublishers(self):
+ for publisher in self.publishers.keys():
+ if not set(self.topics).isdisjoint(publisher.publishes) or self.debug is True:
+ ipublisher = publisher(self.dispatcher, self.settings)
+ self.ipublishers.append(ipublisher)
+ self.logger.log(msg=_(self.publishers[publisher]))
+
+
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/__init__.py b/script.service.kodi.callbacks/resources/lib/publishers/__init__.py
new file mode 100644
index 0000000000..25e9a531ca
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/dummy.py b/script.service.kodi.callbacks/resources/lib/publishers/dummy.py
new file mode 100644
index 0000000000..a15a64364e
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/dummy.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+from resources.lib.events import Events
+from resources.lib.pubsub import Publisher
+
+
+class WatchdogPublisherDummy(Publisher):
+ publishes = Events().Watchdog.keys()
+
+ def __init__(self, dispatcher, _):
+ super(WatchdogPublisherDummy, self).__init__(dispatcher)
+
+ def start(self):
+ pass
+
+ def abort(self, timeout=None):
+ pass
+
+ def join(self, timeout=None):
+ pass
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/log.py b/script.service.kodi.callbacks/resources/lib/publishers/log.py
new file mode 100644
index 0000000000..966bd7e9f9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/log.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+import xbmc
+import threading
+from Queue import Queue, Empty
+import re
+from resources.lib.pubsub import Publisher, Topic, Message
+from resources.lib.events import Events
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+logfn = xbmc.translatePath(r'special://logpath/kodi.log')
+
+class LogMonitor(threading.Thread):
+ def __init__(self, interval=100):
+ super(LogMonitor, self).__init__(name='LogMonitor')
+ self.logfn = logfn
+ self.__abort_evt = threading.Event()
+ self.__abort_evt.clear()
+ self.ouputq = Queue()
+ self.interval = interval
+
+ def run(self):
+ f = open(self.logfn, 'r')
+ f.seek(0, 2) # Seek @ EOF
+ fsize_old = f.tell()
+ while not self.__abort_evt.is_set():
+ f.seek(0, 2) # Seek @ EOF
+ fsize = f.tell() # Get Size
+ if fsize > fsize_old:
+ f.seek(fsize_old, 0)
+ lines = f.readlines() # Read to end
+ for line in lines:
+ self.ouputq.put(line, False)
+ fsize_old = f.tell()
+ xbmc.sleep(self.interval)
+
+ def abort(self, timeout=0):
+ self.__abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop LogMonitor T:%i') % self.ident)
+
+class LogPublisher(threading.Thread, Publisher):
+
+ publishes = Events.Log.keys()
+
+ def __init__(self, dispatcher, settings):
+ Publisher.__init__(self, dispatcher)
+ threading.Thread.__init__(self, name='LogPublisher')
+ self._checks_simple = []
+ self._checks_regex = []
+ self.abort_evt = threading.Event()
+ self.abort_evt.clear()
+ self.interval_checker = settings.general['LogFreq']
+ self.interval_monitor = settings.general['LogFreq']
+ self.add_simple_checks(settings.getLogSimples())
+ self.add_regex_checks(settings.getLogRegexes())
+
+ def add_simple_checks(self, simpleList):
+ for chk in simpleList:
+ self.add_simple_check(chk['matchIf'], chk['rejectIf'], chk['eventId'])
+
+ def add_regex_checks(self, regexList):
+ for chk in regexList:
+ self.add_re_check(chk['matchIf'], chk['rejectIf'], chk['eventId'])
+
+ def add_simple_check(self, match, nomatch, subtopic):
+ self._checks_simple.append(LogCheckSimple(match, nomatch, subtopic, self.publish))
+
+ def add_re_check(self, match, nomatch, subtopic):
+ self._checks_regex.append(LogCheckRegex(match, nomatch, subtopic, self.publish))
+
+ def run(self):
+ lm = LogMonitor(interval=self.interval_monitor)
+ lm.start()
+ for chk in self._checks_simple:
+ chk.start()
+ for chk in self._checks_regex:
+ chk.start()
+ while not self.abort_evt.is_set():
+ while not lm.ouputq.empty():
+ try:
+ line = lm.ouputq.get_nowait()
+ except Empty:
+ continue
+ for chk in self._checks_simple:
+ chk.queue.put(line, False)
+ for chk in self._checks_regex:
+ chk.queue.put(line, False)
+ if self.abort_evt.is_set():
+ continue
+ xbmc.sleep(self.interval_checker)
+ for chk in self._checks_simple:
+ chk.abort()
+ for chk in self._checks_regex:
+ chk.abort()
+ lm.abort()
+
+ def abort(self, timeout=0):
+ self.abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop LogPublisher T:%i') % self.ident)
+
+class LogCheck(object):
+ def __init__(self, match, nomatch, callback, param):
+ self.match = match
+ self.nomatch = nomatch
+ self.callback = callback
+ self.param = param
+
+class LogCheckSimple(threading.Thread):
+ def __init__(self, match, nomatch, subtopic, publish):
+ super(LogCheckSimple, self).__init__(name='LogCheckSimple')
+ self.match = match
+ self.nomatch = nomatch
+ self.publish = publish
+ self.queue = Queue()
+ self._abort_evt = threading.Event()
+ self._abort_evt.clear()
+ self.topic = Topic('onLogSimple', subtopic)
+
+ def run(self):
+ while not self._abort_evt.is_set():
+ while not self.queue.empty():
+ try:
+ line = self.queue.get_nowait()
+ except Empty:
+ continue
+ if self.match in line:
+ if self.nomatch != '':
+ if (self.nomatch in line) is not True:
+ msg = Message(self.topic, line=line)
+ self.publish(msg)
+ else:
+ msg = Message(self.topic, line=line)
+ self.publish(msg)
+
+ def abort(self, timeout=0):
+ self._abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop LogCheckSimple T:%i') % self.ident)
+
+class LogCheckRegex(threading.Thread):
+ def __init__(self, match, nomatch, subtopic, publish):
+ super(LogCheckRegex, self).__init__(name='LogCheckRegex')
+ try:
+ re_match = re.compile(match, flags=re.IGNORECASE)
+ except re.error:
+ raise
+ if nomatch != '':
+ try:
+ re_nomatch = re.compile(nomatch, flags=re.IGNORECASE)
+ except re.error:
+ raise
+ else:
+ re_nomatch = None
+ self.match = re_match
+ self.nomatch = re_nomatch
+ self.publish = publish
+ self.queue = Queue()
+ self._abort_evt = threading.Event()
+ self._abort_evt.clear()
+ self.topic = Topic('onLogRegex', subtopic)
+
+ def run(self):
+ while not self._abort_evt.is_set():
+ while not self.queue.empty():
+ try:
+ line = self.queue.get_nowait()
+ except Empty:
+ continue
+ if self.match.search(line):
+ if self.nomatch is not None:
+ if (self.nomatch.search(line)) is None:
+ msg = Message(self.topic, line=line)
+ self.publish(msg)
+ else:
+ msg = Message(self.topic, line=line)
+ self.publish(msg)
+
+ def abort(self, timeout=0):
+ self._abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop LogCheckRegex T:%i') % self.ident)
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/loop.py b/script.service.kodi.callbacks/resources/lib/publishers/loop.py
new file mode 100644
index 0000000000..17dcde937a
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/loop.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+
+import threading
+import time
+from json import loads as jloads
+
+import xbmc
+import xbmcgui
+from resources.lib.events import Events
+from resources.lib.pubsub import Publisher, Message, Topic
+from resources.lib.utils.poutil import KodiPo
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+
+def getStereoscopicMode():
+ """
+ Retrieves stereoscopic mode from json-rpc
+ @return: "off", "split_vertical", "split_horizontal", "row_interleaved", "hardware_based", "anaglyph_cyan_red",
+ "anaglyph_green_magenta", "monoscopic"
+ @rtype: str
+ """
+ query = '{"jsonrpc": "2.0", "method": "GUI.GetProperties", "params": {"properties": ["stereoscopicmode"]}, "id": 1}'
+ result = xbmc.executeJSONRPC(query)
+ jsonr = jloads(result)
+ ret = ''
+ if 'result' in jsonr:
+ if 'stereoscopicmode' in jsonr['result']:
+ if 'mode' in jsonr['result']['stereoscopicmode']:
+ ret = jsonr['result']['stereoscopicmode']['mode'].encode('utf-8')
+ return ret
+
+
+def getProfileString():
+ """
+ Retrieves the current profile as a path
+ @rtype: str
+ """
+ ps = xbmc.translatePath('special://profile')
+ return ps
+
+
+class LoopPublisher(threading.Thread, Publisher):
+ publishes = Events().CustomLoop.keys()
+
+ def __init__(self, dispatcher, settings):
+ Publisher.__init__(self, dispatcher)
+ threading.Thread.__init__(self, name='LoopPublisher')
+ self.interval = settings.general['LoopFreq']
+ self.abort_evt = threading.Event()
+ self.abort_evt.clear()
+ self.openwindowids = settings.getOpenwindowids()
+ self.closewindowsids = settings.getClosewindowids()
+ idleT = settings.getIdleTimes()
+ afterIdle = settings.getAfterIdleTimes()
+ self.player = xbmc.Player()
+ if idleT is not None:
+ if len(idleT) > 0:
+ self.idleTs = []
+ self._startidle = 0
+ self._playeridle = False
+ for key in idleT.keys():
+ # time, key, executed
+ self.idleTs.append([idleT[key], key, False])
+ else:
+ self.idleTs = []
+ else:
+ self.idleTs = []
+ if afterIdle is not None:
+ if len(afterIdle) > 0:
+ self.afterIdles = []
+ self._startidle = 0
+ self._playeridle = False
+ for key in afterIdle.keys():
+ # time, key, triggered
+ self.afterIdles.append([afterIdle[key], key, False])
+ else:
+ self.afterIdles = []
+ else:
+ self.afterIdles = []
+ if len(self.idleTs) > 0 or len(self.afterIdles) > 0:
+ self.doidle = True
+ else:
+ self.doidle = False
+
+ def run(self):
+ lastwindowid = xbmcgui.getCurrentWindowId()
+ lastprofile = getProfileString()
+ laststereomode = getStereoscopicMode()
+ interval = self.interval
+ firstloop = True
+ starttime = time.time()
+ while not self.abort_evt.is_set():
+
+ self._checkIdle()
+
+ newprofile = getProfileString()
+ if newprofile != lastprofile:
+ self.publish(Message(Topic('onProfileChange'), profilePath=newprofile))
+ lastprofile = newprofile
+
+ newstereomode = getStereoscopicMode()
+ if newstereomode != laststereomode:
+ self.publish(Message(Topic('onStereoModeChange'), stereoMode=newstereomode))
+ laststereomode = newstereomode
+
+ newwindowid = xbmcgui.getCurrentWindowId()
+ if newwindowid != lastwindowid:
+ if lastwindowid in self.closewindowsids.keys():
+ self.publish(Message(Topic('onWindowClose', self.closewindowsids[lastwindowid])))
+ if newwindowid in self.openwindowids:
+ self.publish(Message(Topic('onWindowOpen', self.openwindowids[newwindowid])))
+ lastwindowid = newwindowid
+
+ if firstloop:
+ endtime = time.time()
+ interval = int(interval - (endtime - starttime) * 1000)
+ interval = max(5, interval)
+ firstloop = False
+ xbmc.sleep(interval)
+ del self.player
+
+ def _checkIdle(self):
+ if self.doidle is False:
+ return
+ XBMCit = xbmc.getGlobalIdleTime()
+ if self.player.isPlaying():
+ self._playeridle = False
+ self._startidle = XBMCit
+ else: # player is not playing
+ if self._playeridle is False: # if the first pass with player idle, start timing here
+ self._playeridle = True
+ self._startidle = XBMCit
+ myit = XBMCit - self._startidle # amount of time idle and not playing
+ for it in self.idleTs:
+ if myit > it[0]: # if time exceeded idle timer
+ if it[2] is False: # idle task has NOT been executed
+ msg = Message(Topic('onIdle', it[1]))
+ self.publish(msg)
+ it[2] = True
+ else: # if time has not exceeded timer
+ it[2] = False # reset task executed flag
+ for it in self.afterIdles:
+ if myit > it[0]: # time has exceeded timer
+ it[2] = True # set flag that task needs to be executed when exiting idle
+ else: # time has not exceeded idle timer
+ if it[2] is True: # if flag to execute has been set
+ msg = Message(Topic('afterIdle', it[1]))
+ self.publish(msg)
+ it[2] = False # reset flag to execute
+
+ def abort(self, timeout=0):
+ self.abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop LoopPublisher T:%i') % self.ident)
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/monitor.py b/script.service.kodi.callbacks/resources/lib/publishers/monitor.py
new file mode 100644
index 0000000000..29e99e94b8
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/monitor.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+import threading
+import xbmc
+from resources.lib.pubsub import Publisher, Topic, Message
+from resources.lib.events import Events
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+class MonitorPublisher(threading.Thread, Publisher):
+ publishes = Events().Monitor.keys()
+
+ def __init__(self, dispatcher, settings):
+ Publisher.__init__(self, dispatcher)
+ threading.Thread.__init__(self, name='MonitorPublisher')
+ self.dispatcher = dispatcher
+ self._abortevt = threading.Event()
+ self._abortevt.clear()
+ self.jsoncriteria = settings.getJsonNotifications()
+
+ def run(self):
+ publish = super(MonitorPublisher, self).publish
+ monitor = _Monitor()
+ monitor.jsoncriteria = self.jsoncriteria
+ monitor.publish = publish
+ while not self._abortevt.is_set():
+ xbmc.sleep(500)
+ del monitor
+
+ def abort(self, timeout=0):
+ self._abortevt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop MonitorPublisher T:%i') % self.ident)
+
+class _Monitor(xbmc.Monitor):
+ def __init__(self):
+ super(_Monitor, self).__init__()
+ self.publish = None
+ self.jsoncriteria = None
+
+ def onCleanFinished(self, library):
+ topic = Topic('onCleanFinished')
+ kwargs = {'library':library}
+ self.publish(Message(topic, **kwargs))
+
+ def onCleanStarted(self, library):
+ topic = Topic('onCleanStarted')
+ kwargs = {'library':library}
+ self.publish(Message(topic, **kwargs))
+
+ def onDPMSActivated(self):
+ topic = Topic('onDPMSActivated')
+ kwargs = {}
+ self.publish(Message(topic, **kwargs))
+
+ def onDPMSDeactivated(self):
+ topic = Topic('onDPMSDeactivated')
+ kwargs = {}
+ self.publish(Message(topic, **kwargs))
+
+ def onNotification(self, sender, method, data):
+ for criterion in self.jsoncriteria:
+ if criterion['sender'] == sender and criterion['method'] == method and criterion['data'] == data:
+ topic = Topic('onNotification', criterion['eventId'])
+ kwargs = {'sender':sender, 'method':method, 'data':data}
+ self.publish(Message(topic, **kwargs))
+
+ def onScanStarted(self, library):
+ topic = Topic('onScanStarted')
+ kwargs = {'library':library}
+ self.publish(Message(topic, **kwargs))
+
+ def onScanFinished(self, library):
+ topic = Topic('onScanFinished')
+ kwargs = {'library':library}
+ self.publish(Message(topic, **kwargs))
+
+ def onScreensaverActivated(self):
+ topic = Topic('onScreensaverActivated')
+ kwargs = {}
+ self.publish(Message(topic, **kwargs))
+
+ def onScreensaverDeactivated(self):
+ topic = Topic('onScreensaverDeactivated')
+ kwargs = {}
+ self.publish(Message(topic, **kwargs))
+
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/player.py b/script.service.kodi.callbacks/resources/lib/publishers/player.py
new file mode 100644
index 0000000000..7c5791183c
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/player.py
@@ -0,0 +1,364 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+import json
+import threading
+
+import xbmc
+from resources.lib.events import Events
+from resources.lib.pubsub import Publisher, Topic, Message
+from resources.lib.utils.poutil import KodiPo
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+
+class PlayerPublisher(threading.Thread, Publisher):
+ publishes = Events.Player.keys()
+
+ def __init__(self, dispatcher, settings):
+ assert settings is not None
+ Publisher.__init__(self, dispatcher)
+ threading.Thread.__init__(self, name='PlayerPublisher')
+ self.dispatcher = dispatcher
+ self.publishes = Events.Player.keys()
+ self._abortevt = threading.Event()
+ self._abortevt.clear()
+
+ def run(self):
+ publish = super(PlayerPublisher, self).publish
+ player = Player()
+ player.publish = publish
+ while not self._abortevt.is_set():
+ if player.isPlaying():
+ player.playingTime = player.getTime()
+ xbmc.sleep(500)
+ del player
+
+ def abort(self, timeout=0):
+ self._abortevt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop PlayerPublisher T:%i') % self.ident)
+
+
+class Player(xbmc.Player):
+ def __init__(self):
+ super(Player, self).__init__()
+ self.publish = None
+ self.totalTime = -1
+ self.playingTime = 0
+ self.info = {}
+
+ def playing_type(self):
+ """
+ @return: [music|movie|episode|stream|liveTV|recordedTV|PVRradio|unknown]
+ """
+ substrings = ['-trailer', 'http://']
+ isMovie = False
+ if self.isPlayingAudio():
+ return "music"
+ else:
+ if xbmc.getCondVisibility('VideoPlayer.Content(movies)'):
+ isMovie = True
+ try:
+ filename = self.getPlayingFile()
+ except RuntimeError:
+ filename = ''
+ if filename != '':
+ if filename[0:3] == 'pvr':
+ if xbmc.getCondVisibility('Pvr.IsPlayingTv'):
+ return 'liveTV'
+ elif xbmc.getCondVisibility('Pvr.IsPlayingRecording'):
+ return 'recordedTV'
+ elif xbmc.getCondVisibility('Pvr.IsPlayingRadio'):
+ return 'PVRradio'
+ else:
+ for string in substrings:
+ if string in filename:
+ isMovie = False
+ break
+ if isMovie:
+ return "movie"
+ elif xbmc.getCondVisibility('VideoPlayer.Content(episodes)'):
+ # Check for tv show title and season to make sure it's really an episode
+ if xbmc.getInfoLabel('VideoPlayer.Season') != "" and xbmc.getInfoLabel('VideoPlayer.TVShowTitle') != "":
+ return "episode"
+ elif xbmc.getCondVisibility('Player.IsInternetStream'):
+ return 'stream'
+ else:
+ return 'unknown'
+
+ def getTitle(self):
+ if self.isPlayingAudio():
+ tries = 0
+ while xbmc.getInfoLabel('MusicPlayer.Title') is None and tries < 8:
+ xbmc.sleep(250)
+ tries += 1
+ title = xbmc.getInfoLabel('MusicPlayer.Title')
+ if title is None or title == '':
+ return 'Kodi cannot detect title'
+ else:
+ return title
+ elif self.isPlayingVideo():
+ tries = 0
+ while xbmc.getInfoLabel('VideoPlayer.Title') is None and tries < 8:
+ xbmc.sleep(250)
+ tries += 1
+ if xbmc.getCondVisibility('VideoPlayer.Content(episodes)'):
+ if xbmc.getInfoLabel('VideoPlayer.Season') != "" and xbmc.getInfoLabel('VideoPlayer.TVShowTitle') != "":
+ return '%s-S%sE%s-%s' % (xbmc.getInfoLabel('VideoPlayer.TVShowTitle'),
+ xbmc.getInfoLabel('VideoPlayer.Season'),
+ xbmc.getInfoLabel('VideoPlayer.Episode'),
+ xbmc.getInfoLabel('VideoPlayer.Title'))
+ else:
+ title = xbmc.getInfoLabel('VideoPlayer.Title')
+ if title is None or title == '':
+ try:
+ ret = json.loads(xbmc.executeJSONRPC(
+ '{"jsonrpc": "2.0", "method": "Player.GetItem", "params": { "properties": ["title"], "playerid": 1 }, "id": "1"}'))
+ except RuntimeError:
+ title = ''
+ else:
+ try:
+ title = ret['result']['item']['title']
+ except KeyError:
+ title = 'Kodi cannot detect title'
+ else:
+ if title == '':
+ title = ret['result']['item']['label']
+ if title == '':
+ title = 'Kodi cannot detect title'
+ return title
+ else:
+ return title
+ else:
+ return 'Kodi cannot detect title - none playing'
+
+ def getAudioInfo(self, playerid):
+ try:
+ info = json.loads(xbmc.executeJSONRPC(
+ '{"jsonrpc": "2.0", "method": "Player.GetItem", "params": { "properties": ["title", "album",'
+ ' "artist", "duration", "file", "streamdetails"], "playerid": %s }, "id": "AudioGetItem"}'
+ % playerid))['result']['item']
+ if 'artist' in info.keys():
+ t = info['artist']
+ if isinstance(t, list):
+ info['artist'] = t[0]
+ elif isinstance(t, unicode):
+ if t != u'':
+ info['artist'] = t
+ else:
+ info['artist'] = u'unknown'
+ else:
+ info['artist'] = u'unknown'
+ else:
+ info['artist'] = u'unknown'
+ items = ['duration', 'id', 'label', 'type']
+ for item in items:
+ try:
+ del info[item]
+ except KeyError:
+ pass
+ info['mediaType'] = 'audio'
+ except RuntimeError:
+ self.info = {}
+ else:
+ self.info = info
+
+ def getVideoInfo(self, playerid):
+ try:
+ info = json.loads(xbmc.executeJSONRPC(
+ '{"jsonrpc": "2.0", "method": "Player.GetItem", "params": { "properties": ["title", "album",'
+ ' "artist", "season", "episode", "duration", "showtitle", "tvshowid", "file", "streamdetails"],'
+ ' "playerid": %s }, "id": "VideoGetItem"}' % playerid))['result']['item']
+ except RuntimeError:
+ self.info = {}
+ else:
+ items = ['label', 'id', 'tvshowid']
+ for item in items:
+ try:
+ del info[item]
+ except KeyError:
+ pass
+ items = {'mediaType': 'type', 'fileName': 'file'}
+ for item in items.keys():
+ try:
+ t = items[item]
+ info[item] = info.pop(t, 'unknown')
+ except KeyError:
+ info[item] = 'unknown'
+ if info['mediaType'] != 'musicvideo':
+ items = ['artist', 'album']
+ for item in items:
+ try:
+ del info[item]
+ except KeyError:
+ pass
+ else:
+ info['artist'] = info['artist'][0]
+ if 'streamdetails' in info.keys():
+ sd = info.pop('streamdetails', {})
+ info['stereomode'] = sd['video'][0]['stereomode']
+ info['width'] = str(sd['video'][0]['width'])
+ info['height'] = str(sd['video'][0]['height'])
+ info['aspectRatio'] = str(int((sd['video'][0]['aspect'] * 100.0) + 0.5) / 100.0)
+ if info['mediaType'] == u'episode':
+ items = ['episode', 'season']
+ for item in items:
+ try:
+ info[item] = str(info[item]).zfill(2)
+ except KeyError:
+ info[item] = 'unknown'
+ else:
+ items = ['episode', 'season', 'showtitle']
+ for item in items:
+ try:
+ del info[item]
+ except KeyError:
+ pass
+ self.info = info
+
+ def getInfo(self):
+ tries = 0
+ while tries < 8 and self.isPlaying() is False:
+ xbmc.sleep(250)
+ try:
+ player = json.loads(xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}'))
+ except RuntimeError:
+ playerid = -1
+ playertype = 'none'
+ else:
+ try:
+ playerid = player['result'][0]['playerid']
+ playertype = player['result'][0]['type']
+ except KeyError:
+ playerid = -1
+ playertype = 'none'
+ if playertype == 'audio':
+ self.getAudioInfo(playerid)
+ self.rectifyUnknowns()
+ elif playertype == 'video':
+ self.getVideoInfo(playerid)
+ self.rectifyUnknowns()
+ else:
+ self.info = {}
+
+ def rectifyUnknowns(self):
+ items = {'filename': self.getPlayingFileX, 'aspectRatio': self.getAspectRatio, 'height': self.getResoluion,
+ 'title': self.getTitle}
+ for item in items.keys():
+ if item not in self.info.keys():
+ self.info[item] = items[item]()
+ else:
+ try:
+ if self.info[item] == '' or self.info[item] == 'unknown':
+ self.info[item] = items[item]()
+ except KeyError:
+ pass
+ pt = self.playing_type()
+ if 'mediaType' not in self.info.keys():
+ self.info['mediaType'] = pt
+ else:
+ try:
+ if pt != 'unknown' and self.info['mediaType'] != pt:
+ self.info['mediaType'] = pt
+ except KeyError:
+ pass
+
+ def getPlayingFileX(self):
+ try:
+ fn = self.getPlayingFile()
+ except RuntimeError:
+ fn = 'unknown'
+ if fn is None or fn == '':
+ fn = 'unknown'
+ return xbmc.translatePath(fn)
+
+ @staticmethod
+ def getAspectRatio():
+ ar = xbmc.getInfoLabel("VideoPlayer.VideoAspect")
+ if ar is None:
+ ar = 'unknown'
+ elif ar == '':
+ ar = 'unknown'
+ return str(ar)
+
+ @staticmethod
+ def getResoluion():
+ vr = xbmc.getInfoLabel("VideoPlayer.VideoResolution")
+ if vr is None:
+ vr = 'unknown'
+ elif vr == '':
+ vr = 'unknown'
+ return str(vr)
+
+ def onPlayBackStarted(self):
+ self.getInfo()
+ try:
+ self.totalTime = self.getTotalTime()
+ except RuntimeError:
+ self.totalTime = -1
+ finally:
+ if self.totalTime == 0:
+ self.totalTime = -1
+ topic = Topic('onPlayBackStarted')
+ self.publish(Message(topic, **self.info))
+
+ def onPlayBackEnded(self):
+ topic = Topic('onPlayBackEnded')
+ try:
+ tt = self.totalTime
+ tp = self.playingTime
+ pp = int(100 * tp / tt)
+ except RuntimeError:
+ pp = -1
+ except OverflowError:
+ pp = -1
+ self.publish(Message(topic, percentPlayed=str(pp), **self.info))
+ self.totalTime = -1.0
+ self.playingTime = 0.0
+ self.info = {}
+
+ def onPlayBackStopped(self):
+ self.onPlayBackEnded()
+
+ def onPlayBackPaused(self):
+ topic = Topic('onPlayBackPaused')
+ self.publish(Message(topic, time=str(self.getTime()), **self.info))
+
+ def onPlayBackResumed(self):
+ topic = Topic('onPlayBackResumed')
+ self.publish(Message(topic, time=str(self.getTime()), **self.info))
+
+ def onPlayBackSeek(self, time, seekOffset):
+ topic = Topic('onPlayBackSeek')
+ self.publish(Message(topic, time=str(time), **self.info))
+
+ def onPlayBackSeekChapter(self, chapter):
+ topic = Topic('onPlayBackSeekChapter')
+ self.publish(Message(topic, chapter=str(chapter), **self.info))
+
+ def onPlayBackSpeedChanged(self, speed):
+ topic = Topic('onPlayBackSpeedChanged')
+ self.publish(Message(topic, speed=str(speed), **self.info))
+
+ def onQueueNextItem(self):
+ topic = Topic('onQueueNextItem')
+ self.publish(Message(topic, **self.info))
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/schedule.py b/script.service.kodi.callbacks/resources/lib/publishers/schedule.py
new file mode 100644
index 0000000000..2e617d0717
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/schedule.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+from resources.lib import schedule
+import xbmc
+import threading
+from resources.lib.pubsub import Publisher, Message, Topic
+from resources.lib.events import Events
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+class SchedulePublisher(threading.Thread, Publisher):
+ publishes = Events().Schedule.keys()
+
+ def __init__(self, dispatcher, settings):
+ Publisher.__init__(self, dispatcher)
+ threading.Thread.__init__(self, name='SchedulePublisher')
+ self.dailyAlarms = settings.getEventsByType('onDailyAlarm')
+ self.intervalAlarms = settings.getEventsByType('onIntervalAlarm')
+ self.abortEvt = threading.Event()
+ self.abortEvt.clear()
+ self.sleep = xbmc.sleep
+ self.sleepinterval = 1000
+ self.schedules = []
+
+ def run(self):
+ for alarm in self.dailyAlarms:
+ hour = str(alarm['hour']).zfill(2)
+ minute = str(alarm['minute']).zfill(2)
+ stime = ':'.join([hour, minute])
+ schedule.every().day.at(stime).do(self.prePublishDailyAlarm, key=alarm['key'])
+ for alarm in self.intervalAlarms:
+ interval = alarm['hours'] * 3600 + alarm['minutes'] * 60 + alarm['seconds']
+ if interval > 0:
+ schedule.every(interval).seconds.do(self.prePublishIntervalAlarm, key=alarm['key'])
+ else:
+ xbmc.log(msg=_('onIntervalAlarm interval cannot be zero'))
+
+ while not self.abortEvt.is_set():
+ schedule.run_pending()
+ self.sleep(self.sleepinterval)
+ schedule.clear()
+
+ def prePublishDailyAlarm(self, key):
+ meseage = Message(Topic('onDailyAlarm', key))
+ self.publish(meseage)
+
+ def prePublishIntervalAlarm(self, key):
+ meseage = Message(Topic('onIntervalAlarm', key))
+ self.publish(meseage)
+
+ def abort(self, timeout=0):
+ self.abortEvt.set()
+ if timeout > 0:
+ self.join(timeout)
+ if self.is_alive():
+ xbmc.log(msg=_('Could not stop SchedulePublisher T:%i') % self.ident)
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/watchdog.py b/script.service.kodi.callbacks/resources/lib/publishers/watchdog.py
new file mode 100644
index 0000000000..26b058a968
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/watchdog.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import sys
+
+from resources.lib.pubsub import Publisher, Message, Topic
+from resources.lib.events import Events
+from resources.lib.utils.kodipathtools import translatepath
+try:
+ from watchdog.observers import Observer
+ from watchdog.events import PatternMatchingEventHandler
+except ImportError:
+ libs = translatepath('special://addon/resources/lib')
+ sys.path.append(libs)
+ libs = translatepath('special://addon/resources/lib/watchdog')
+ sys.path.append(libs)
+ try:
+ from resources.lib.watchdog.observers import Observer
+ from resources.lib.watchdog.events import PatternMatchingEventHandler
+ except ImportError:
+ raise
+
+class EventHandler(PatternMatchingEventHandler):
+ def __init__(self, patterns, ignore_patterns, ignore_directories, topic, publish):
+ super(EventHandler, self).__init__(patterns=patterns, ignore_patterns=ignore_patterns, ignore_directories=ignore_directories)
+ self.topic = topic
+ self.publish = publish
+
+ def on_any_event(self, event):
+ msg = Message(self.topic, path=event.src_path, event=event.event_type)
+ self.publish(msg)
+
+class WatchdogPublisher(Publisher):
+ publishes = Events().Watchdog.keys()
+
+ def __init__(self, dispatcher, settings):
+ super(WatchdogPublisher, self).__init__(dispatcher)
+ self.watchdogSettings = settings.getWatchdogSettings()
+ self.event_handlers = []
+ self.observersettings = []
+ self.observers = []
+ self.initialize()
+
+ def initialize(self):
+ for setting in self.watchdogSettings:
+ patterns = setting['patterns'].split(',')
+ ignore_patterns = setting['ignore_patterns'].split(',')
+ eh = EventHandler(patterns=patterns, ignore_patterns=ignore_patterns,
+ ignore_directories=setting['ignore_directories'],
+ topic=Topic('onFileSystemChange', setting['key']), publish=self.publish)
+ self.event_handlers.append(eh)
+ folder = translatepath(setting['folder'])
+ mysetting = [eh, folder, setting['recursive']]
+ self.observersettings.append(mysetting)
+
+ def start(self):
+ for item in self.observersettings:
+ observer = Observer()
+ observer.schedule(item[0], item[1], recursive=item[2])
+ observer.start()
+ self.observers.append(observer)
+
+ def abort(self, timeout=0):
+ for item in self.observers:
+ assert isinstance(item, Observer)
+ item.stop()
+ if timeout > 0:
+ self.join(timeout)
+
+ def join(self, timeout=0):
+ for item in self.observers:
+ if timeout > 0:
+ item.join(timeout)
+ else:
+ item.join()
+
+
diff --git a/script.service.kodi.callbacks/resources/lib/publishers/watchdogStartup.py b/script.service.kodi.callbacks/resources/lib/publishers/watchdogStartup.py
new file mode 100644
index 0000000000..e27236c88b
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/publishers/watchdogStartup.py
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import sys
+import pickle
+import os
+import time
+from resources.lib.utils.kodipathtools import translatepath, setPathRW
+libs = translatepath('special://addon/resources/lib')
+sys.path.append(libs)
+libs = translatepath('special://addon/resources/lib/watchdog')
+sys.path.append(libs)
+
+import xbmc
+from resources.lib.events import Events
+from resources.lib.watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff
+from resources.lib.watchdog.observers import Observer
+from resources.lib.watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CREATED, EVENT_TYPE_DELETED, EVENT_TYPE_MODIFIED, EVENT_TYPE_MOVED
+from resources.lib.pubsub import Publisher, Message, Topic
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+from resources.lib.kodilogging import KodiLogger
+klogger = KodiLogger()
+log = klogger.log
+
+
+class EventHandler(PatternMatchingEventHandler):
+ def __init__(self, patterns, ignore_patterns, ignore_directories):
+ super(EventHandler, self).__init__(patterns=patterns, ignore_patterns=ignore_patterns,
+ ignore_directories=ignore_directories)
+ self.data = {}
+
+ def on_any_event(self, event):
+ if event.is_directory:
+ et = 'Dirs%s' % event.event_type.capitalize()
+ else:
+ et = 'Files%s' % event.event_type.capitalize()
+ if et in self.data.keys():
+ self.data[et].append(event.src_path)
+ else:
+ self.data[et] = [event.src_path]
+
+class WatchdogStartup(Publisher):
+ publishes = Events().WatchdogStartup.keys()
+
+ def __init__(self, dispatcher, settings):
+ super(WatchdogStartup, self).__init__(dispatcher)
+ self.settings = settings.getWatchdogStartupSettings()
+
+ def start(self):
+ oldsnapshots = WatchdogStartup.getPickle()
+ newsnapshots = {}
+ for setting in self.settings:
+ folder = translatepath(setting['ws_folder'])
+ if os.path.exists(folder):
+ newsnapshot = DirectorySnapshot(folder, recursive=setting['ws_recursive'])
+ newsnapshots[folder] = newsnapshot
+ if oldsnapshots is not None:
+ if folder in oldsnapshots.keys():
+ oldsnapshot = oldsnapshots[folder]
+ diff = DirectorySnapshotDiff(oldsnapshot, newsnapshot)
+ changes = self.getChangesFromDiff(diff)
+ if len(changes) > 0:
+ eh = EventHandler(patterns=setting['ws_patterns'].split(','), ignore_patterns=setting['ws_ignore_patterns'].split(','),
+ ignore_directories=setting['ws_ignore_directories'])
+ observer = Observer()
+ try:
+ observer.schedule(eh, folder, recursive=setting['ws_recursive'])
+ time.sleep(0.5)
+ for change in changes:
+ eh.dispatch(change)
+ time.sleep(0.25)
+ try:
+ observer.unschedule_all()
+ except Exception:
+ pass
+ except Exception:
+ raise
+ if len(eh.data) > 0:
+ message = Message(Topic('onStartupFileChanges', setting['key']), listOfChanges=eh.data)
+ self.publish(message)
+ else:
+ message = Message(Topic('onStartupFileChanges', setting['key']), listOfChanges=[{'DirsDeleted':folder}])
+ log(msg=_('Watchdog Startup folder not found: %s') % folder)
+ self.publish(message)
+
+ @staticmethod
+ def getChangesFromDiff(diff):
+ ret = []
+ events = {'dirs_created':(EVENT_TYPE_CREATED, True), 'dirs_deleted':(EVENT_TYPE_DELETED, True), 'dirs_modified':(EVENT_TYPE_MODIFIED, True), 'dirs_moved':(EVENT_TYPE_MOVED, True),
+ 'files_created':(EVENT_TYPE_CREATED, False), 'files_deleted':(EVENT_TYPE_DELETED, False), 'files_modified':(EVENT_TYPE_MODIFIED, False), 'files_moved':(EVENT_TYPE_MOVED, False)}
+ for event in events.keys():
+ try:
+ mylist = diff.__getattribute__(event)
+ except AttributeError:
+ mylist = []
+ if len(mylist) > 0:
+ for item in mylist:
+ evt = FileSystemEvent(item)
+ evt.event_type = events[event][0]
+ evt.is_directory = events[event][1]
+ ret.append(evt)
+ return ret
+
+ def abort(self, *args):
+ snapshots = {}
+ for setting in self.settings:
+ folder = xbmc.translatePath(setting['ws_folder'])
+ if folder == u'':
+ folder = setting['ws_folder']
+ folder = translatepath(folder)
+ if os.path.exists(folder):
+ snapshot = DirectorySnapshot(folder, recursive=setting['ws_recursive'])
+ snapshots[folder] = snapshot
+ WatchdogStartup.savePickle(snapshots)
+
+ def join(self, timeout=None):
+ pass
+
+ @staticmethod
+ def savePickle(snapshots):
+ picklepath = WatchdogStartup.getPicklePath()
+ try:
+ with open(picklepath, 'w') as f:
+ pickle.dump(snapshots, f)
+ except pickle.PicklingError:
+ log(msg=_('Watchdog startup pickling error on exit'))
+ except OSError:
+ log(msg=_('Watchdog startup OSError on pickle attempt'))
+ else:
+ log(msg=_('Watchdog startup pickle saved'))
+
+ @staticmethod
+ def clearPickle():
+ path = WatchdogStartup.getPicklePath()
+ if os.path.exists(path):
+ try:
+ os.remove(path)
+ except OSError:
+ log(msg=_('Watchdog startup could not clear pickle'))
+
+ @staticmethod
+ def getPicklePath():
+ path = translatepath('special://addondata/watchdog.pkl')
+ setPathRW(os.path.split(path)[0])
+ return path
+
+ @staticmethod
+ def getPickle():
+ picklepath = WatchdogStartup.getPicklePath()
+ if not os.path.exists(picklepath):
+ return
+ try:
+ with open(picklepath, 'r') as f:
+ oldsnapshots = pickle.load(f)
+ except OSError:
+ log (msg=_('Watchdog Startup could not load pickle'))
+ except pickle.UnpicklingError:
+ log (msg=_('Watchdog Startup unpickling error'))
+ else:
+ return oldsnapshots
+
diff --git a/script.service.kodi.callbacks/resources/lib/pubsub.py b/script.service.kodi.callbacks/resources/lib/pubsub.py
new file mode 100644
index 0000000000..85ade45df9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/pubsub.py
@@ -0,0 +1,316 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+import Queue
+import abc
+import copy
+import threading
+import time
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+LOGLEVEL_CRITICAL = 50
+LOGLEVEL_ERROR = 40
+LOGLEVEL_WARNING = 30
+LOGLEVEL_INFO = 20
+LOGLEVEL_DEBUG = 10
+
+
+class BaseLogger(object):
+ __metaclass__ = abc.ABCMeta
+ selfloglevel = LOGLEVEL_INFO
+
+ @staticmethod
+ def setLogLevel(loglevel):
+ BaseLogger.selfloglevel = loglevel
+
+ @staticmethod
+ @abc.abstractmethod
+ def log(loglevel=None, msg=None):
+ pass
+
+
+class PrintLogger(BaseLogger):
+ @staticmethod
+ def log(loglevel=LOGLEVEL_INFO, msg='Log Event'):
+ if loglevel >= BaseLogger.selfloglevel:
+ print msg
+
+class TaskReturn(object):
+ def __init__(self, iserror=False, msg=''):
+ self.iserror = iserror
+ self.msg = msg
+ self.taskId = None
+ self.eventId = None
+
+
+def DummyReturnHandler(*args, **kwargs):
+ pass
+
+
+class DummyLogger(BaseLogger):
+ @staticmethod
+ def log(*args):
+ pass
+
+
+class Topic(object):
+ def __init__(self, topic, subtopic=None):
+ self.topic = topic
+ self.subtopic = subtopic
+
+ def has_subtopic(self):
+ if self.subtopic is None:
+ return False
+ else:
+ return True
+
+ def __eq__(self, other):
+ assert isinstance(other, Topic)
+ if other.has_subtopic():
+ if self.has_subtopic():
+ if other.topic == self.topic and other.subtopic == self.subtopic:
+ return True
+ else:
+ return False
+ else: ## self has no subtopic
+ return False
+ else:
+ if self.has_subtopic():
+ if self.topic == other.topic:
+ return True
+ else:
+ return False
+ else:
+ if self.topic == other.topic:
+ return True
+ else:
+ return False
+
+ def __repr__(self):
+ if self.has_subtopic():
+ return '%s:%s' % (self.topic, self.subtopic)
+ else:
+ return self.topic
+
+
+class Message(object):
+ def __init__(self, topic, **kwargs):
+ self.topic = topic
+ self.kwargs = kwargs
+
+
+class Dispatcher(threading.Thread):
+ def __init__(self, interval=0.1, sleepfxn=time.sleep):
+ super(Dispatcher, self).__init__(name='Dispatcher')
+ self._message_q = Queue.Queue()
+ self._abort_evt = threading.Event()
+ self._abort_evt.clear()
+ self.subscribers = []
+ self.running = False
+ self.interval = interval
+ self.sleepfxn = sleepfxn
+
+ def addSubscriber(self, subscriber):
+ assert isinstance(subscriber, Subscriber)
+ wasrunning = False
+ if self.running:
+ self.abort()
+ self.join()
+ wasrunning = True
+ self.subscribers.append(subscriber)
+ if wasrunning:
+ self.start()
+
+ def q_message(self, message):
+ self._message_q.put(copy.copy(message), block=False)
+
+ def run(self):
+ self.running = True
+ while not self._abort_evt.is_set():
+ while not self._message_q.empty():
+ try:
+ msg = self._message_q.get_nowait()
+ assert isinstance(msg, Message)
+ except Queue.Empty:
+ continue
+ except AssertionError:
+ raise
+ for s in self.subscribers:
+ if msg.topic in s.topics:
+ s.notify(msg)
+ self.sleepfxn(self.interval)
+ self.running = False
+
+ def abort(self, timeout=0):
+ if self.running:
+ self._abort_evt.set()
+ if timeout > 0:
+ self.join(timeout)
+
+
+class Publisher(object):
+ __metaclass__ = abc.ABCMeta
+ def __init__(self, dispatcher):
+ self.dispatcher = dispatcher
+
+ def publish(self, message):
+ self.dispatcher.q_message(message)
+
+ @abc.abstractmethod
+ def start(self):
+ pass
+
+ @abc.abstractmethod
+ def abort(self, timeout=0):
+ pass
+
+ @abc.abstractmethod
+ def join(self, timeout=0):
+ pass
+
+
+
+class Task(threading.Thread):
+ __metaclass__ = abc.ABCMeta
+
+ def __init__(self):
+ super(Task, self).__init__(name='Task')
+ self.kwargs = {}
+ self.userargs = []
+ self.topic = None
+ self.returnQ = Queue.Queue()
+
+ def t_start(self, topic, *args, **kwargs):
+
+ self.userargs = args
+ self.kwargs = kwargs
+ self.topic = topic
+ self.start()
+
+ @abc.abstractmethod
+ def run(self):
+ ret = TaskReturn()
+ self.returnQ.put(ret)
+
+
+class TaskManager(object):
+ def __init__(self, task, maxrunning=1, refractory=None, maxruns=-1, **taskKwargs):
+ self.task = task
+ self.maxrunning = maxrunning
+ self.refractory_period = refractory
+ self.run_tasks = []
+ self.most_recent_task_time = time.time()
+ self.max_runs = maxruns
+ self.run_count = 0
+ self.taskKwargs = taskKwargs
+ self.returnHandler = DummyReturnHandler
+
+ def start(self, topic, **kwargs):
+ taskReturn = TaskReturn(iserror=True)
+ taskReturn.eventId = str(topic)
+ taskReturn.taskId = ''
+ if self.max_runs > 0:
+ if self.run_count >= self.max_runs:
+ taskReturn.msg = TaskManagerException_TaskCountExceeded.message
+ self.returnHandler(taskReturn)
+ return
+ if self.maxrunning > 0:
+ count = 0
+ for i, task in enumerate(self.run_tasks):
+ assert isinstance(task, threading.Thread)
+ if task.is_alive():
+ count += 1
+ else:
+ del task
+ del self.run_tasks[i]
+ if count >= self.maxrunning:
+ taskReturn.msg = TaskManagerException_TaskAlreadyRunning.message
+ self.returnHandler(taskReturn)
+ return
+ if self.refractory_period > 0:
+ tnow = time.time()
+ if tnow - self.most_recent_task_time < self.refractory_period:
+ taskReturn.msg = TaskManagerException_TaskInRefractoryPeriod.message
+ self.returnHandler(taskReturn)
+ return
+ else:
+ self.most_recent_task_time = tnow
+
+ # Launch the task
+ self.run_count += 1
+
+ task = self.task()
+ task.t_start(topic, self.taskKwargs, **kwargs)
+ if self.maxrunning > 0:
+ self.run_tasks.append(task)
+ while task.returnQ.empty():
+ pass
+ taskReturn = task.returnQ.get_nowait()
+ assert isinstance(taskReturn, TaskReturn)
+ self.returnHandler(taskReturn)
+
+
+class TaskManagerException_TaskCountExceeded(Exception):
+ def __init__(self):
+ super(TaskManagerException_TaskCountExceeded, self).__init__(_('Task not run because task count exceeded'))
+
+
+class TaskManagerException_TaskAlreadyRunning(Exception):
+ def __init__(self):
+ super(TaskManagerException_TaskAlreadyRunning, self).__init__(_('Task not run because task already running'))
+
+
+class TaskManagerException_TaskInRefractoryPeriod(Exception):
+ def __init__(self):
+ super(TaskManagerException_TaskInRefractoryPeriod, self).__init__(
+ _('Task not run because task is in refractory period'))
+
+
+class Subscriber(object):
+ def __init__(self, logger=DummyLogger, loglevel=LOGLEVEL_INFO):
+ self.topics = []
+ self.taskmanagers = []
+ self.logger = logger
+ self.loglevel = loglevel
+
+ def addTopic(self, topic):
+ assert isinstance(topic, Topic)
+ self.topics.append(topic)
+
+ def addTaskManager(self, tm):
+ assert isinstance(tm, TaskManager)
+ self.taskmanagers.append(tm)
+
+ def notify(self, message):
+ for taskmanager in self.taskmanagers:
+ try:
+ self.logger.log(self.loglevel, _('Task starting for %s') % message.topic)
+ taskmanager.start(message.topic, **message.kwargs)
+ except TaskManagerException_TaskAlreadyRunning as e:
+ self.logger.log(self.loglevel, '%s - %s' % (message.topic, e.message))
+ except TaskManagerException_TaskInRefractoryPeriod as e:
+ self.logger.log(self.loglevel, '%s - %s' % (message.topic, e.message))
+ except TaskManagerException_TaskCountExceeded as e:
+ self.logger.log(self.loglevel, '%s - %s' % (message.topic, e.message))
+ except Exception as e:
+ raise e
+ else:
+ self.logger.log(self.loglevel, _('Task finalized for %s') % message.topic)
diff --git a/script.service.kodi.callbacks/resources/lib/schedule/__init__.py b/script.service.kodi.callbacks/resources/lib/schedule/__init__.py
new file mode 100644
index 0000000000..ed13587b43
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/schedule/__init__.py
@@ -0,0 +1,454 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""
+Python job scheduling for humans.
+
+An in-process scheduler for periodic jobs that uses the builder pattern
+for configuration. Schedule lets you run Python functions (or any other
+callable) periodically at pre-determined intervals using a simple,
+human-friendly syntax.
+
+Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the
+"clockwork" Ruby module [2][3].
+
+Features:
+ - A simple to use API for scheduling jobs.
+ - Very lightweight and no external dependencies.
+ - Excellent test coverage.
+ - Works with Python 2.7 and 3.3
+
+Usage:
+ >>> from resources.lib schedule
+ >>> import time
+
+ >>> def job(message='stuff'):
+ >>> print("I'm working on:", message)
+
+ >>> schedule.every(10).minutes.do(job)
+ >>> schedule.every().hour.do(job, message='things')
+ >>> schedule.every().day.at("10:30").do(job)
+
+ >>> while True:
+ >>> schedule.run_pending()
+ >>> time.sleep(1)
+
+[1] http://adam.heroku.com/past/2010/4/13/rethinking_cron/
+[2] https://github.com/tomykaira/clockwork
+[3] http://adam.heroku.com/past/2010/6/30/replace_cron_with_clockwork/
+"""
+import datetime
+import functools
+import logging
+import time
+
+logger = logging.getLogger('schedule')
+
+
+class CancelJob(object):
+ pass
+
+
+class Scheduler(object):
+ def __init__(self):
+ self.jobs = []
+
+ def run_pending(self):
+ """Run all jobs that are scheduled to run.
+
+ Please note that it is *intended behavior that tick() does not
+ run missed jobs*. For example, if you've registered a job that
+ should run every minute and you only call tick() in one hour
+ increments then your job won't be run 60 times in between but
+ only once.
+ """
+ runnable_jobs = (job for job in self.jobs if job.should_run)
+ for job in sorted(runnable_jobs):
+ self._run_job(job)
+
+ def run_all(self, delay_seconds=0):
+ """
+ Run all jobs regardless if they are scheduled to run or not.
+
+ A delay of `delay` seconds is added between each job. This helps
+ distribute system load generated by the jobs more evenly
+ over time.
+ :param delay_seconds:
+ :type delay_seconds: float
+ :return:
+ :rtype:
+ """
+ logger.info('Running *all* %i jobs with %is delay inbetween',
+ len(self.jobs), delay_seconds)
+ for job in self.jobs:
+ self._run_job(job)
+ time.sleep(delay_seconds)
+
+ def clear(self):
+ """Deletes all scheduled jobs."""
+ del self.jobs[:]
+
+ def cancel_job(self, job):
+ """
+ Delete a scheduled job.
+ :param job:
+ :type job: Job
+ :return:
+ :rtype:
+ """
+ try:
+ self.jobs.remove(job)
+ except ValueError:
+ pass
+
+ def every(self, interval=1):
+ """
+ Schedule a new periodic job.
+ :param interval:
+ :type interval: int
+ :return:
+ :rtype:
+ """
+ job = Job(interval)
+ self.jobs.append(job)
+ return job
+
+ def _run_job(self, job):
+ ret = job.run()
+ if isinstance(ret, CancelJob) or ret is CancelJob:
+ self.cancel_job(job)
+
+ @property
+ def next_run(self):
+ """Datetime when the next job should run."""
+ if not self.jobs:
+ return None
+ return min(self.jobs).next_run
+
+ @property
+ def idle_seconds(self):
+ """Number of seconds until `next_run`."""
+ return (self.next_run - datetime.datetime.now()).total_seconds()
+
+
+class Job(object):
+ """A periodic job as used by `Scheduler`."""
+ def __init__(self, interval):
+ self.interval = interval # pause interval * unit between runs
+ self.job_func = None # the job job_func to run
+ self.unit = None # time units, e.g. 'minutes', 'hours', ...
+ self.at_time = None # optional time at which this job runs
+ self.last_run = None # datetime of the last run
+ self.next_run = None # datetime of the next run
+ self.period = None # timedelta between runs, only valid for
+ self.start_day = None # Specific day of the week to start on
+
+ def __lt__(self, other):
+ """
+ PeriodicJobs are sortable based on the scheduled time
+ they run next.
+ :param other:
+ :type other: Job
+ :return:
+ :rtype:
+ """
+ return self.next_run < other.next_run
+
+ def __repr__(self):
+ def format_time(t):
+ return t.strftime('%Y-%m-%d %H:%M:%S') if t else '[never]'
+
+ timestats = '(last run: %s, next run: %s)' % (
+ format_time(self.last_run), format_time(self.next_run))
+
+ if hasattr(self.job_func, '__name__'):
+ job_func_name = self.job_func.__name__
+ else:
+ job_func_name = repr(self.job_func)
+ args = [repr(x) for x in self.job_func.args]
+ kwargs = ['%s=%s' % (k, repr(v))
+ for k, v in self.job_func.keywords.items()]
+ call_repr = job_func_name + '(' + ', '.join(args + kwargs) + ')'
+
+ if self.at_time is not None:
+ return 'Every %s %s at %s do %s %s' % (
+ self.interval,
+ self.unit[:-1] if self.interval == 1 else self.unit,
+ self.at_time, call_repr, timestats)
+ else:
+ return 'Every %s %s do %s %s' % (
+ self.interval,
+ self.unit[:-1] if self.interval == 1 else self.unit,
+ call_repr, timestats)
+
+ @property
+ def second(self):
+ assert self.interval == 1, 'Use seconds instead of second'
+ return self.seconds
+
+ @property
+ def seconds(self):
+ self.unit = 'seconds'
+ return self
+
+ @property
+ def minute(self):
+ assert self.interval == 1, 'Use minutes instead of minute'
+ return self.minutes
+
+ @property
+ def minutes(self):
+ self.unit = 'minutes'
+ return self
+
+ @property
+ def hour(self):
+ assert self.interval == 1, 'Use hours instead of hour'
+ return self.hours
+
+ @property
+ def hours(self):
+ self.unit = 'hours'
+ return self
+
+ @property
+ def day(self):
+ assert self.interval == 1, 'Use days instead of day'
+ return self.days
+
+ @property
+ def days(self):
+ self.unit = 'days'
+ return self
+
+ @property
+ def week(self):
+ assert self.interval == 1, 'Use weeks instead of week'
+ return self.weeks
+
+ @property
+ def weeks(self):
+ self.unit = 'weeks'
+ return self
+
+ @property
+ def monday(self):
+ assert self.interval == 1, 'Use mondays instead of monday'
+ self.start_day = 'monday'
+ return self.weeks
+
+ @property
+ def tuesday(self):
+ assert self.interval == 1, 'Use tuesdays instead of tuesday'
+ self.start_day = 'tuesday'
+ return self.weeks
+
+ @property
+ def wednesday(self):
+ assert self.interval == 1, 'Use wedesdays instead of wednesday'
+ self.start_day = 'wednesday'
+ return self.weeks
+
+ @property
+ def thursday(self):
+ assert self.interval == 1, 'Use thursday instead of thursday'
+ self.start_day = 'thursday'
+ return self.weeks
+
+ @property
+ def friday(self):
+ assert self.interval == 1, 'Use fridays instead of friday'
+ self.start_day = 'friday'
+ return self.weeks
+
+ @property
+ def saturday(self):
+ assert self.interval == 1, 'Use saturdays instead of saturday'
+ self.start_day = 'saturday'
+ return self.weeks
+
+ @property
+ def sunday(self):
+ assert self.interval == 1, 'Use sundays instead of sunday'
+ self.start_day = 'sunday'
+ return self.weeks
+
+ def at(self, time_str):
+ """
+ Schedule the job every day at a specific time.
+
+ Calling this is only valid for jobs scheduled to run every
+ N day(s).
+ :param time_str:
+ :type time_str: str
+ :return:
+ :rtype:
+ """
+ assert self.unit in ('days', 'hours') or self.start_day
+ hour, minute = time_str.split(':')
+ minute = int(minute)
+ if self.unit == 'days' or self.start_day:
+ hour = int(hour)
+ assert 0 <= hour <= 23
+ elif self.unit == 'hours':
+ hour = 0
+ assert 0 <= minute <= 59
+ self.at_time = datetime.time(hour, minute)
+ return self
+
+ def do(self, job_func, *args, **kwargs):
+ """
+ Specifies the job_func that should be called every time the
+ job runs.
+
+ Any additional arguments are passed on to job_func when
+ the job runs.
+ :param job_func:
+ :type job_func: instancemethod
+ :param args:
+ :type args:
+ :param kwargs:
+ :type kwargs:
+ :return:
+ :rtype:
+ """
+ self.job_func = functools.partial(job_func, *args, **kwargs)
+ try:
+ functools.update_wrapper(self.job_func, job_func)
+ except AttributeError:
+ # job_funcs already wrapped by functools.partial won't have
+ # __name__, __module__ or __doc__ and the update_wrapper()
+ # call will fail.
+ pass
+ self._schedule_next_run()
+ return self
+
+ @property
+ def should_run(self):
+ """True if the job should be run now."""
+ return datetime.datetime.now() >= self.next_run
+
+ def run(self):
+ """Run the job and immediately reschedule it."""
+ logger.info('Running job %s', self)
+ ret = self.job_func()
+ self.last_run = datetime.datetime.now()
+ self._schedule_next_run()
+ return ret
+
+ def _schedule_next_run(self):
+ """Compute the instant when this job should run next."""
+ # Allow *, ** magic temporarily:
+ # pylint: disable=W0142
+ assert self.unit in ('seconds', 'minutes', 'hours', 'days', 'weeks')
+ self.period = datetime.timedelta(**{self.unit: self.interval})
+ self.next_run = datetime.datetime.now() + self.period
+ if self.start_day is not None:
+ assert self.unit == 'weeks'
+ weekdays = (
+ 'monday',
+ 'tuesday',
+ 'wednesday',
+ 'thursday',
+ 'friday',
+ 'saturday',
+ 'sunday'
+ )
+ assert self.start_day in weekdays
+ weekday = weekdays.index(self.start_day)
+ days_ahead = weekday - self.next_run.weekday()
+ if days_ahead <= 0: # Target day already happened this week
+ days_ahead += 7
+ self.next_run += datetime.timedelta(days_ahead) - self.period
+ if self.at_time is not None:
+ assert self.unit in ('days', 'hours') or self.start_day is not None
+ kwargs = {
+ 'minute': self.at_time.minute,
+ 'second': self.at_time.second,
+ 'microsecond': 0
+ }
+ if self.unit == 'days' or self.start_day is not None:
+ kwargs['hour'] = self.at_time.hour
+ self.next_run = self.next_run.replace(**kwargs)
+ # If we are running for the first time, make sure we run
+ # at the specified time *today* (or *this hour*) as well
+ if not self.last_run:
+ now = datetime.datetime.now()
+ if (self.unit == 'days' and self.at_time > now.time() and
+ self.interval == 1):
+ self.next_run = self.next_run - datetime.timedelta(days=1)
+ elif self.unit == 'hours' and self.at_time.minute > now.minute:
+ self.next_run = self.next_run - datetime.timedelta(hours=1)
+ if self.start_day is not None and self.at_time is not None:
+ # Let's see if we will still make that time we specified today
+ if (self.next_run - datetime.datetime.now()).days >= 7:
+ self.next_run -= self.period
+
+# The following methods are shortcuts for not having to
+# create a Scheduler instance:
+
+default_scheduler = Scheduler()
+jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?
+
+
+def every(interval=1):
+ """
+ Schedule a new periodic job.
+ :param interval:
+ :type interval: int
+ :return:
+ :rtype:
+ """
+ return default_scheduler.every(interval)
+
+
+def run_pending():
+ """Run all jobs that are scheduled to run.
+
+ Please note that it is *intended behavior that run_pending()
+ does not run missed jobs*. For example, if you've registered a job
+ that should run every minute and you only call run_pending()
+ in one hour increments then your job won't be run 60 times in
+ between but only once.
+ """
+ default_scheduler.run_pending()
+
+
+def run_all(delay_seconds=0):
+ """
+ Run all jobs regardless if they are scheduled to run or not.
+
+ A delay of `delay` seconds is added between each job. This can help
+ to distribute the system load generated by the jobs more evenly over
+ time.
+ :param delay_seconds:
+ :type delay_seconds: float
+ :return:
+ :rtype:
+ """
+ default_scheduler.run_all(delay_seconds=delay_seconds)
+
+
+def clear():
+ """Deletes all scheduled jobs."""
+ default_scheduler.clear()
+
+
+def cancel_job(job):
+ """
+ Delete a scheduled job.
+ :param job:
+ :type job: Job
+ :return:
+ :rtype:
+ """
+ default_scheduler.cancel_job(job)
+
+
+def next_run():
+ """Datetime when the next job should run."""
+ return default_scheduler.next_run
+
+
+def idle_seconds():
+ """Number of seconds until `next_run`."""
+ return default_scheduler.idle_seconds
diff --git a/script.service.kodi.callbacks/resources/lib/settings.py b/script.service.kodi.callbacks/resources/lib/settings.py
new file mode 100644
index 0000000000..6ef721debf
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/settings.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+import xbmcaddon
+from resources.lib import taskdict
+from resources.lib.events import Events
+from resources.lib.events import requires_subtopic
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.pubsub import Topic
+from resources.lib.utils.kodipathtools import translatepath
+from resources.lib.utils.poutil import PoDict
+
+podict = PoDict()
+podict.read_from_file(translatepath('special://addon/resources/language/English/strings.po'))
+
+
+def getEnglishStringFromId(msgctxt):
+ status, ret = podict.has_msgctxt(msgctxt)
+ if status is True:
+ return ret
+ else:
+ return ''
+
+
+_ = getEnglishStringFromId
+
+try:
+ addonid = xbmcaddon.Addon('script.service.kodi.callbacks').getAddonInfo('id')
+except RuntimeError:
+ addonid = 'script.service.kodi.callbacks'
+else:
+ if addonid == '':
+ addonid = 'script.service.kodi.callbacks'
+
+kl = KodiLogger()
+log = kl.log
+
+
+def get(settingid, var_type):
+ t = xbmcaddon.Addon(addonid).getSetting(settingid)
+ if var_type == 'text' or var_type == 'file' or var_type == 'folder' or var_type == 'sfile' or var_type == 'sfolder' or var_type == 'labelenum':
+ return t
+ elif var_type == 'int':
+ try:
+ return int(t)
+ except TypeError:
+ log(msg='TYPE ERROR for variable %s. Expected int got "%s"' % (settingid, t))
+ return 0
+ elif var_type == 'bool':
+ if t == 'false':
+ return False
+ else:
+ return True
+ else:
+ log(msg='ERROR Could not process variable %s = "%s"' % (settingid, t))
+ return None
+
+
+class Settings(object):
+ allevents = Events().AllEvents
+ taskSuffixes = {'general': [['maxrunning', 'int'], ['maxruns', 'int'], ['refractory', 'int']],
+ }
+ eventsReverseLookup = None
+
+ def __init__(self):
+ self.tasks = {}
+ self.events = {}
+ self.general = {}
+ rl = {}
+ for key in Settings.allevents.keys():
+ evt = Settings.allevents[key]
+ rl[evt['text']] = key
+ Settings.eventsReverseLookup = rl
+
+ def logSettings(self):
+ import pprint
+ settingspp = {'Tasks': self.tasks, 'Events': self.events, 'General': self.general}
+ pp = pprint.PrettyPrinter(indent=2)
+ msg = pp.pformat(settingspp)
+ kl = KodiLogger()
+ kl.log(msg=msg)
+
+ def getSettings(self):
+ self.getTaskSettings()
+ self.getEventSettings()
+ self.getGeneralSettings()
+
+ def getTaskSettings(self):
+ for i in xrange(1, 11):
+ pid = 'T%s' % str(i)
+ tsk = self.getTaskSetting(pid)
+ if tsk is not None:
+ self.tasks[pid] = tsk
+
+ @staticmethod
+ def getTaskSetting(pid):
+ tsk = {}
+ tasktype = get('%s.type' % pid, 'text')
+ if tasktype == 'none':
+ return None
+ else:
+ tsk['type'] = tasktype
+ for suff in Settings.taskSuffixes['general']:
+ tsk[suff[0]] = get('%s.%s' % (pid, suff[0]), suff[1])
+ for var in taskdict[tasktype]['variables']:
+ tsk[var['id']] = get('%s.%s' % (pid, var['id']), var['settings']['type'])
+ return tsk
+
+ def getEventSettings(self):
+ for i in xrange(1, 11):
+ pid = "E%s" % str(i)
+ evt = self.getEventSetting(pid)
+ if evt is not None:
+ self.events[pid] = evt
+
+ @staticmethod
+ def getEventSetting(pid):
+ evt = {}
+ et = get('%s.type' % pid, 'text')
+ if et == podict.has_msgid('None')[1]:
+ return
+ else:
+ et = _(et)
+ et = Settings.eventsReverseLookup[et]
+ evt['type'] = et
+ tsk = get('%s.task' % pid, 'text')
+ if tsk == '' or tsk.lower() == 'none':
+ return None
+ evt['task'] = 'T%s' % int(tsk[5:])
+ for ri in Settings.allevents[et]['reqInfo']:
+ evt[ri[0]] = get('%s.%s' % (pid, ri[0]), ri[1])
+ evt['userargs'] = get('%s.userargs' % pid, 'text')
+ return evt
+
+ @staticmethod
+ def getTestEventSettings(taskId):
+ evt = {'type': 'onTest', 'task': taskId}
+ for oa in Settings.allevents['onTest']['optArgs']:
+ evt[oa] = True
+ evt['eventId'] = True
+ evt['taskId'] = True
+ return evt
+
+ def getGeneralSettings(self):
+ polls = ['LoopFreq', 'LogFreq', 'TaskFreq']
+ self.general['Notify'] = get('Notify', 'bool')
+ for p in polls:
+ self.general[p] = get(p, 'int')
+ self.general['elevate_loglevel'] = get('loglevel', 'bool')
+
+ def getOpenwindowids(self):
+ ret = {}
+ for evtkey in self.events.keys():
+ evt = self.events[evtkey]
+ if evt['type'] == 'onWindowOpen':
+ ret[evt['windowIdO']] = evt['key']
+ return ret
+
+ def getClosewindowids(self):
+ ret = {}
+ for evtkey in self.events.keys():
+ evt = self.events[evtkey]
+ if evt['type'] == 'onWindowClose':
+ ret[evt['windowIdC']] = evt['key']
+ return ret
+
+ def getEventsByType(self, eventType):
+ ret = []
+ for key in self.events.keys():
+ evt = self.events[key]
+ if evt['type'] == eventType:
+ evt['key'] = key
+ ret.append(evt)
+ return ret
+
+ def getIdleTimes(self):
+ idleEvts = self.getEventsByType('onIdle')
+ ret = {}
+ for evt in idleEvts:
+ ret[evt['key']] = int(evt['idleTime'])
+ return ret
+
+ def getAfterIdleTimes(self):
+ idleEvts = self.getEventsByType('afterIdle')
+ ret = {}
+ for evt in idleEvts:
+ ret[evt['key']] = int(evt['afterIdleTime'])
+ return ret
+
+ def getJsonNotifications(self):
+ jsonEvts = self.getEventsByType('onNotification')
+ ret = []
+ dic = {}
+ for evt in jsonEvts:
+ dic['eventId'] = evt['key']
+ dic['sender'] = evt['reqInfo']['sender']
+ dic['method'] = evt['regInfo']['method']
+ dic['data'] = evt['reqInfo']['data']
+ ret.append(dic)
+ return ret
+
+ def getLogSimples(self):
+ evts = self.getEventsByType('onLogSimple')
+ ret = []
+ for evt in evts:
+ ret.append({'matchIf': evt['matchIf'], 'rejectIf': evt['rejectIf'], 'eventId': evt['key']})
+ return ret
+
+ def getLogRegexes(self):
+ evts = self.getEventsByType('onLogRegex')
+ ret = []
+ for evt in evts:
+ ret.append({'matchIf': evt['matchIf'], 'rejectIf': evt['rejectIf'], 'eventId': evt['key']})
+ return ret
+
+ def getWatchdogSettings(self):
+ evts = self.getEventsByType('onFileSystemChange')
+ return evts
+
+ def getWatchdogStartupSettings(self):
+ evts = self.getEventsByType('onStartupFileChanges')
+ return evts
+
+ def topicFromSettingsEvent(self, key):
+ top = self.events[key]['type']
+ if top in requires_subtopic():
+ return Topic(top, key)
+ else:
+ return Topic(top)
diff --git a/script.service.kodi.callbacks/resources/lib/subscriberfactory.py b/script.service.kodi.callbacks/resources/lib/subscriberfactory.py
new file mode 100644
index 0000000000..9bdf2753d3
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/subscriberfactory.py
@@ -0,0 +1,85 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+from resources.lib import taskdict
+from resources.lib.pubsub import TaskManager, Subscriber, TaskReturn
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.utils.poutil import KodiPo
+kl = KodiLogger()
+log = kl.log
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+def returnHandler(taskReturn):
+ assert isinstance(taskReturn, TaskReturn)
+ if taskReturn.iserror is False:
+ msg = _('Command for Task %s, Event %s completed succesfully!') % (taskReturn.taskId, taskReturn.eventId)
+ if taskReturn.msg.strip() != '':
+ msg += _('\nThe following message was returned: %s') % taskReturn.msg
+ log(msg=msg)
+ else:
+ msg = _('ERROR encountered for Task %s, Event %s\nERROR mesage: %s') % (
+ taskReturn.taskId, taskReturn.eventId, taskReturn.msg)
+ log(loglevel=kl.LOGERROR, msg=msg)
+
+
+class SubscriberFactory(object):
+
+ def __init__(self, settings, logger):
+ self.settings = settings
+ self.topics = []
+ self.logger = logger
+
+ def createSubscribers(self, retHandler=returnHandler):
+ subscribers = []
+ for key in self.settings.events.keys():
+ subscriber = self.createSubscriber(key, retHandler)
+ if subscriber is not None:
+ subscribers.append(subscriber)
+ return subscribers
+
+ def createSubscriber(self, eventkey, retHandler=returnHandler):
+ task_key = self.settings.events[eventkey]['task']
+ evtsettings = self.settings.events[eventkey]
+ topic = self.settings.topicFromSettingsEvent(eventkey)
+ self.topics.append(topic.topic)
+ task = self.createTask(task_key)
+ if task is not None:
+ tm = TaskManager(task, taskid=evtsettings['task'], userargs=evtsettings['userargs'],
+ **self.settings.tasks[task_key])
+ tm.returnHandler = retHandler
+ tm.taskKwargs['notify'] = self.settings.general['Notify']
+ subscriber = Subscriber(logger=self.logger)
+ subscriber.addTaskManager(tm)
+ subscriber.addTopic(topic)
+ self.logger.log(msg=_('Subscriber for event: %s, task: %s created') % (str(topic), task_key))
+ return subscriber
+ else:
+ self.logger.log(loglevel=self.logger.LOGERROR,
+ msg=_('Subscriber for event: %s, task: %s NOT created due to errors') % (str(topic), task_key))
+ return None
+
+ def createTask(self, taskkey):
+ tasksettings = self.settings.tasks[taskkey]
+ mytask = taskdict[tasksettings['type']]['class']
+ if mytask.validate(tasksettings, xlog=self.logger.log) is True:
+ return mytask
+ else:
+ return None
+
diff --git a/script.service.kodi.callbacks/resources/lib/taskABC.py b/script.service.kodi.callbacks/resources/lib/taskABC.py
new file mode 100644
index 0000000000..5e2ea0d4c2
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/taskABC.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import threading
+import Queue
+import abc
+import re
+import copy
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.pubsub import TaskReturn
+from resources.lib.events import Events
+import xbmcgui
+
+events = Events()
+
+def notify(msg):
+ dialog = xbmcgui.Dialog()
+ dialog.notification('Kodi Callbacks', msg, xbmcgui.NOTIFICATION_INFO, 5000)
+
+
+class AbstractTask(threading.Thread):
+ """
+ Abstract class for command specific workers to follow
+ """
+ __metaclass__ = abc.ABCMeta
+ tasktype = 'abstract'
+ lock = threading.RLock()
+
+ def __init__(self, logger=KodiLogger.log):
+ super(AbstractTask, self).__init__(name='Worker')
+ self.cmd_str = ''
+ self.userargs = ''
+ self.log = logger
+ self.runtimeargs = []
+ self.taskKwargs = {}
+ self.publisherKwargs = {}
+ self.topic = None
+ self.type = ''
+ self.taskId = ''
+ self.returnQ = Queue.Queue()
+ self.delimitregex = re.compile(r'\s+,\s+|,\s+|\s+')
+
+
+ def processUserargs(self, kwargs):
+ if self.userargs == '':
+ return []
+ ret = copy.copy(self.userargs)
+ ret = ret.replace(r'%%', '{@literal%@}')
+ if self.tasktype == 'script' or self.tasktype == 'python':
+ tmp = self.delimitregex.sub(r'{@originaldelim@}', ret)
+ ret = tmp
+ try:
+ varArgs = events.AllEvents[self.topic.topic]['varArgs']
+ except KeyError:
+ pass
+ else:
+ for key in varArgs.keys():
+ try:
+ kw = str(kwargs[varArgs[key]])
+ kw = kw.replace(" ", '%__')
+ ret = ret.replace(key, kw)
+ except KeyError:
+ pass
+ ret = ret.replace('%__', " ")
+ ret = ret.replace('%_', ",")
+ ret = ret.replace('{@literal%@}', r'%')
+ if self.tasktype == 'script' or self.tasktype == 'python':
+ ret = ret.split('{@originaldelim@}')
+ return ret
+ else:
+ return ret
+
+ @staticmethod
+ @abc.abstractmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ pass
+
+ def t_start(self, topic, taskKwargs, **kwargs):
+ with AbstractTask.lock:
+ self.topic = topic
+ self.taskKwargs = taskKwargs
+ self.userargs = taskKwargs['userargs']
+ self.taskId = taskKwargs['taskid']
+ self.publisherKwargs = kwargs
+ self.runtimeargs = self.processUserargs(kwargs)
+ self.start()
+
+ @abc.abstractmethod
+ def run(self):
+ err = None # True if error occured
+ msg = '' # string containing error message or return message
+ self.threadReturn(err, msg)
+
+ def threadReturn(self, err, msg):
+ taskReturn = TaskReturn(err, msg)
+ taskReturn.eventId = str(self.topic)
+ taskReturn.taskId = self.taskId
+ self.returnQ.put(taskReturn)
diff --git a/script.service.kodi.callbacks/resources/lib/taskExample.py b/script.service.kodi.callbacks/resources/lib/taskExample.py
new file mode 100644
index 0000000000..4d91396e1c
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/taskExample.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+import sys
+import traceback
+from resources.lib.taskABC import AbstractTask, KodiLogger, notify
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+class TaskCustom(AbstractTask):
+ """
+ Your task MUST subclass AbstractTask. If it doesn't, it will not be accessible.
+ The following two parameters are REQUIRED with the same structure as shown.
+ 'Varaibles' will be prompted for from the Settings screen.
+ If you have no variables, then you probably do not need to be writing a custom task.
+ Shorten or extend the list as needed for more or less variables.
+ This allows for the task to be properly 'discovered' and also will allow users to imput required information.
+ See http://kodi.wiki/view/Add-on_settings for details about the settings variables.
+ """
+ tasktype = 'mycustomtasktype'
+ variables = [
+ {
+ 'id':'mycustomvariable1',
+ 'settings':{
+ 'default':'',
+ 'label':__('My Custom Task Variable 1', update=True), # Surround label for settings with localization function
+ 'type':'text'
+ }
+ },
+ {
+ 'id':'mycustomvariable2',
+ 'settings':{
+ 'default':'false',
+ 'label':__('My Custom Task Variable 2', update=True), # Surround label for settings with localization function
+ 'type':'bool'
+ }
+ },
+ ]
+
+ def __init__(self):
+ """Do not request any other variables via __init__. Nothing else will be provided and an exception will be raised.
+ The following super call is REQUIRED."""
+ super(TaskCustom, self).__init__()
+ # Put anything else here you may need, but note that validate is a staticmethod and cannot access 'self'.
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ """
+ :param taskKwargs:
+ :param xlog: class or method that accepts msg=str and loglevel=int see below
+ :type:
+ :return: whether validate
+ :rtype: bool
+ Place any code here to validate the users input from the settings page.
+ Referring to the above taskKwargs will be a dictionary objected keyed for your variable ids.
+ For example: {'mycustomvariable1':'userinput', 'mycustomvariable2':False}
+ The appropraite logger will be passed in. During testing, log output is captured and then displayed
+ on the screen. Usage:
+ xlog(msg='My message')
+ Return True if validating passed. False if it didn't. If there is nothing to validate, just return True.
+
+ ** If you generate any log messages here, surround them with the _( , update=True) localization function.
+ This will cause the po file to be updated with your strings.
+ See below. During direct testing from the settings page, these log messages will be displayed on the screen
+ """
+ xlog(msg=_('My Custom Task passed validation', update=True))
+
+ return True
+
+ def run(self):
+ """
+ The following templated code is recommended. As above, self.taskKwargs contains your user input variables.
+ There are a few other things added to taskKwargs (such as 'notify' seen below). If you have access to a debugger,
+ stop the code here and look at that variable. Or try logging it as a string, if interested.
+ self.runtimeargs contains the variable substituted event string.
+ Information is passed out via the err and msg variables.
+
+ ** If you generate any log messages here, surround them with the _( , update=True) localization function.
+ This will cause the po file to be updated with your strings.
+ See below. During direct testing from the settings page, these log messages will be displayed on the screen
+ """
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ err = False # Change this to True if an error is encountered
+ msg = '' # Accumulate error or non-error information that needs to be returned in this string
+ args = self.runtimeargs # This contains a list derived from the variable subsituted event string
+ # This list format is needed for using python's subprocess and Popen calls so that's why it is formatted
+ # in this fashion.
+ # If you need the args in a different format consider rejoining it into a string such as ' '.join(args) or
+ # ', '.join(args). If you need something very different, you will need to override self.processUserargs()
+ # See taskABC for the default processing and taskHttp for an example of overriding.
+ assert isinstance(args, list)
+ try:
+ pass
+ # Put your task implementation code here. Consider an inner try/except block accumulating specific error info
+ # by setting err=True and appending to the message.
+ except Exception:
+ # Non-specific error catching and processing.
+ e = sys.exc_info()[0]
+ err = True
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ # The following needs to be the last call. Since this code is running in its own thread, to pass information
+ # backout, the following is formatted and placed in an output Queue where the parent thread is waiting.
+ # If you do not pass anything out, a memory leak will accumulate with 'TaskManagers' accumulating over time.
+ self.threadReturn(err, msg)
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/__init__.py b/script.service.kodi.callbacks/resources/lib/tasks/__init__.py
new file mode 100644
index 0000000000..9789d2f536
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/__init__.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/taskBuiltin.py b/script.service.kodi.callbacks/resources/lib/tasks/taskBuiltin.py
new file mode 100644
index 0000000000..5fd622cb34
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/taskBuiltin.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import sys
+import traceback
+import xbmc
+from resources.lib.taskABC import AbstractTask, KodiLogger, notify
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+class TaskBuiltin(AbstractTask):
+ tasktype = 'builtin'
+ variables = [
+ {
+ 'id':u'builtin',
+ 'settings':{
+ 'default':u'',
+ 'label':u'Kodi Builtin Function',
+ 'type':'text'
+ }
+ },
+ ]
+
+ def __init__(self):
+ super(TaskBuiltin, self).__init__()
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ return True
+
+ def run(self):
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ err = False
+ msg = ''
+ args = ' %s' % ' '.join(self.runtimeargs)
+ # noinspection PyBroadException,PyBroadException,PyBroadException
+ try:
+ if len(self.runtimeargs) > 0:
+ result = xbmc.executebuiltin('%s, %s' % (self.taskKwargs['builtin'], args))
+ else:
+ result = xbmc.executebuiltin('%s' % self.taskKwargs['builtin'])
+ if result is not None:
+ msg = result
+ if result != '':
+ err = True
+ except Exception:
+ e = sys.exc_info()[0]
+ err = True
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ self.threadReturn(err, msg)
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/taskHttp.py b/script.service.kodi.callbacks/resources/lib/tasks/taskHttp.py
new file mode 100644
index 0000000000..bf42ca1bf0
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/taskHttp.py
@@ -0,0 +1,184 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import sys
+import traceback
+import requests
+import urllib2
+import httplib
+from urlparse import urlparse
+import socket
+from resources.lib.taskABC import AbstractTask, KodiLogger, notify
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+class TaskHttp(AbstractTask):
+ tasktype = 'http'
+ variables = [
+ {
+ 'id':u'http',
+ 'settings':{
+ 'default':u'',
+ 'label':u'HTTP string (without parameters)',
+ 'type':'text'
+ }
+ },
+ {
+ 'id':u'user',
+ 'settings':{
+ 'default':u'',
+ 'label':u'user for Basic Auth (optional)',
+ 'type':'text'
+ }
+ },
+ {
+ 'id':u'pass',
+ 'settings':{
+ 'default':u'',
+ 'label':u'password for Basic Auth (optional)',
+ 'type':'text',
+ 'option':u'hidden'
+ }
+ },
+ {
+ 'id': u'request-type',
+ 'settings': {
+ 'default': u'GET',
+ 'label': u'Request Type',
+ 'type': u'labelenum',
+ 'values': [u'GET', u'POST', u'POST-GET', u'PUT', u'DELETE', u'HEAD', u'OPTIONS']
+ }
+ },
+ {
+ 'id': u'content-type',
+ 'settings': {
+ 'default': u'application/json',
+ 'label': u'Content-Type (for POST or PUT only)',
+ 'type': u'labelenum',
+ 'values': [u'application/json', u'application/x-www-form-urlencoded', u'text/html', u'text/plain']
+ }
+ }
+ ]
+
+ def __init__(self):
+ super(TaskHttp, self).__init__()
+ self.runtimeargs = ''
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ o = urlparse(taskKwargs['http'])
+ if o.scheme != '' and o.netloc != '':
+ return True
+ else:
+ xlog(msg=_('Invalid url: %s') % taskKwargs['http'])
+ return False
+
+ def sendRequest(self, session, verb, url, postget=False):
+ if postget or verb == 'POST' or verb == 'PUT':
+ url, data = url.split('??', 1)
+ if postget:
+ data = None
+ else:
+ data = None
+ req = requests.Request(verb, url, data=data)
+ prepped = session.prepare_request(req)
+ if verb == 'POST' or verb == 'PUT':
+ prepped.headers['Content-Type'] = self.taskKwargs['content-type']
+ msg = 'Prepped URL: %s\nBody: %s' % (prepped.url, prepped.body)
+ try:
+ resp = session.send(prepped, timeout=20)
+ msg += '\nStatus: %s' % str(resp.status_code)
+ resp.raise_for_status()
+ err = False
+ if resp.text == '':
+ respmsg = 'No response received'
+ else:
+ respmsg = resp.text
+ msg += '\nResponse for %s: %s' %(verb, respmsg)
+ resp.close()
+ except requests.ConnectionError as e:
+ err = True
+ msg = _('Requests Connection Error')
+ except requests.HTTPError as e:
+ err = True
+ msg = '%s: %s' %(_('Requests HTTPError'), str(e))
+ except requests.URLRequired as e:
+ err = True
+ msg = '%s: %s' %(_('Requests URLRequired Error'), str(e))
+ except requests.Timeout as e:
+ err = True
+ msg = '%s: %s' %(_('Requests Timeout Error'), str(e))
+ except requests.RequestException as e:
+ err = True
+ msg = '%s: %s' %(_('Generic Requests Error'), str(e))
+ except urllib2.HTTPError, e:
+ err = True
+ msg = _('HTTPError = ') + str(e.code)
+ except urllib2.URLError, e:
+ err = True
+ msg = _('URLError\n') + e.reason
+ except httplib.BadStatusLine:
+ err = False
+ self.log(msg=_('Http Bad Status Line caught and passed'))
+ except httplib.HTTPException, e:
+ err = True
+ msg = _('HTTPException')
+ if hasattr(e, 'message'):
+ msg = msg + '\n' + e.message
+ except socket.timeout:
+ err = True
+ msg = _('The request timed out, host unreachable')
+ except Exception:
+ err = True
+ e = sys.exc_info()[0]
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ return err, msg
+
+
+ def run(self):
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ if isinstance(self.runtimeargs, list):
+ if len(self.runtimeargs) > 0:
+ self.runtimeargs = ''.join(self.runtimeargs)
+ else:
+ self.runtimeargs = ''
+ s = requests.Session()
+ url = self.taskKwargs['http']+self.runtimeargs
+ if self.taskKwargs['user'] != '' and self.taskKwargs['pass'] != '':
+ s.auth = (self.taskKwargs['user'], self.taskKwargs['pass'])
+ if self.taskKwargs['request-type'] == 'POST-GET':
+ verb = 'POST'
+ else:
+ verb = self.taskKwargs['request-type']
+
+ err, msg = self.sendRequest(s, verb, url)
+
+ if self.taskKwargs['request-type'] == 'POST-GET':
+ err2, msg2 = self.sendRequest(s, 'GET', url, postget=True)
+ err = err or err2
+ msg = '\n'.join([msg, msg2])
+
+ s.close()
+ self.threadReturn(err, msg)
+
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/taskJson.py b/script.service.kodi.callbacks/resources/lib/tasks/taskJson.py
new file mode 100644
index 0000000000..c5223d1493
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/taskJson.py
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import sys
+import traceback
+import xbmc
+import json
+from resources.lib.taskABC import AbstractTask, KodiLogger, notify
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+class TaskJsonNotify(AbstractTask):
+ tasktype = 'json_rpc_notify'
+ variables = [
+ {
+ 'id':u'jsonnotify',
+ 'settings':{
+ 'default':u'kodi.callbacks',
+ 'label':u'Sender string',
+ 'type':'text'
+ }
+ },
+ ]
+
+ def __init__(self):
+ super(TaskJsonNotify, self).__init__()
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ return True
+
+ def run(self):
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ err = False
+ msg = ''
+ message = str(self.topic)
+ data = str(self.publisherKwargs).replace('\'', '"')
+ try:
+ json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "JSONRPC.NotifyAll", "params": {"sender":"%s", "message":"%s", "data":%s} }' %(self.taskKwargs['jsonnotify'], message, data))
+ json_query = unicode(json_query, 'utf-8', errors='ignore')
+ json_response = json.loads(json_query)
+ except Exception:
+ e = sys.exc_info()[0]
+ err = True
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ else:
+ if json_response.has_key('result'):
+ if json_response['result'] != u'OK':
+ err = True
+ msg = 'JSON Notify Error %s' % json_response['result']
+ else:
+ err = True
+ msg = 'JSON Notify Error: %s' % str(json_response)
+
+ self.threadReturn(err, msg)
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/taskPython.py b/script.service.kodi.callbacks/resources/lib/tasks/taskPython.py
new file mode 100644
index 0000000000..56fec1be95
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/taskPython.py
@@ -0,0 +1,123 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import sys
+import os
+import traceback
+import xbmc
+import xbmcvfs
+from resources.lib.taskABC import AbstractTask, KodiLogger, notify
+from resources.lib.utils.poutil import KodiPo
+from resources.lib.utils.kodipathtools import translatepath
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+
+class TaskPython(AbstractTask):
+ tasktype = 'python'
+ variables = [
+ {
+ 'id':u'pythonfile',
+ 'settings':{
+ 'default':u'',
+ 'label':u'Python file',
+ 'type':'file'
+ }
+ },
+ {
+ 'id':u'import',
+ 'settings':{
+ 'default':u'false',
+ 'label':u'Import and call run() (default=no)?',
+ 'type':'bool'
+ }
+ }
+ ]
+
+ def __init__(self):
+ super(TaskPython, self).__init__()
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+ tmp = xbmc.translatePath(taskKwargs['pythonfile'])
+ if xbmcvfs.exists(tmp):
+ ext = os.path.splitext(tmp)[1]
+ if ext == '.py':
+ return True
+ else:
+ xlog(msg=_('Error - not a python script: %s') % tmp)
+ return False
+ else:
+ xlog(msg=_('Error - File not found: %s') % tmp)
+ return False
+
+ def run(self):
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ err = False
+ msg = ''
+ args = self.runtimeargs
+ try:
+ useImport = self.taskKwargs['import']
+ except KeyError:
+ useImport = False
+ fn = translatepath(self.taskKwargs['pythonfile'])
+ try:
+ if len(self.runtimeargs) > 0:
+ if useImport is False:
+ args = ' %s' % ' '.join(args)
+ result = xbmc.executebuiltin('XBMC.RunScript(%s, %s)' % (fn, args))
+ else:
+ directory, module_name = os.path.split(fn)
+ module_name = os.path.splitext(module_name)[0]
+
+ path = list(sys.path)
+ sys.path.insert(0, directory)
+ try:
+ module = __import__(module_name)
+ result = module.run(args)
+ finally:
+ sys.path[:] = path
+ else:
+ if useImport is False:
+ result = xbmc.executebuiltin('XBMC.RunScript(%s)' % fn)
+ else:
+ directory, module_name = os.path.split(fn)
+ module_name = os.path.splitext(module_name)[0]
+
+ path = list(sys.path)
+ sys.path.insert(0, directory)
+ try:
+ module = __import__(module_name)
+ result = module.run(args)
+ finally:
+ sys.path[:] = path
+ if result is not None:
+ msg = result
+ if result != '':
+ err = True
+ except Exception:
+ e = sys.exc_info()[0]
+ err = True
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+
+ self.threadReturn(err, msg)
diff --git a/script.service.kodi.callbacks/resources/lib/tasks/taskScript.py b/script.service.kodi.callbacks/resources/lib/tasks/taskScript.py
new file mode 100644
index 0000000000..f1c4cc036d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tasks/taskScript.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+import sys
+import os
+import stat
+import subprocess
+import traceback
+from resources.lib.taskABC import AbstractTask, notify, KodiLogger
+from resources.lib.utils.detectPath import process_cmdline
+import xbmc
+import xbmcvfs
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
+
+sysplat = sys.platform
+isAndroid = 'XBMC_ANDROID_SYSTEM_LIBS' in os.environ.keys()
+
+class TaskScript(AbstractTask):
+ tasktype = 'script'
+ variables = [
+ {
+ 'id':u'scriptfile',
+ 'settings':{
+ 'default':u'',
+ 'label':u'Script executable file',
+ 'type':'sfile'
+ }
+ },
+ {
+ 'id':u'use_shell',
+ 'settings':{
+ 'default':u'false',
+ 'label':u'Requires shell?',
+ 'type':'bool'
+ }
+ },
+ {
+ 'id':u'waitForCompletion',
+ 'settings':{
+ 'default':u'true',
+ 'label':u'Wait for script to complete?',
+ 'type':'bool'
+ }
+ }
+ ]
+
+
+ def __init__(self):
+ super(TaskScript, self).__init__()
+
+ @staticmethod
+ def validate(taskKwargs, xlog=KodiLogger.log):
+
+ tmpl = process_cmdline(taskKwargs['scriptfile'])
+ found = False
+ for tmp in tmpl:
+ tmp = xbmc.translatePath(tmp)
+ if xbmcvfs.exists(tmp) or os.path.exists(tmp) and found is False:
+ try:
+ mode = os.stat(tmp).st_mode
+ mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
+ os.chmod(tmp, mode)
+ except OSError:
+ if sysplat.startswith('win') is False:
+ xlog(msg=_('Failed to set execute bit on script: %s') % tmp)
+ finally:
+ found = True
+ return True
+
+ def run(self):
+ msg = ''
+ if self.taskKwargs['notify'] is True:
+ notify(_('Task %s launching for event: %s') % (self.taskId, str(self.topic)))
+ try:
+ needs_shell = self.taskKwargs['use_shell']
+ except KeyError:
+ needs_shell = False
+ try:
+ wait = self.taskKwargs['waitForCompletion']
+ except KeyError:
+ wait = True
+ tmpl = process_cmdline(self.taskKwargs['scriptfile'])
+ filefound = False
+ basedir = None
+ sysexecutable = None
+ for i, tmp in enumerate(tmpl):
+ tmp = xbmc.translatePath(tmp)
+ if os.path.exists(tmp) and filefound is False:
+ basedir, fn = os.path.split(tmp)
+ basedir = os.path.realpath(basedir)
+ tmpl[i] = fn
+ filefound = True
+ if i == 0:
+ if os.path.splitext(fn)[1] == u'.sh':
+ if isAndroid:
+ sysexecutable = '/system/bin/sh'
+ elif not sysplat.startswith('win'):
+ sysexecutable = '/bin/bash'
+ else:
+ tmpl[i] = tmp
+ if sysexecutable == '/system/bin/sh':
+ tmpl.insert(0, 'sh')
+ elif sysexecutable == '/bin/bash':
+ tmpl.insert(0, 'bash')
+
+ cwd = os.getcwd()
+ args = tmpl + self.runtimeargs
+ if needs_shell:
+ args = ' '.join(args)
+ err = False
+ msg += 'taskScript ARGS = %s\n SYSEXEC = %s\n BASEDIR = %s\n' % (args, sysexecutable, basedir)
+ sys.exc_clear()
+ try:
+ if basedir is not None:
+ os.chdir(basedir)
+ if sysexecutable is not None:
+ if isAndroid or sysplat.startswith('darwin'):
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=needs_shell, stderr=subprocess.STDOUT, executable=sysexecutable)
+ else:
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=needs_shell, stderr=subprocess.STDOUT, executable=sysexecutable, cwd=basedir)
+ else:
+ if isAndroid or sysplat.startswith('darwin'):
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=needs_shell, stderr=subprocess.STDOUT)
+ else:
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=needs_shell, stderr=subprocess.STDOUT, cwd=basedir)
+ if wait:
+ stdoutdata, stderrdata = p.communicate()
+ if stdoutdata is not None:
+ stdoutdata = str(stdoutdata).strip()
+ if stdoutdata != '':
+ msg += _('Process returned data: [%s]\n') % stdoutdata
+ else:
+ msg += _('Process returned no data\n')
+ else:
+ msg += _('Process returned no data\n')
+ if stderrdata is not None:
+ stderrdata = str(stderrdata).strip()
+ if stderrdata != '':
+ msg += _('Process returned error: %s') % stdoutdata
+ except ValueError, e:
+ err = True
+ msg = str(e)
+ except subprocess.CalledProcessError, e:
+ err = True
+ msg = e.output
+ except Exception:
+ e = sys.exc_info()[0]
+ err = True
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ finally:
+ os.chdir(cwd)
+ self.threadReturn(err, msg)
+
diff --git a/script.service.kodi.callbacks/resources/lib/tests/__init__.py b/script.service.kodi.callbacks/resources/lib/tests/__init__.py
new file mode 100644
index 0000000000..25e9a531ca
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
diff --git a/script.service.kodi.callbacks/resources/lib/tests/direct_test.py b/script.service.kodi.callbacks/resources/lib/tests/direct_test.py
new file mode 100644
index 0000000000..9db35c340d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/direct_test.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 KenV99
+#
+# 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 .
+#
+
+from resources.lib.pubsub import TaskReturn
+from resources.lib.utils.poutil import KodiPo
+from resources.lib.kodilogging import KodiLogger
+kl = KodiLogger()
+log = kl.log
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+def testMsg(taskManager, taskSettings, kwargs):
+ msg = [_('Testing for task type: %s') % taskSettings['type'], _('Settings: %s') % str(taskManager.taskKwargs),
+ _('Runtime kwargs: %s') % str(kwargs)]
+ return msg
+
+class TestHandler(object):
+ testMessage = []
+
+ def __init__(self, testMessage):
+ TestHandler.testMessage = testMessage
+
+ @staticmethod
+ def testReturnHandler(taskReturn):
+ assert isinstance(taskReturn, TaskReturn)
+ from resources.lib.dialogtb import show_textbox
+ if taskReturn.iserror is False:
+ TestHandler.testMessage.append(_('Command for Task %s, Event %s completed succesfully!') % (taskReturn.taskId, taskReturn.eventId))
+ if taskReturn.msg != '':
+ TestHandler.testMessage.append(_('The following message was returned: %s') % taskReturn.msg)
+ log(msg='\n'.join(TestHandler.testMessage), loglevel=kl.LOGNOTICE)
+ else:
+ TestHandler.testMessage.append(_('ERROR encountered for Task %s, Event %s\nERROR mesage: %s') % (taskReturn.taskId, taskReturn.eventId, taskReturn.msg))
+ log(msg='\n'.join(TestHandler.testMessage), loglevel=kl.LOGERROR)
+ show_textbox('Test Results', TestHandler.testMessage)
+
+class TestLogger(object):
+ def __init__(self):
+ self._log = []
+
+ def log(self, level=1, msg=''):
+ assert isinstance(level, int)
+ msg_list = msg.split('\n')
+ if not isinstance(msg_list, list):
+ msg_list = [msg]
+ self._log.extend(msg_list)
+
+ def retrieveLogAsList(self):
+ return self._log
diff --git a/script.service.kodi.callbacks/resources/lib/tests/testPublishers.py b/script.service.kodi.callbacks/resources/lib/tests/testPublishers.py
new file mode 100644
index 0000000000..80ce1a2876
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/testPublishers.py
@@ -0,0 +1,602 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+# MEANT TO BE RUN USING NOSE
+
+import xbmc
+import os
+import sys
+from resources.lib.publishers.log import LogPublisher
+import resources.lib.publishers.loop as loop
+import resources.lib.publishers.log as log
+from resources.lib.publishers.loop import LoopPublisher
+from resources.lib.publishers.watchdog import WatchdogPublisher
+from resources.lib.publishers.watchdogStartup import WatchdogStartup
+from resources.lib.publishers.schedule import SchedulePublisher
+from resources.lib.pubsub import Dispatcher, Subscriber, Message, Topic
+from resources.lib.settings import Settings
+from resources.lib.utils.kodipathtools import translatepath, setPathRW
+from flexmock import flexmock
+import Queue
+import threading
+import time
+import nose
+
+
+# from nose.plugins.skip import SkipTest
+
+def printlog(msg,level=0):
+ print msg, level
+
+
+flexmock(xbmc, log=printlog)
+
+
+def sleep(xtime):
+ time.sleep(xtime / 1000.0)
+
+
+class MockSubscriber(Subscriber):
+ def __init__(self):
+ super(MockSubscriber, self).__init__()
+ self.testq = Queue.Queue()
+
+ def notify(self, message):
+ self.testq.put(message)
+
+ def retrieveMessages(self):
+ messages = []
+ while not self.testq.empty():
+ try:
+ message = self.testq.get(timeout=1)
+ except Queue.Empty:
+ pass
+ else:
+ messages.append(message)
+ return messages
+
+ def waitForMessage(self, count=1, timeout=30):
+ loopcount = 0
+ while loopcount < timeout:
+ msgcount = self.testq.qsize()
+ if msgcount >= count:
+ return
+ else:
+ time.sleep(1)
+ loopcount += 1
+
+
+# @SkipTest
+class testWatchdogStartup(object):
+ def __init__(self):
+ self.publisher = None
+ self.dispatcher = None
+ self.subscriber = None
+ self.topic = None
+ self.folder = None
+ self.saveduserpickle = None
+
+ def setup(self):
+ self.folder = translatepath('special://addon/resources/lib/tests')
+ watchdogStartupSettings = [
+ {'ws_folder': self.folder, 'ws_patterns': '*', 'ws_ignore_patterns': '', 'ws_ignore_directories': True,
+ 'ws_recursive': False, 'key': 'E1'}]
+ self.saveduserpickle = WatchdogStartup.getPickle()
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+ self.topic = Topic('onStartupFileChanges', 'E1')
+ self.subscriber.addTopic(self.topic)
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, getWatchdogStartupSettings=watchdogStartupSettings)
+ self.publisher = WatchdogStartup(self.dispatcher, settings)
+ self.dispatcher.start()
+
+ def teardown(self):
+ WatchdogStartup.savePickle(self.saveduserpickle)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ del self.publisher
+ del self.dispatcher
+
+ def testWatchdogPublisherCreate(self):
+ fn = os.path.join(self.folder, 'test.txt')
+ if os.path.exists(fn):
+ os.remove(fn)
+ self.publisher.start()
+ self.publisher.abort()
+ time.sleep(1)
+ self.subscriber.testq = Queue.Queue()
+ with open(fn, 'w') as f:
+ f.writelines('test')
+ time.sleep(1)
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=2, timeout=2)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ os.remove(fn)
+ messages = self.subscriber.retrieveMessages()
+ found = False
+ for message in messages:
+ assert isinstance(message, Message)
+ assert 'listOfChanges' in message.kwargs.keys()
+ tmp = message.kwargs['listOfChanges']
+ if 'FilesCreated' in tmp.keys() and [fn] in tmp.values():
+ found = True
+ assert message.topic == self.topic
+ assert found == True
+ if len(messages) > 1:
+ raise AssertionError('Warning: Too many messages found for Watchdog Startup Create')
+
+
+# @SkipTest
+class testWatchdog(object):
+ def __init__(self):
+ self.publisher = None
+ self.dispatcher = None
+ self.subscriber = None
+ self.topic = None
+ self.folder = None
+
+ def setup(self):
+ self.folder = translatepath('special://addon/resources/lib/tests')
+ setPathRW(self.folder)
+ watchdogSettings = [{'folder': self.folder, 'patterns': '*', 'ignore_patterns': '', 'ignore_directories': True,
+ 'recursive': False, 'key': 'E1'}]
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+ self.topic = Topic('onFileSystemChange', 'E1')
+ self.subscriber.addTopic(self.topic)
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, getWatchdogSettings=watchdogSettings)
+ self.publisher = WatchdogPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+
+ def teardown(self):
+ self.publisher.abort()
+ self.dispatcher.abort()
+ del self.publisher
+ del self.dispatcher
+
+ def testWatchdogPublisherCreate(self):
+ fn = os.path.join(self.folder, 'test.txt')
+ if os.path.exists(fn):
+ os.remove(fn)
+ time.sleep(1)
+ self.publisher.start()
+ time.sleep(1)
+ with open(fn, 'w') as f:
+ f.writelines('test')
+ self.subscriber.waitForMessage(count=2, timeout=2)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ time.sleep(1)
+ os.remove(fn)
+ messages = self.subscriber.retrieveMessages()
+ foundc = False
+ foundm = False
+ fmesgc = None
+ fmesgm = None
+ for message in messages:
+ assert isinstance(message, Message)
+ if message.kwargs['event'] == 'created':
+ foundc = True
+ fmesgc = message
+ elif message.kwargs['event'] == 'modified':
+ foundm = True
+ fmesgm = message
+ assert foundc is True
+ assert fmesgc.topic == self.topic
+ assert fmesgc.kwargs['path'] == fn
+ assert foundm is True
+ assert fmesgm.topic == self.topic, 'Warning Only'
+ assert fmesgm.kwargs['path'] == fn, 'Warning Only'
+ if len(messages) > 2:
+ raise AssertionError('Warning: Too many messages found for Watchdog Create')
+
+ def testWatchdogPublisherDelete(self):
+ fn = os.path.join(self.folder, 'test.txt')
+ if os.path.exists(fn) is False:
+ with open(fn, 'w') as f:
+ f.writelines('test')
+ time.sleep(1)
+ self.publisher.start()
+ time.sleep(2)
+ os.remove(fn)
+ self.subscriber.waitForMessage(count=1, timeout=2)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ found = False
+ fmesg = None
+ for message in messages:
+ assert isinstance(message, Message)
+ if message.kwargs['event'] == 'deleted':
+ found = True
+ fmesg = message
+ assert found is True
+ assert fmesg.topic == self.topic
+ assert fmesg.kwargs['path'] == fn
+ if len(messages) > 1:
+ raise AssertionError('Warning: Too many messages found for Watchdog Delete')
+
+ def testWatchdogPublisherModify(self):
+ fn = os.path.join(self.folder, 'test.txt')
+ if os.path.exists(fn) is False:
+ with open(fn, 'w') as f:
+ f.writelines('test')
+ time.sleep(1)
+ self.publisher.start()
+ time.sleep(1)
+ with open(fn, 'a') as f:
+ f.writelines('test2')
+ self.subscriber.waitForMessage(count=1, timeout=2)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ os.remove(fn)
+ messages = self.subscriber.retrieveMessages()
+ found = False
+ fmesg = None
+ for message in messages:
+ assert isinstance(message, Message)
+ if message.kwargs['event'] == 'modified':
+ found = True
+ fmesg = message
+ assert found is True
+ assert fmesg.topic == self.topic
+ assert fmesg.kwargs['path'] == fn
+ if len(messages) > 1:
+ raise AssertionError('Warning: Too many messages found for Watchdog Modify')
+
+
+# @SkipTest
+class testLoop(object):
+ def __init__(self):
+ self.publisher = None
+ self.dispatcher = None
+ self.subscriber = None
+ self.globalidletime = None
+ self.starttime = None
+ self.topics = None
+
+ def getGlobalIdleTime(self):
+ if self.globalidletime is None:
+ self.starttime = time.time()
+ self.globalidletime = 0
+ return 0
+ else:
+ self.globalidletime = int(time.time() - self.starttime)
+ return self.globalidletime
+
+ def getStereoMode(self):
+ if self.getGlobalIdleTime() < 2:
+ return 'off'
+ else:
+ return 'split_vertical'
+
+ def getCurrentWindowId(self):
+ git = self.getGlobalIdleTime()
+ if git < 2:
+ return 10000
+ elif 2 <= git < 4:
+ return 10001
+ else:
+ return 10002
+
+ def getProfileString(self):
+ if self.getGlobalIdleTime() < 2:
+ return 'Bob'
+ else:
+ return 'Mary'
+
+ def setup(self):
+ flexmock(loop.xbmc, getGlobalIdleTime=self.getGlobalIdleTime)
+ flexmock(loop.xbmc, sleep=sleep)
+ flexmock(loop, getStereoscopicMode=self.getStereoMode)
+ flexmock(loop, getProfileString=self.getProfileString)
+ flexmock(loop.xbmc.Player, isPlaying=False)
+ flexmock(loop.xbmcgui, getCurrentWindowId=self.getCurrentWindowId)
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+
+ def teardown(self):
+ self.publisher.abort()
+ self.dispatcher.abort()
+ del self.publisher
+ del self.dispatcher
+
+ def testLoopIdle(self):
+ self.topics = [Topic('onIdle', 'E1'), Topic('onIdle', 'E2')]
+ for topic in self.topics:
+ self.subscriber.addTopic(topic)
+ self.dispatcher.addSubscriber(self.subscriber)
+ idleSettings = {'E1': 3, 'E2': 5}
+ settings = Settings()
+ flexmock(settings, getIdleTimes=idleSettings)
+ flexmock(settings, general={'LoopFreq': 100})
+ self.publisher = LoopPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=2, timeout=7)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testStereoModeChange(self):
+ self.topics = [Topic('onStereoModeChange')]
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, general={'LoopFreq': 100})
+ self.publisher = LoopPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=5)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testOnWindowOpen(self):
+ self.topics = [Topic('onWindowOpen', 'E1')]
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, general={'LoopFreq': 100})
+ flexmock(settings, getOpenwindowids={10001: 'E1'})
+ self.publisher = LoopPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=5)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testOnWindowClose(self):
+ self.topics = [Topic('onWindowClose', 'E1')]
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, general={'LoopFreq': 100})
+ flexmock(settings, getClosewindowids={10001: 'E1'})
+ self.publisher = LoopPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=5)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testProfileChange(self):
+ self.topics = [Topic('onProfileChange')]
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ settings = Settings()
+ flexmock(settings, general={'LoopFreq': 100})
+ self.publisher = LoopPublisher(self.dispatcher, settings)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=5)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+
+# @SkipTest
+class testLog(object):
+ path = translatepath('special://addondata')
+ if not os.path.exists(path):
+ os.mkdir(path)
+ setPathRW(path)
+ fn = translatepath('special://addondata/kodi.log')
+
+ def __init__(self):
+ self.publisher = None
+ self.dispatcher = None
+ self.subscriber = None
+ self.globalidletime = None
+ self.starttime = None
+ self.topics = None
+
+ @staticmethod
+ def logSimulate():
+ import random, string
+ randomstring = ''.join(random.choice(string.lowercase) for _ in range(30)) + '\n'
+ targetstring = '%s%s%s' % (randomstring[:12], 'kodi_callbacks', randomstring[20:])
+ for i in xrange(0, 10):
+ with open(testLog.fn, 'a') as f:
+ if i == 5:
+ f.writelines(targetstring)
+ else:
+ f.writelines(randomstring)
+ time.sleep(0.25)
+
+ def setup(self):
+ flexmock(log, logfn=testLog.fn)
+ flexmock(log.xbmc, log=printlog)
+ flexmock(log.xbmc, sleep=sleep)
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+
+ def teardown(self):
+ self.publisher.abort()
+ self.dispatcher.abort()
+ del self.publisher
+ del self.dispatcher
+
+ def testLogSimple(self):
+ self.topics = [Topic('onLogSimple', 'E1')]
+ xsettings = [{'matchIf': 'kodi_callbacks', 'rejectIf': '', 'eventId': 'E1'}]
+ settings = Settings()
+ flexmock(settings, getLogSimples=xsettings)
+ flexmock(settings, general={'LogFreq': 100})
+ self.publisher = LogPublisher(self.dispatcher, settings)
+ try:
+ os.remove(testLog.fn)
+ except OSError:
+ pass
+ finally:
+ with open(testLog.fn, 'w') as f:
+ f.writelines('')
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ self.dispatcher.start()
+ self.publisher.start()
+ xthread = threading.Thread(target=testLog.logSimulate)
+ xthread.start()
+ xthread.join()
+ self.publisher.abort()
+ self.dispatcher.abort()
+ self.subscriber.waitForMessage(count=1, timeout=2)
+ try:
+ os.remove(testLog.fn)
+ except OSError:
+ pass
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testLogRegex(self):
+ self.topics = [Topic('onLogRegex', 'E1')]
+ xsettings = [{'matchIf': 'kodi_callbacks', 'rejectIf': '', 'eventId': 'E1'}]
+ settings = Settings()
+ flexmock(settings, getLogRegexes=xsettings)
+ flexmock(settings, general={'LogFreq': 100})
+ self.publisher = LogPublisher(self.dispatcher, settings)
+ try:
+ os.remove(testLog.fn)
+ except OSError:
+ pass
+ finally:
+ with open(testLog.fn, 'w') as f:
+ f.writelines('')
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ self.dispatcher.start()
+ self.publisher.start()
+ t = threading.Thread(target=testLog.logSimulate)
+ t.start()
+ t.join()
+ self.publisher.abort()
+ self.dispatcher.abort()
+ self.subscriber.waitForMessage(count=1, timeout=2)
+ try:
+ os.remove(testLog.fn)
+ except OSError:
+ pass
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+# @SkipTest
+class TestSchedule(object):
+ def __init__(self):
+ self.publisher = None
+ self.dispatcher = None
+ self.subscriber = None
+ self.topics = None
+
+ def setup(self):
+ flexmock(log.xbmc, log=printlog)
+ flexmock(log.xbmc, sleep=sleep)
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+
+ def teardown(self):
+ self.publisher.abort()
+ self.dispatcher.abort()
+ del self.publisher
+ del self.dispatcher
+
+ def testDailyAlarm(self):
+ from time import strftime
+ self.topics = [Topic('onDailyAlarm', 'E1')]
+ hour, minute = strftime('%H:%M').split(':')
+ xsettings = [{'hour': int(hour), 'minute': int(minute) + 1, 'key': 'E1'}]
+ settings = Settings()
+ flexmock(settings, getEventsByType=xsettings)
+ self.publisher = SchedulePublisher(self.dispatcher, settings)
+ self.publisher.intervalAlarms = []
+ self.publisher.sleep = time.sleep
+ self.publisher.sleepinterval = 1
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=65)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ time.sleep(1)
+ for topic in self.topics:
+ assert topic in msgtopics
+
+ def testIntervalAlarm(self):
+ self.topics = [Topic('onIntervalAlarm', 'E1')]
+ xsettings = [{'hours': 0, 'minutes': 0, 'seconds': 10, 'key': 'E1'}]
+ settings = Settings()
+ self.dispatcher = Dispatcher()
+ self.subscriber = MockSubscriber()
+ flexmock(settings, getEventsByType=xsettings)
+ self.publisher = SchedulePublisher(self.dispatcher, settings)
+ self.publisher.dailyAlarms = []
+ self.publisher.sleep = time.sleep
+ self.publisher.sleepinterval = 1
+ self.subscriber.testq = Queue.Queue()
+ self.subscriber.addTopic(self.topics[0])
+ self.dispatcher.addSubscriber(self.subscriber)
+ self.dispatcher.start()
+ self.publisher.start()
+ self.subscriber.waitForMessage(count=1, timeout=20)
+ self.publisher.abort()
+ self.dispatcher.abort()
+ messages = self.subscriber.retrieveMessages()
+ msgtopics = [msg.topic for msg in messages]
+ for topic in self.topics:
+ assert topic in msgtopics
+
+
+def main():
+ module_name = sys.modules[__name__].__file__
+ result = nose.run(
+ argv=[sys.argv[0],
+ module_name]
+ )
+ return result
+
+
+if __name__ == '__main__':
+ main()
diff --git a/script.service.kodi.callbacks/resources/lib/tests/testTasks.py b/script.service.kodi.callbacks/resources/lib/tests/testTasks.py
new file mode 100644
index 0000000000..707266f50a
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/testTasks.py
@@ -0,0 +1,272 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import os
+import sys
+import Queue
+import time
+import re
+import traceback
+import json
+from resources.lib import taskdict
+from resources.lib.pubsub import Topic, TaskManager
+from resources.lib.events import Events
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.utils.kodipathtools import translatepath, setPathExecuteRW
+import xbmc
+from resources.lib.utils.poutil import KodiPo
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+kl = KodiLogger()
+log = kl.log
+events = Events().AllEvents
+isAndroid = 'XBMC_ANDROID_SYSTEM_LIBS' in os.environ.keys()
+testdir = translatepath('special://addon/resources/lib/tests')
+setPathExecuteRW(testdir)
+
+
+def is_xbmc_debug():
+ json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Settings.GetSettingValue", "params": {"setting":"debug.showloginfo"} }')
+ json_query = unicode(json_query, 'utf-8', errors='ignore')
+ json_response = json.loads(json_query)
+ if json_response.has_key('result'):
+ if json_response['result'].has_key('value'):
+ return json_response['result']['value']
+ else:
+ raise SyntaxError('Bad JSON')
+ else:
+ raise SyntaxError('Bad JSON')
+
+
+def getWebserverInfo():
+ json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Settings.getSettings", "params":'
+ ' {"filter":{"section":"services", "category":"webserver"}}}')
+ json_query = unicode(json_query, 'utf-8', errors='ignore')
+ json_response = json.loads(json_query)
+
+ if json_response.has_key('result') and json_response['result'].has_key('settings') and json_response['result']['settings'] is not None:
+ serverEnabled = False
+ serverPort = 8080
+ serverUser = ''
+ serverPassword = ''
+ for item in json_response['result']['settings']:
+ if item["id"] == u"services.webserver":
+ if item["value"] is True:
+ serverEnabled = True
+ elif item["id"] == u'services.webserverport':
+ serverPort = item['value']
+ elif item['id'] == u'services.webserverusername':
+ serverUser = item['value']
+ elif item['id'] == u'services.webserverpassword':
+ serverPassword = item['value']
+ return serverEnabled, serverPort, serverUser, serverPassword
+
+
+class testTasks(object):
+ def __init__(self):
+ self.task = None
+ self.q = Queue.Queue()
+
+ def clear_q(self):
+ self.q = Queue.Queue()
+
+ def testHttp(self):
+ serverEnabled, serverPort, serverUser, serverPassword = getWebserverInfo()
+ if serverEnabled:
+ self.task = taskdict['http']['class']
+ taskKwargs = {'http':'http://localhost:%s/jsonrpc' % str(serverPort), 'user':serverUser, 'pass':serverPassword, 'type':'http', 'request-type':'GET', 'notify':False}
+ userargs = '?request={"jsonrpc":%__"2.0"%_%__"id": 1%_%__"method":"Application.Setmute"%_%__"params":{"mute":"toggle"}}'
+ tm = TaskManager(self.task, 1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ tm.returnHandler = self.returnHandler
+ topic = Topic('onPlaybackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ try:
+ tr = self.q.get(timeout=1)
+ except Queue.Empty:
+ raise Queue.Empty(_('testHttp never returned'))
+ else:
+ tm.start(topic, **runKwargs) # Toggle Mute again
+ if tr.iserror is True:
+ log(loglevel=xbmc.LOGERROR, msg=_('testHttp returned with an error: %s') % tr.msg)
+ if '{"id":1,"jsonrpc":"2.0","result":' not in tr.msg:
+ raise AssertionError(_('Http test failed'))
+ else:
+ raise AssertionError('Http test cannot be run because webserver not enabled')
+
+ def returnHandler(self, taskReturn):
+ self.q.put_nowait(taskReturn)
+
+ def testBuiltin(self):
+ start_debug = is_xbmc_debug()
+ self.task = taskdict['builtin']['class']
+ taskKwargs = {'builtin':'ToggleDebug', 'type':'builtin', 'notify':False}
+ userargs = ''
+ tm = TaskManager(self.task,1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ topic = Topic('onPlayBackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ time.sleep(3)
+ debug = is_xbmc_debug()
+ tm.start(topic, **runKwargs)
+ if debug == start_debug:
+ raise AssertionError(_('Builtin test failed'))
+
+
+ def testScriptNoShell(self):
+ self.task = taskdict['script']['class']
+ if sys.platform.startswith('win'):
+ testfile = 'tstScript.bat'
+ else:
+ testfile = 'tstScript.sh'
+ taskKwargs = {'scriptfile':'"%s"' % os.path.join(testdir, testfile),
+ 'use_shell':False, 'type':'script', 'waitForCompletion': True, 'notify':False}
+ self.task.validate(taskKwargs)
+ userargs = 'abc def:ghi'
+ tm = TaskManager(self.task, 1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ tm.returnHandler = self.returnHandler
+ topic = Topic('onPlaybackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ try:
+ tr = self.q.get(timeout=2)
+ except Queue.Empty:
+ raise AssertionError(_('Timed out waiting for return'))
+ else:
+ if tr.iserror is True:
+ log(loglevel=xbmc.LOGERROR, msg=_('testScriptNoShell returned with an error: %s') % tr.msg)
+ raise AssertionError (_('Script without shell test failed'))
+ else:
+ retArgs = re.findall(r'Process returned data: \[(.+)\]', tr.msg)[0]
+ if retArgs != userargs:
+ raise AssertionError(_('Script without shell test failed'))
+
+ def testScriptShell(self):
+ self.task = taskdict['script']['class']
+ if sys.platform.startswith('win'):
+ testfile = 'tstScript.bat'
+ else:
+ testfile = 'tstScript.sh'
+ taskKwargs = {'scriptfile':'"%s"' % os.path.join(testdir, testfile),
+ 'use_shell':True, 'type':'script', 'waitForCompletion': True, 'notify':False}
+ userargs = 'abc def:ghi'
+ self.task.validate(taskKwargs)
+ tm = TaskManager(self.task, 1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ tm.returnHandler = self.returnHandler
+ topic = Topic('onPlaybackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ try:
+ tr = self.q.get(timeout=2)
+ except Queue.Empty:
+ raise AssertionError(_('Timed out waiting for return'))
+ else:
+ if tr.iserror is True:
+ log(loglevel=xbmc.LOGERROR, msg=_('testScriptShell returned with an error: %s') % tr.msg)
+ raise AssertionError(_('Script with shell test failed'))
+ else:
+ retArgs = re.findall(r'Process returned data: \[(.+)\]', tr.msg)[0]
+ if retArgs != userargs:
+ raise AssertionError(_('Script with shell test failed'))
+
+
+ def testPythonImport(self):
+ self.task = taskdict['python']['class']
+ taskKwargs = {'pythonfile':os.path.join(testdir,'tstPythonGlobal.py'),
+ 'import':True, 'type':'python', 'notify':False}
+ userargs = 'abc def:ghi'
+ tm = TaskManager(self.task, 1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ tm.returnHandler = self.returnHandler
+ topic = Topic('onPlaybackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ try:
+ tr = self.q.get(timeout=1)
+ except Queue.Empty:
+ raise AssertionError(_('Timed out waiting for return'))
+ if tr.iserror is True:
+ log(loglevel=xbmc.LOGERROR, msg=_('testPythonImport returned with an error: %s') % tr.msg)
+ try:
+ retArgs = sys.modules['__builtin__'].__dict__['testReturn']
+ except KeyError:
+ retArgs = sys.modules['builtins'].__dict__['testReturn']
+ finally:
+ try:
+ sys.modules['__builtin__'].__dict__.pop('testReturn', None)
+ except KeyError:
+ sys.modules['builtins'].__dict__.pop('testReturn', None)
+ if ' '.join(retArgs[0]) != userargs:
+ raise AssertionError(_('Python import test failed'))
+
+ def testPythonExternal(self):
+ self.task = taskdict['python']['class']
+ taskKwargs = {'pythonfile':os.path.join(testdir, 'tstPythonGlobal.py'),
+ 'import':True, 'type':'python', 'notify':False}
+ userargs = 'jkl mno:pqr'
+ tm = TaskManager(self.task, 1, None, -1, taskid='T1', userargs=userargs, **taskKwargs)
+ tm.returnHandler = self.returnHandler
+ topic = Topic('onPlaybackStarted')
+ runKwargs = events['onPlayBackStarted']['expArgs']
+ self.clear_q()
+ tm.start(topic, **runKwargs)
+ try:
+ tr = self.q.get(timeout=1)
+ except Queue.Empty:
+ raise AssertionError(_('Timed out waiting for return'))
+ if tr.iserror is True:
+ log(loglevel=xbmc.LOGERROR, msg=_('testPythonExternal returned with an error: %s') % tr.msg)
+ if isAndroid:
+ raise AssertionError(_('Cannot fully test pythonExternal on Android'))
+ try:
+ retArgs = sys.modules['__builtin__'].__dict__['testReturn']
+ except KeyError:
+ retArgs = sys.modules['builtins'].__dict__['testReturn']
+ finally:
+ try:
+ sys.modules['__builtin__'].__dict__.pop('testReturn', None)
+ except KeyError:
+ sys.modules['builtins'].__dict__.pop('testReturn', None)
+ if ' '.join(retArgs[0]) != userargs:
+ raise AssertionError(_('Python external test failed'))
+
+ def runTests(self):
+ tests = [self.testHttp, self.testBuiltin, self.testScriptNoShell, self.testScriptShell, self.testPythonExternal,
+ self.testPythonImport]
+ for test in tests:
+ testname = test.__name__
+ sys.exc_clear()
+ try:
+ test()
+ except AssertionError as e:
+ log(msg=_('Error testing %s: %s') % (testname, str(e)))
+ except Exception:
+ msg = _('Error testing %s\n') % testname
+ e = sys.exc_info()[0]
+ if hasattr(e, 'message'):
+ msg += str(e.message)
+ else:
+ msg += str(e)
+ msg = msg + '\n' + traceback.format_exc()
+ log(loglevel=xbmc.LOGERROR, msg=msg)
+ else:
+ log(msg=_('Test passed for task %s') % str(testname))
diff --git a/script.service.kodi.callbacks/resources/lib/tests/tstPythonGlobal.py b/script.service.kodi.callbacks/resources/lib/tests/tstPythonGlobal.py
new file mode 100644
index 0000000000..e8675cdc15
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/tstPythonGlobal.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import sys
+
+def run(*args):
+ try:
+ sys.modules['__builtin__'].__dict__['testReturn'] = args
+ except KeyError:
+ sys.modules['builtins'].__dict__['testReturn'] = args
+
+if __name__ == '__main__':
+ run(list(sys.argv[1:]))
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/tests/tstScript.bat b/script.service.kodi.callbacks/resources/lib/tests/tstScript.bat
new file mode 100644
index 0000000000..828a2127b3
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/tstScript.bat
@@ -0,0 +1 @@
+@echo %1 %2
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/tests/tstScript.sh b/script.service.kodi.callbacks/resources/lib/tests/tstScript.sh
new file mode 100644
index 0000000000..b13cae6ff9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/tests/tstScript.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+echo $1 $2
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/utils/__init__.py b/script.service.kodi.callbacks/resources/lib/utils/__init__.py
new file mode 100644
index 0000000000..0463614978
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
diff --git a/script.service.kodi.callbacks/resources/lib/utils/copyToDir.py b/script.service.kodi.callbacks/resources/lib/utils/copyToDir.py
new file mode 100644
index 0000000000..1b3896c635
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/copyToDir.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import os
+import shutil
+import fnmatch
+import stat
+import itertools
+
+def copyToDir(src, dst, updateonly=True, symlinks=True, ignore=None, forceupdate=None, dryrun=False):
+
+ def copySymLink(srclink, destlink):
+ if os.path.lexists(destlink):
+ os.remove(destlink)
+ os.symlink(os.readlink(srclink), destlink)
+ try:
+ st = os.lstat(srclink)
+ mode = stat.S_IMODE(st.st_mode)
+ os.lchmod(destlink, mode)
+ except OSError:
+ pass # lchmod not available
+ fc = []
+ if not os.path.exists(dst) and not dryrun:
+ os.makedirs(dst)
+ shutil.copystat(src, dst)
+ if ignore is not None:
+ ignorepatterns = [os.path.join(src, *x.split('/')) for x in ignore]
+ else:
+ ignorepatterns = []
+ if forceupdate is not None:
+ forceupdatepatterns = [os.path.join(src, *x.split('/')) for x in forceupdate]
+ else:
+ forceupdatepatterns = []
+ srclen = len(src)
+ for root, dirs, files in os.walk(src):
+ fullsrcfiles = [os.path.join(root, x) for x in files]
+ t = root[srclen+1:]
+ dstroot = os.path.join(dst, t)
+ fulldstfiles = [os.path.join(dstroot, x) for x in files]
+ excludefiles = list(itertools.chain.from_iterable([fnmatch.filter(fullsrcfiles, pattern) for pattern in ignorepatterns]))
+ forceupdatefiles = list(itertools.chain.from_iterable([fnmatch.filter(fullsrcfiles, pattern) for pattern in forceupdatepatterns]))
+ for directory in dirs:
+ fullsrcdir = os.path.join(src, directory)
+ fulldstdir = os.path.join(dstroot, directory)
+ if os.path.islink(fullsrcdir):
+ if symlinks and dryrun is False:
+ copySymLink(fullsrcdir, fulldstdir)
+ else:
+ if not os.path.exists(fulldstdir) and dryrun is False:
+ os.makedirs(fulldstdir)
+ shutil.copystat(src, dst)
+ for s,d in zip(fullsrcfiles, fulldstfiles):
+ if s not in excludefiles:
+ if updateonly:
+ go = False
+ if os.path.isfile(d):
+ srcdate = os.stat(s).st_mtime
+ dstdate = os.stat(d).st_mtime
+ if srcdate > dstdate:
+ go = True
+ else:
+ go = True
+ if s in forceupdatefiles:
+ go = True
+ if go is True:
+ fc.append(d)
+ if not dryrun:
+ if os.path.islink(s) and symlinks is True:
+ copySymLink(s, d)
+ else:
+ shutil.copy2(s, d)
+ else:
+ fc.append(d)
+ if not dryrun:
+ if os.path.islink(s) and symlinks is True:
+ copySymLink(s, d)
+ else:
+ shutil.copy2(s, d)
+ return fc
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/utils/debugger.py b/script.service.kodi.callbacks/resources/lib/utils/debugger.py
new file mode 100644
index 0000000000..c8e000d613
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/debugger.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import os
+import sys
+
+class pydevd_dummy(object):
+ @staticmethod
+ def settrace(*args, **kwargs):
+ pass
+
+def startdebugger():
+ debugegg = ''
+ if sys.platform.lower().startswith('win'):
+ debugegg = os.path.expandvars('%programfiles(x86)%\\JetBrains\\PyCharm 2016.1\\debug-eggs\\pycharm-debug.egg')
+ elif sys.platform.lower().startswith('darwin'):
+ debugegg = '/Applications/PyCharm.app/Contents/debug-eggs/pycharm-debug.egg'
+ elif sys.platform.lower().startswith('linux'):
+ debugegg = os.path.expandvars(os.path.expanduser('~/PyCharm 2016.1/debug-eggs/pycharm-debug.egg'))
+ if os.path.exists(debugegg):
+ sys.path.append(debugegg)
+ try:
+ import pydevd
+ except ImportError:
+ pydevd = pydevd_dummy
+ pydevd.settrace('localhost', port=51234, stdoutToServer=True, stderrToServer=True, suspend=False)
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/utils/detectPath.py b/script.service.kodi.callbacks/resources/lib/utils/detectPath.py
new file mode 100644
index 0000000000..a974358adc
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/detectPath.py
@@ -0,0 +1,64 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import os
+import shlex
+import sys
+from resources.lib.utils.kodipathtools import translatepath
+
+def process_cmdline(cmd):
+ posspaths = []
+ parts = shlex.split(cmd, posix= not sys.platform.startswith('win'))
+ for i in xrange(0, len(parts)):
+ found=-1
+ for j in xrange(i+1, len(parts)+1):
+ t = ' '.join(parts[i:j])
+ t = translatepath(t)
+ t = t.strip('"')
+ if os.path.exists(t):
+ if j > found:
+ found = j
+ if found != -1:
+ posspaths.append([i, found])
+ paths = []
+ args = []
+ if len(posspaths) > 0:
+ for i, path in enumerate(posspaths): # Check for overlaps
+ if i > 0:
+ if path[0] < posspaths[i-1][1]:
+ pass # If possible paths overlap, treat the first as a path and treat the rest of the overlap as non-path
+ else:
+ paths.append(path)
+ else:
+ paths.append(path)
+ for i in xrange(0, len(parts)):
+ for j in xrange(0, len(paths)):
+ if i == paths[j][0]:
+ t = ' '.join(parts[i:paths[j][1]])
+ t = translatepath(t)
+ t = t.strip('"')
+ parts[i] = t
+ for k in xrange(i+1, paths[j][1]):
+ parts[k]=''
+ for i in xrange(0, len(parts)):
+ if parts[i] != '':
+ args.append(parts[i])
+ else:
+ args = parts
+ return args
diff --git a/script.service.kodi.callbacks/resources/lib/utils/kodipathtools.py b/script.service.kodi.callbacks/resources/lib/utils/kodipathtools.py
new file mode 100644
index 0000000000..9e34b36a4c
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/kodipathtools.py
@@ -0,0 +1,182 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import os
+import platform
+import re
+import stat
+import sys
+
+import xbmc
+import xbmcaddon
+
+
+def _translatePathMock(path):
+ return kodiTranslatePathMock(path)
+
+
+try:
+ from xbmc import translatePath as kodiTP
+except ImportError:
+ kodiTP = _translatePathMock
+ isStub = True
+else:
+ if kodiTP('special://home') == u'':
+ isStub = True
+ kodiTP = _translatePathMock
+ else:
+ isStub = False
+
+_split = re.compile(r'\0')
+
+
+def getPlatform():
+ if sys.platform.startswith('win'):
+ ret = 'win'
+ elif platform.system().lower().startswith('darwin'):
+ if platform.machine().startswith('iP'):
+ ret = 'ios'
+ else:
+ ret = 'osx'
+ elif 'XBMC_ANDROID_SYSTEM_LIBS' in os.environ.keys():
+ ret = 'and'
+ else: # Big assumption here
+ ret = 'nix'
+ return ret
+
+
+def secure_filename(path):
+ return _split.sub('', path)
+
+
+def translatepath(path):
+ ret = []
+ if path.lower().startswith('special://'):
+ special = re.split(r'\\|/', path[10:])[0]
+ if special.startswith('addondata'):
+ myid = re.findall(r'addondata\((.+?)\)', special)
+ if len(myid) > 0:
+ ret.append(addondatapath(myid[0]))
+ else:
+ ret.append(addondatapath())
+ elif special.startswith('addon'):
+ myid = re.findall(r'addon\((.+?)\)', special)
+ if len(myid) > 0:
+ ret.append(addonpath(myid[0]))
+ else:
+ ret.append(addonpath())
+ else:
+ ret.append(kodiTP('special://%s' % special))
+ path = path[10:]
+ ret = ret + re.split(r'\\|/', path)[1:]
+ else:
+ ret = re.split(r'\\|/', path)
+ if ret[0].endswith(':'):
+ ret[0] = '%s\\' % ret[0]
+ for i, r in enumerate(ret):
+ ret[i] = secure_filename(r)
+ ret = os.path.join(*ret)
+ ret = os.path.expandvars(ret)
+ ret = os.path.expanduser(ret)
+ ret = os.path.normpath(ret)
+ if path.startswith('/'):
+ ret = '/%s' % ret
+
+ if not os.path.supports_unicode_filenames:
+ ret = ret.decode('utf-8')
+
+ ret = secure_filename(ret)
+ return ret
+
+
+def kodiTranslatePathMock(path):
+ ret = []
+ special = re.split(r'\\|/', path[10:])[0]
+ if special == 'home':
+ ret.append(homepath())
+ elif special == 'logpath':
+ ret.append(logpath())
+ elif special == 'masterprofile' or special == 'userdata':
+ ret = ret + [homepath(), 'userdata']
+ return os.path.join(*ret)
+
+
+def addonpath(addon_id='script.service.kodi.callbacks'):
+ if isStub:
+ path = os.path.join(*[homepath(), 'addons', addon_id])
+ else:
+ try:
+ path = xbmcaddon.Addon(addon_id).getAddonInfo('path')
+ except RuntimeError:
+ path = ''
+ if path == '':
+ path = os.path.join(*[homepath(), 'addons', addon_id])
+ return path
+
+
+def addondatapath(addon_id='script.service.kodi.callbacks'):
+ if isStub:
+ path = os.path.join(*[homepath(), 'userdata', 'addon_data', addon_id])
+ else:
+ path = os.path.join(*[xbmc.translatePath('special://userdata'), 'addon_data', addon_id])
+ return path
+
+
+def homepath():
+ paths = {'win': r'%APPDATA%\Kodi', 'nix': r'$HOME/.kodi', 'osx': r'~/Library/Application Support/Kodi',
+ 'ios': r'/private/var/mobile/Library/Preferences/Kodi',
+ 'and': r' /sdcard/Android/data/org.xbmc.kodi/files/.kodi/'}
+ if isStub:
+ return translatepath(paths[getPlatform()])
+ else:
+ return xbmc.translatePath('special://home')
+
+
+def logpath():
+ paths = {'win': r'%APPDATA%\Kodi\kodi.log', 'nix': r'$HOME/.kodi/temp/kodi.log', 'osx': r'~/Library/Logs/kodi.log',
+ 'ios': r'/private/var/mobile/Library/Preferences/kodi.log',
+ 'and': r'/sdcard/Android/data/org.xbmc.kodi/files/.kodi/temp/kodi.log'}
+ if isStub:
+ return translatepath(paths[getPlatform()])
+ else:
+ return xbmc.translatePath('special://logpath')
+
+
+def setPathExecuteRW(path):
+ path = translatepath(path)
+ try:
+ os.chmod(path, os.stat(
+ path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+ except OSError:
+ pass
+
+
+def setPathExecute(path):
+ path = translatepath(path)
+ try:
+ os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+ except OSError:
+ pass
+
+
+def setPathRW(path):
+ path = translatepath(path)
+ try:
+ os.chmod(path, os.stat(path).st_mode | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+ except OSError:
+ pass
diff --git a/script.service.kodi.callbacks/resources/lib/utils/poutil.py b/script.service.kodi.callbacks/resources/lib/utils/poutil.py
new file mode 100644
index 0000000000..13800dff47
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/poutil.py
@@ -0,0 +1,341 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import os
+import codecs
+import fnmatch
+import re
+import operator
+import xbmcaddon
+import threading
+import copy
+from resources.lib.utils.kodipathtools import translatepath
+from resources.lib.kodilogging import KodiLogger
+klogger = KodiLogger()
+log = klogger.log
+
+try:
+ addonid = xbmcaddon.Addon().getAddonInfo('id')
+except RuntimeError:
+ addonid = 'script.service.kodi.callbacks'
+if addonid == '':
+ addonid = 'script.service.kodi.callbacks'
+
+class KodiPo(object):
+
+ _instance = None
+ _lock = threading.Lock()
+
+ def __new__(cls):
+ if KodiPo._instance is None:
+ with KodiPo._lock:
+ if KodiPo._instance is None:
+ KodiPo._instance = super(KodiPo, cls).__new__(cls)
+ KodiPo.cls_init()
+ return KodiPo._instance
+
+ @classmethod
+ def cls_init(cls):
+ cls.pofn = translatepath('special://addon/resources/language/English/strings.po')
+ cls.isStub = xbmcaddon.Addon().getAddonInfo('id') == ''
+ cls.podict = PoDict()
+ cls.podict.read_from_file(cls.pofn)
+ cls.updateAlways = False
+
+
+ def __init__(self):
+ KodiPo._instance = self
+
+ def _(self, strToId, update=False):
+ self.getLocalizedString(strToId, update)
+
+ def getLocalizedString(self, strToId, update=False):
+ strToId = strToId.replace('\n', '\\n')
+ idFound, strid = self.podict.has_msgid(strToId)
+ if idFound:
+ if self.podict.savethread.is_alive():
+ self.podict.savethread.join()
+ ret = xbmcaddon.Addon(addonid).getLocalizedString(int(strid))
+ if ret == u'': # Occurs with stub or undefined number
+ if not self.isStub:
+ log(_('Localized string not found for: [%s]') % str(strToId))
+ ret = strToId
+ return ret
+ else:
+ if update is True or self.updateAlways is True:
+
+ log(msg=_('Localized string added to po for: [%s]') % strToId)
+ self.updatePo(strid, strToId)
+ else:
+ log(msg=_('Localized string id not found for: [%s]') % strToId)
+ return strToId
+
+ def getLocalizedStringId(self, strToId, update=False):
+ idFound, strid = self.podict.has_msgid(strToId)
+ if idFound:
+ return strid
+ else:
+ if update is True or self.updateAlways is True:
+ self.updatePo(strid, strToId)
+ log(msg=_('Localized string added to po for: [%s]') % strToId)
+ return strid
+ else:
+ log(msg=_('Localized string not found for: [%s]') % strToId)
+ return 32168
+
+ def updatePo(self, strid, txt):
+ self.podict.addentry(strid, txt)
+ self.podict.write_to_file(self.pofn)
+
+class PoDict(object):
+
+ _instance = None
+ _lock = threading.Lock()
+ _rlock = threading.RLock()
+
+ def __new__(cls):
+ if PoDict._instance is None:
+ with PoDict._lock:
+ if PoDict._instance is None:
+ PoDict._instance = super(PoDict, cls).__new__(cls)
+ return PoDict._instance
+
+ def __init__(self):
+ PoDict._instance = self
+ self.dict_msgctxt = dict()
+ self.dict_msgid = dict()
+ self.chkdict = dict()
+ self.remsgid = re.compile(r'"([^"\\]*(?:\\.[^"\\]*)*)"')
+ self.savethread = threading.Thread()
+
+ def get_new_key(self):
+ if len(self.dict_msgctxt) > 0:
+ with PoDict._rlock:
+ mmax = max(self.dict_msgctxt.iteritems(), key=operator.itemgetter(0))[0]
+ else:
+ mmax = '32000'
+ try:
+ int_key = int(mmax)
+ except ValueError:
+ int_key = -1
+ return int_key + 1
+
+ def addentry(self, str_msgctxt, str_msgid):
+ with PoDict._lock:
+ self.dict_msgctxt[str_msgctxt] = str_msgid
+ self.dict_msgid[str_msgid] = str_msgctxt
+
+ def has_msgctxt(self, str_msgctxt): # Returns the English string associated with the id provided
+ with PoDict._lock:
+ if str_msgctxt in self.dict_msgctxt.keys():
+ return [True, self.dict_msgctxt[str_msgctxt]]
+ else:
+ return [False, None]
+
+ def has_msgid(self, str_msgid): # Returns the id in .po as a string i.e. "32000"
+ with PoDict._lock:
+ if str_msgid in self.dict_msgid.keys():
+ return [True, self.dict_msgid[str_msgid]]
+ else:
+ return [False, str(self.get_new_key())]
+
+ def read_from_file(self, url):
+ if url is None:
+ log(loglevel=klogger.LOGERROR, msg='No URL to Read PoDict From')
+ return
+ if os.path.exists(url):
+ try:
+ with codecs.open(url, 'r', 'UTF-8') as f:
+ poin = f.readlines()
+ i = 0
+ while i < len(poin):
+ line = poin[i]
+ if line[0:7] == 'msgctxt':
+ t = re.findall(r'".+"', line)
+ if not t[0].startswith('"Addon'):
+ str_msgctxt = t[0][2:7]
+ i += 1
+ line2 = poin[i]
+ str_msgid = self.remsgid.findall(line2)[0]
+ self.dict_msgctxt[str_msgctxt] = str_msgid
+ self.dict_msgid[str_msgid] = str_msgctxt
+ self.chkdict[str_msgctxt] = False
+ else:
+ i += 1
+ i += 1
+ except Exception as e:
+ log(loglevel=klogger.LOGERROR, msg='Error reading po: %s' % e.message)
+ else:
+ log(loglevel=klogger.LOGERROR, msg='Could not locate po at %s' % url)
+
+ def write_to_file(self, url):
+ if self.savethread is not None:
+ assert isinstance(self.savethread, threading.Thread)
+ if self.savethread.is_alive():
+ self.savethread.join()
+ with PoDict._lock:
+ tmp = copy.copy(self.dict_msgctxt)
+ self.savethread = threading.Thread(target=PoDict._write_to_file, args=[tmp, url])
+ self.savethread.start()
+
+ @staticmethod
+ def _write_to_file(dict_msgctxt, url):
+ fo = codecs.open(url, 'wb', 'UTF-8')
+ PoDict.write_po_header(fo)
+ str_max = max(dict_msgctxt.iteritems(), key=operator.itemgetter(0))[0]
+ str_min = min(dict_msgctxt.iteritems(), key=operator.itemgetter(0))[0]
+
+ fo.write('msgctxt "Addon Summary"\n')
+ fo.write('msgid "Callbacks for Kodi"\n')
+ fo.write('msgstr ""\n\n')
+ fo.write('msgctxt "Addon Description"\n')
+ fo.write('msgid "Provides user definable actions for specific events within Kodi. Credit to Yesudeep Mangalapilly (gorakhargosh on github) and contributors for watchdog and pathtools modules."\n')
+ fo.write('msgstr ""\n\n')
+ fo.write('msgctxt "Addon Disclaimer"\n')
+ fo.write('msgid "For bugs, requests or general questions visit the Kodi forums."\n')
+ fo.write('msgstr ""\n\n')
+ fo.write('#Add-on messages id=%s to %s\n\n' % (str_min, str_max))
+ last = int(str_min) - 1
+ for str_msgctxt in sorted(dict_msgctxt):
+ if not str_msgctxt.startswith('Addon'):
+ if int(str_msgctxt) != last + 1:
+ fo.write('#empty strings from id %s to %s\n\n' % (str(last + 1), str(int(str_msgctxt) - 1)))
+ PoDict.write_to_po(fo, str_msgctxt, PoDict.format_string_forpo(dict_msgctxt[str_msgctxt]))
+ last = int(str_msgctxt)
+ fo.close()
+
+ @staticmethod
+ def format_string_forpo(mstr):
+ out = ''
+ for (i, x) in enumerate(mstr):
+ if i == 1 and x == r'"':
+ out += "\\" + x
+ elif x == r'"' and mstr[i-1] != "\\":
+ out += "\\" + x
+ else:
+ out += x
+ return out
+
+ @staticmethod
+ def write_po_header(fo):
+ fo.write('# Kodi Media Center language file\n')
+ fo.write('# Addon Name: Kodi Callbacks\n')
+ fo.write('# Addon id: script.service.kodi.callbacks\n')
+ fo.write('# Addon Provider: KenV99\n')
+ fo.write('msgid ""\n')
+ fo.write('msgstr ""\n')
+ fo.write('"Project-Id-Version: XBMC Addons\\n"\n')
+ fo.write('"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n')
+ fo.write('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n')
+ fo.write('"MIME-Version: 1.0\\n"\n')
+ fo.write('"Content-Type: text/plain; charset=UTF-8\\n"\n')
+ fo.write('"Content-Transfer-Encoding: 8bit\\n"\n')
+ fo.write('"Language: en\\n"')
+ fo.write('"Plural-Forms: nplurals=2; plural=(n != 1);\\n"\n\n')
+
+ @staticmethod
+ def write_to_po(fileobject, int_num, str_msg):
+ w = r'"#' + str(int_num) + r'"'
+ fileobject.write('msgctxt ' + w + '\n')
+ fileobject.write('msgid ' + r'"' + str_msg + r'"' + '\n')
+ fileobject.write('msgstr ' + r'""' + '\n')
+ fileobject.write('\n')
+
+ def createreport(self):
+ cnt = 0
+ reportpo = []
+ for x in self.chkdict:
+ if not self.chkdict[x]:
+ if cnt == 0:
+ reportpo = ['No usage found for the following pairs:']
+ msgid = self.dict_msgctxt[x]
+ reportpo.append(' %s:%s' % (x, msgid))
+ cnt += 1
+ ret = '\n '.join(reportpo)
+ return ret
+
+
+class UpdatePo(object):
+
+ def __init__(self, root_directory_to_scan, current_working_English_strings_po, exclude_directories=None, exclude_files=None):
+ if exclude_directories is None:
+ exclude_directories = []
+ if exclude_files is None:
+ exclude_files = []
+ self.root_directory_to_scan = root_directory_to_scan
+ self.current_working_English_strings_po = current_working_English_strings_po
+ self.podict = PoDict()
+ self.podict.read_from_file(self.current_working_English_strings_po)
+ self.exclude_directories = exclude_directories
+ self.exclude_files = exclude_files
+ self.find_localizer = re.compile(r'^(\S+?)\s*=\s*kodipo.getLocalizedString\s*$', flags=re.MULTILINE)
+
+ def getFileList(self):
+ files_to_scan = []
+ exclusions = []
+ for direct in self.exclude_directories:
+ for root, ___, filenames in os.walk(os.path.join(self.root_directory_to_scan, direct)):
+ for filename in filenames:
+ exclusions.append(os.path.join(root, filename))
+ for root, ___, filenames in os.walk(self.root_directory_to_scan):
+ for filename in fnmatch.filter(filenames, '*.py'):
+ if os.path.split(filename)[1] in self.exclude_files:
+ continue
+ elif os.path.join(root, filename) in exclusions:
+ continue
+ else:
+ files_to_scan.append(os.path.join(root, filename))
+ return files_to_scan
+
+ def scanPyFilesForStrings(self):
+ files = self.getFileList()
+ lstrings = []
+ for myfile in files:
+ finds = []
+ with open(myfile, 'r') as f:
+ lines = ''.join(f.readlines())
+ try:
+ finds = self.find_localizer.findall(lines)
+ except re.error:
+ pass
+ finally:
+ if len(finds) != 1:
+ log(msg='Skipping file: %s, localizer not found' % myfile)
+ else:
+ findstr = r"%s\('(.+?)'\)" % finds[0]
+ find = re.compile(findstr)
+ finds = []
+ try:
+ finds = find.findall(lines)
+ except re.error:
+ pass
+ lstrings += finds
+ return lstrings
+
+ def updateStringsPo(self):
+ lstrings = self.scanPyFilesForStrings()
+ for s in lstrings:
+ found, strid = self.podict.has_msgid(s)
+ if found is False:
+ self.podict.addentry(strid, s)
+ self.podict.write_to_file(self.current_working_English_strings_po)
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+__ = kodipo.getLocalizedStringId
diff --git a/script.service.kodi.callbacks/resources/lib/utils/selector.py b/script.service.kodi.callbacks/resources/lib/utils/selector.py
new file mode 100644
index 0000000000..9c4af6e553
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/selector.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+
+import xbmcaddon
+import xbmcgui
+
+
+def selectordialog(args):
+ """
+ Emulates a selector control that is lvalues compatible for subsequent conditionals in Kodi settings
+ args is a list of strings that is kwarg 'like'.
+ 'id=myid': (Required) where myid is the settings.xml id that will be updated
+ The plain id will contain the actual index value and is expected to be a hidden text element.
+ This is the value to be tested against in subsequent conitionals.
+ The string passed will be appended with '-v' i.e. myid-v. It is expected that this refers to a
+ disabled selector element with the same lvalues as passed to the script.
+ NOTE: There is an undocumented feature of type="select" controls to set the default based on an lvalue:
+ ldefault="lvalue" where lvalue is a po string id.
+ 'useindex=bool': (Optional)If True, the zero based index of the subsequent lvalues will be stored in the hidden test
+ element.
+ If False or not provided, will store the actual lvalue in the hidden field.
+ 'heading=lvalue': (Optional) String id for heading of dialog box.
+ 'lvalues=int|int|int|...': (Required) The list of lvalues to display as choices.
+
+ Usage example for settings.xml:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :param args: List of string args
+ :type args: list
+ :return: True is selected, False if cancelled
+ :rtype: bool
+ """
+
+ settingid = None
+ useindex = False
+ lvalues_str = None
+ heading = ''
+ for arg in args:
+ splitarg = arg.split('=')
+ kw = splitarg[0].strip()
+ value = splitarg[1].strip()
+ if kw == 'id':
+ settingid = value
+ elif kw == 'useindex':
+ useindex = value.lower() == 'true'
+ elif kw == 'lvalues':
+ lvalues_str = value.split('|')
+ elif kw == 'heading':
+ heading = value
+ if lvalues_str is None or settingid is None:
+ raise SyntaxError('Selector Dialog: Missing elements from args')
+ lvalues = []
+ choices = []
+ for lvalue in lvalues_str:
+ try:
+ lvalues.append(int(lvalue))
+ except TypeError:
+ raise TypeError('Selector Dialog: lvalue not int')
+ else:
+ choices.append(xbmcaddon.Addon().getLocalizedString(int(lvalue)))
+ if heading != '':
+ try:
+ lheading = int(heading)
+ except TypeError:
+ raise TypeError('Selector Dialog: heading lvalue not int')
+ else:
+ lheading = ''
+ result = xbmcgui.Dialog().select(heading=xbmcaddon.Addon().getLocalizedString(lheading), list=choices)
+ if result != -1:
+ if useindex:
+ xbmcaddon.Addon().setSetting(settingid, str(result))
+ else:
+ xbmcaddon.Addon().setSetting(settingid, str(lvalues[result]))
+ xbmcaddon.Addon().setSetting('%s-v' % settingid, str(result))
+ return True
+ else:
+ return False
diff --git a/script.service.kodi.callbacks/resources/lib/utils/updateaddon.py b/script.service.kodi.callbacks/resources/lib/utils/updateaddon.py
new file mode 100644
index 0000000000..a22b497b29
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/utils/updateaddon.py
@@ -0,0 +1,441 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import contextlib
+import datetime
+import fnmatch
+import json
+import os
+import re
+import shutil
+import time
+import zipfile
+from stat import S_ISREG, ST_MTIME, ST_MODE
+from time import strftime
+
+import xbmc
+import xbmcaddon
+import xbmcgui
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.utils.copyToDir import copyToDir
+from resources.lib.utils.kodipathtools import translatepath
+from resources.lib.utils.poutil import KodiPo
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+
+kl = KodiLogger()
+log = kl.log
+
+
+class UpdateAddon(object):
+ def __init__(self, addonid=None, silent=False, numbackups=5):
+
+ self.addonid = addonid
+ self.addondir = translatepath('special://addon(%s)' % self.addonid)
+ self.addondatadir = translatepath('special://addondata(%s)' % self.addonid)
+ self.tmpdir = translatepath('%s/temp' % self.addondatadir)
+ self.backupdir = translatepath('%s/backup' % self.addondatadir)
+ self.silent = silent
+ self.numbackups = numbackups
+
+ @staticmethod
+ def currentversion(addonid):
+ currentversion = xbmcaddon.Addon(addonid).getAddonInfo('version')
+ if currentversion == u'': # Running stub
+ currentversion = '0.9.9'
+ return currentversion
+
+ @staticmethod
+ def prompt(strprompt, silent=False, force=False):
+ if not silent or force:
+ ddialog = xbmcgui.Dialog()
+ if ddialog.yesno('', strprompt):
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def notify(message, silent=False, force=False):
+ log(msg=message)
+ if not silent or force:
+ ddialog = xbmcgui.Dialog()
+ ddialog.ok('', message)
+
+ def cleartemp(self, recreate=True):
+ if os.path.exists(os.path.join(self.tmpdir, '.git')):
+ shutil.rmtree(os.path.join(self.tmpdir, '.git'))
+ if os.path.exists(self.tmpdir):
+ try:
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
+ except OSError:
+ return False
+ else:
+ if recreate is True:
+ os.mkdir(self.tmpdir)
+ return True
+ else:
+ if recreate is True:
+ os.mkdir(self.tmpdir)
+ return True
+
+ @staticmethod
+ def unzip(source_filename, dest_dir):
+ try:
+ with contextlib.closing(zipfile.ZipFile(source_filename, "r")) as zf:
+ zf.extractall(dest_dir)
+ except zipfile.BadZipfile:
+ log(msg='Zip File Error')
+ return False
+ return True
+
+ @staticmethod
+ def zipdir(dest, srcdir):
+ dest = '%s.zip' % dest
+ zipf = ZipArchive(dest, 'w', zipfile.ZIP_DEFLATED)
+ zipf.addDir(srcdir, srcdir)
+ zipf.close()
+
+ def backup(self, src=None, destdir=None, numbackups=5):
+ if src is None:
+ src = self.addondir
+ if destdir is None:
+ destdir = self.backupdir
+ ts = strftime("%Y-%m-%d-%H-%M-%S")
+ destname = os.path.join(destdir, '%s-%s' % (ts, self.addonid))
+ if not os.path.exists(destdir):
+ os.mkdir(destdir)
+ else:
+ if os.path.exists(destname):
+ os.remove(destname)
+ self.cleartemp(recreate=True)
+ archivedir = os.path.join(self.tmpdir,
+ '%s-%s' % (os.path.split(src)[1], xbmcaddon.Addon().getSetting('installedbranch')))
+ shutil.copytree(src, archivedir, ignore=shutil.ignore_patterns('*.pyc', '*.pyo', '.git', '.idea'))
+ UpdateAddon.zipdir(destname, self.tmpdir)
+ self.cleartemp(recreate=False)
+ sorteddir = UpdateAddon.datesorteddir(destdir)
+ num = len(sorteddir)
+ if num > numbackups:
+ for i in xrange(0, num - numbackups):
+ try:
+ os.remove(sorted(sorteddir)[i][2])
+ except OSError:
+ raise
+ return True
+
+ @staticmethod
+ def datesorteddir(sortdir): # oldest first
+ # returns list of tuples: (index, date, path)
+
+ # get all entries in the directory w/ stats
+ entries = (os.path.join(sortdir, fn) for fn in os.listdir(sortdir))
+ entries = ((os.stat(path), path) for path in entries)
+
+ # leave only regular files, insert creation date
+ entries = ((stat[ST_MTIME], path)
+ for stat, path in entries if S_ISREG(stat[ST_MODE]))
+ entrylist = []
+ i = 0
+ for cdate, path in sorted(entries):
+ entrylist.append((i, cdate, path))
+ i += 1
+ return entrylist
+
+ @staticmethod
+ def is_v1_gt_v2(version1, version2):
+ def normalize(v):
+ return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")]
+
+ result = cmp(normalize(version1), normalize(version2))
+ if result == 1:
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def checkfilematch(fn, lst):
+ ret = False
+ for item in lst:
+ if fn == item:
+ ret = True
+ elif fnmatch.fnmatchcase(fn, item):
+ ret = True
+ return ret
+
+ @staticmethod
+ def getBranchFromFile(path):
+ root = UpdateAddon.getAddonxmlPath(path)
+ if root != '':
+ ps = root.split('-')
+ if len(ps) == 1:
+ return ''
+ else:
+ return ps[len(ps) - 1]
+ else:
+ return ''
+
+ def installFromZip(self, zipfn, dryrun=False, updateonly=None, deletezip=False, silent=False):
+ if os.path.split(os.path.split(zipfn)[0])[1] == 'backup':
+ log(msg='Installing from backup')
+ isBackup = True
+ else:
+ isBackup = False
+ unzipdir = os.path.join(self.addondatadir, 'tmpunzip')
+ if UpdateAddon.unzip(zipfn, unzipdir) is False:
+ UpdateAddon.notify(_('Downloaded file could not be extracted'))
+ try:
+ os.remove(zipfn)
+ except OSError:
+ pass
+ try:
+ shutil.rmtree(unzipdir)
+ except OSError:
+ pass
+ return
+ else:
+ if deletezip:
+ os.remove(zipfn)
+ branch = UpdateAddon.getBranchFromFile(unzipdir)
+ installedbranch = xbmcaddon.Addon().getSetting('installedbranch')
+ if branch == '':
+ branch = installedbranch
+ if not isBackup:
+ if self.backup(self.addondir, self.backupdir, self.numbackups) is False:
+ UpdateAddon.notify(_('Backup failed, update aborted'), silent=silent)
+ return
+ else:
+ log(msg='Backup succeeded.')
+ archivedir = UpdateAddon.getAddonxmlPath(unzipdir)
+ addonisGHA = UpdateAddon.isGitHubArchive(self.addondir)
+ if os.path.isfile(os.path.join(archivedir, 'timestamp.json')) and not isBackup:
+ fd = UpdateAddon.loadfiledates(os.path.join(archivedir, 'timestamp.json'))
+ UpdateAddon.setfiledates(archivedir, fd)
+ log(msg='File timestamps updated')
+ if updateonly is None:
+ updateonly = True
+ ziptimestamped = True
+ else:
+ ziptimestamped = False
+ if updateonly is None:
+ updateonly = False
+ if updateonly is True and addonisGHA:
+ updateonly = False
+ if updateonly is True and ziptimestamped is False and isBackup is False:
+ updateonly = False
+ if installedbranch != branch:
+ updateonly = False
+ if archivedir != '':
+ try:
+ fc = copyToDir(archivedir, self.addondir, updateonly=updateonly, dryrun=dryrun)
+ except OSError as e:
+ UpdateAddon.notify(_('Error encountered copying to addon directory: %s') % str(e), silent=silent)
+ shutil.rmtree(unzipdir)
+ self.cleartemp(recreate=False)
+ else:
+ if installedbranch != branch:
+ xbmcaddon.Addon().setSetting('installedbranch', branch)
+ if len(fc) > 0:
+ self.cleartemp(recreate=False)
+ shutil.rmtree(unzipdir)
+ if silent is False:
+ if not isBackup:
+ msg = _('New version installed')
+ msg += _('\nPrevious installation backed up')
+ else:
+ msg = _('Backup restored')
+ UpdateAddon.notify(msg)
+ log(msg=_('The following files were updated: %s') % str(fc))
+ if not silent:
+ answer = UpdateAddon.prompt(_('Attempt to restart addon now?')) == True
+ else:
+ answer = True
+ if answer is True:
+ restartpath = translatepath('special://addon{%s)/restartaddon.py' % self.addonid)
+ if not os.path.isfile(restartpath):
+ self.createRestartPy(restartpath)
+ xbmc.executebuiltin('RunScript(%s, %s)' % (restartpath, self.addonid))
+ else:
+ UpdateAddon.notify(_('All files are current'), silent=silent)
+ else:
+ self.cleartemp(recreate=False)
+ shutil.rmtree(unzipdir)
+ UpdateAddon.notify(_('Could not find addon.xml\nInstallation aborted'), silent=silent)
+
+ @staticmethod
+ def getAddonxmlPath(path):
+ ret = ''
+ for root, __, files in os.walk(path):
+ if 'addon.xml' in files:
+ ret = root
+ break
+ return ret
+
+ @staticmethod
+ def getTS(strtime):
+ t_struct = time.strptime(strtime, '%Y-%m-%dT%H:%M:%SZ')
+ ret = time.mktime(t_struct)
+ return ret
+
+ @staticmethod
+ def setTime(path, strtime):
+ ts = UpdateAddon.getTS(strtime)
+ os.utime(path, (ts, ts))
+
+ @staticmethod
+ def loadfiledates(path):
+ if os.path.isfile(path):
+ with open(path, 'r') as f:
+ try:
+ ret = json.load(f)
+ except:
+ raise
+ else:
+ return ret
+ else:
+ return {}
+
+ @staticmethod
+ def setfiledates(rootpath, filedict):
+ for key in filedict.keys():
+ fl = key.split(r'/')
+ path = os.path.join(rootpath, *fl)
+ if os.path.isfile(path):
+ UpdateAddon.setTime(path, filedict[key])
+
+ @staticmethod
+ def createRestartPy(path):
+ output = []
+ output.append('import xbmc')
+ output.append('import sys')
+ output.append('addonid = sys.argv[1]')
+ output.append(
+ 'xbmc.executeJSONRPC(\'{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled", "params":{"addonid":"%s","enabled":"toggle"},"id":1}\' % addonid)')
+ output.append('xbmc.log(msg=\'***** Toggling addon enabled 1: %s\' % addonid)')
+ output.append('xbmc.sleep(1000)')
+ output.append(
+ 'xbmc.executeJSONRPC(\'{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled", "params":{"addonid":"%s","enabled":"toggle"},"id":1}\' % addonid)')
+ output.append('xbmc.log(msg=\'***** Toggling addon enabled 2: %s\' % addonid)')
+ output = '\n'.join(output)
+ with open(path, 'w') as f:
+ f.writelines(output)
+
+ @staticmethod
+ def getFileModTime(path):
+ return datetime.datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ @staticmethod
+ def createTimeStampJson(src, dst=None, ignore=None):
+ if ignore is None:
+ ignore = []
+ fd = {}
+ if dst is None:
+ dst = os.path.join(src, 'timestamp.json')
+ for root, __, files in os.walk(src):
+ for fn in files:
+ ffn = os.path.join(root, fn)
+ relpath = os.path.relpath(ffn, src).replace('\\', '/')
+ if not UpdateAddon.checkfilematch(relpath, ignore):
+ fd[relpath] = UpdateAddon.getFileModTime(ffn)
+ if os.path.dirname(dst) == src:
+ fd[os.path.relpath(dst, src)] = strftime('%Y-%m-%dT%H:%M:%SZ')
+ with open(dst, 'w') as f:
+ json.dump(fd, f, ensure_ascii=False)
+
+ @staticmethod
+ def isGitHubArchive(path):
+ filelist = []
+ vals = []
+ ignoreDirs = ['.git', '.idea']
+ ignoreExts = ['.pyo', '.pyc']
+ ignoredRoots = []
+ for root, dirs, files in os.walk(path):
+ dirName = os.path.basename(root)
+ if ignoreDirs.count(dirName) > 0:
+ ignoredRoots += [root]
+ continue
+ ignore = False
+ for ignoredRoot in ignoredRoots:
+ if root.startswith(ignoredRoot):
+ ignore = True
+ break
+ if ignore:
+ continue
+ # add files
+ for fn in files:
+ if os.path.splitext(fn)[1] not in ignoreExts:
+ vals.append(os.path.getmtime(os.path.join(root, fn)))
+ filelist.append(os.path.join(root, fn))
+ vals.sort()
+ vals = vals[5:-5]
+ n = len(vals)
+ mean = sum(vals) / n
+ stdev = ((sum((x - mean) ** 2 for x in vals)) / n) ** 0.5
+ if stdev / 60.0 < 1.0:
+ return True
+ else:
+ return False
+
+
+class ZipArchive(zipfile.ZipFile):
+ def __init__(self, *args, **kwargs):
+ zipfile.ZipFile.__init__(self, *args, **kwargs)
+
+ def addEmptyDir(self, path, baseToRemove="", inZipRoot=None):
+ inZipPath = os.path.relpath(path, baseToRemove)
+ if inZipPath == ".": # path == baseToRemove (but still root might be added
+ inZipPath = ""
+ if inZipRoot is not None:
+ inZipPath = os.path.join(inZipRoot, inZipPath)
+ if inZipPath == "": # nothing to add
+ return
+ zipInfo = zipfile.ZipInfo(os.path.join(inZipPath, ''))
+ self.writestr(zipInfo, '')
+
+ def addFile(self, filePath, baseToRemove="", inZipRoot=None):
+ inZipPath = os.path.relpath(filePath, baseToRemove)
+ if inZipRoot is not None:
+ inZipPath = os.path.join(inZipRoot, inZipPath)
+ self.write(filePath, inZipPath)
+
+ def addDir(self, path, baseToRemove="", ignoreDirs=None, inZipRoot=None):
+ if ignoreDirs is None:
+ ignoreDirs = []
+ ignoredRoots = []
+ for root, dirs, files in os.walk(path):
+ # ignore e.g. special folders
+ dirName = os.path.basename(root)
+ if ignoreDirs.count(dirName) > 0:
+ ignoredRoots += [root]
+ continue
+ # ignore descendants of folders ignored above
+ ignore = False
+ for ignoredRoot in ignoredRoots:
+ if root.startswith(ignoredRoot):
+ ignore = True
+ break
+ if ignore:
+ continue
+
+ # add dir itself (needed for empty dirs)
+ if len(files) <= 0:
+ self.addEmptyDir(root, baseToRemove, inZipRoot)
+
+ # add files
+ for fn in files:
+ self.addFile(os.path.join(root, fn), baseToRemove, inZipRoot)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/AUTHORS b/script.service.kodi.callbacks/resources/lib/watchdog/AUTHORS
new file mode 100644
index 0000000000..a4f4a49001
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/AUTHORS
@@ -0,0 +1,66 @@
+Project Lead:
+-------------
+Yesudeep Mangalapilly
+
+Contributors in alphabetical order:
+-----------------------------------
+Adrian Tejn Kern
+Andrew Schaaf
+David LaPalomento
+Filip Noetzel
+Gary van der Merwe
+Gora Khargosh
+Hannu Valtonen
+Jesse Printz
+Luke McCarthy
+Lukáš Lalinský
+Malthe Borch
+Martin Kreichgauer
+Martin Kreichgauer
+Mike Lundy
+Raymond Hettinger
+Roman Ovchinnikov
+Rotem Yaari
+Ryan Kelly
+Senko Rasic
+Senko Rašić
+Shane Hathaway
+Simon Pantzare
+Simon Pantzare
+Steven Samuel Cole
+Stéphane Klein
+Thomas Guest
+Thomas Heller
+Tim Cuthbertson
+Todd Whiteman
+Will McGugan
+Yesudeep Mangalapilly
+Yesudeep Mangalapilly
+dvogel
+gfxmonk
+
+
+We would like to thank these individuals for ideas:
+---------------------------------------------------
+Tim Golden
+Sebastien Martini
+
+Initially we used the flask theme for the documentation which was written by
+----------------------------------------------------------------------------
+Armin Ronacher
+
+
+Watchdog also includes open source libraries or adapted code
+from the following projects:
+
+- MacFSEvents - http://github.com/malthe/macfsevents
+- watch_directory.py - http://timgolden.me.uk/python/downloads/watch_directory.py
+- pyinotify - http://github.com/seb-m/pyinotify
+- fsmonitor - http://github.com/shaurz/fsmonitor
+- echo - http://wordaligned.org/articles/echo
+- Lukáš Lalinský's ordered set queue implementation:
+ http://stackoverflow.com/questions/1581895/how-check-if-a-task-is-already-in-python-queue
+- Armin Ronacher's flask-sphinx-themes for the documentation:
+ https://github.com/mitsuhiko/flask-sphinx-themes
+- pyfilesystem - http://code.google.com/p/pyfilesystem
+- get_FILE_NOTIFY_INFORMATION - http://blog.gmane.org/gmane.comp.python.ctypes/month=20070901
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/COPYING b/script.service.kodi.callbacks/resources/lib/watchdog/COPYING
new file mode 100644
index 0000000000..e6f091e8c5
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/COPYING
@@ -0,0 +1,14 @@
+Copyright 2011 Yesudeep Mangalapilly
+Copyright 2012 Google, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/LICENSE.txt b/script.service.kodi.callbacks/resources/lib/watchdog/LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/README.rst b/script.service.kodi.callbacks/resources/lib/watchdog/README.rst
new file mode 100644
index 0000000000..de3b726c03
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/README.rst
@@ -0,0 +1,276 @@
+Watchdog
+========
+Python API and shell utilities to monitor file system events.
+
+
+Example API Usage
+-----------------
+A simple program that uses watchdog to monitor directories specified
+as command-line arguments and logs events generated:
+
+.. code-block:: python
+
+ import sys
+ import time
+ import logging
+ from watchdog.observers import Observer
+ from watchdog.events import LoggingEventHandler
+
+ if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO,
+ format='%(asctime)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S')
+ path = sys.argv[1] if len(sys.argv) > 1 else '.'
+ event_handler = LoggingEventHandler()
+ observer = Observer()
+ observer.schedule(event_handler, path, recursive=True)
+ observer.start()
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ observer.stop()
+ observer.join()
+
+
+Shell Utilities
+---------------
+Watchdog comes with a utility script called ``watchmedo``.
+Please type ``watchmedo --help`` at the shell prompt to
+know more about this tool.
+
+Here is how you can log the current directory recursively
+for events related only to ``*.py`` and ``*.txt`` files while
+ignoring all directory events:
+
+.. code-block:: bash
+
+ watchmedo log \
+ --patterns="*.py;*.txt" \
+ --ignore-directories \
+ --recursive \
+ .
+
+You can use the ``shell-command`` subcommand to execute shell commands in
+response to events:
+
+.. code-block:: bash
+
+ watchmedo shell-command \
+ --patterns="*.py;*.txt" \
+ --recursive \
+ --command='echo "${watch_src_path}"' \
+ .
+
+Please see the help information for these commands by typing:
+
+.. code-block:: bash
+
+ watchmedo [command] --help
+
+
+About ``watchmedo`` Tricks
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+``watchmedo`` can read ``tricks.yaml`` files and execute tricks within them in
+response to file system events. Tricks are actually event handlers that
+subclass ``watchdog.tricks.Trick`` and are written by plugin authors. Trick
+classes are augmented with a few additional features that regular event handlers
+don't need.
+
+An example ``tricks.yaml`` file:
+
+.. code-block:: yaml
+
+ tricks:
+ - watchdog.tricks.LoggerTrick:
+ patterns: ["*.py", "*.js"]
+ - watchmedo_webtricks.GoogleClosureTrick:
+ patterns: ['*.js']
+ hash_names: true
+ mappings_format: json # json|yaml|python
+ mappings_module: app/javascript_mappings
+ suffix: .min.js
+ compilation_level: advanced # simple|advanced
+ source_directory: app/static/js/
+ destination_directory: app/public/js/
+ files:
+ index-page:
+ - app/static/js/vendor/jquery*.js
+ - app/static/js/base.js
+ - app/static/js/index-page.js
+ about-page:
+ - app/static/js/vendor/jquery*.js
+ - app/static/js/base.js
+ - app/static/js/about-page/**/*.js
+
+The directory containing the ``tricks.yaml`` file will be monitored. Each trick
+class is initialized with its corresponding keys in the ``tricks.yaml`` file as
+arguments and events are fed to an instance of this class as they arrive.
+
+Tricks will be included in the 0.5.0 release. I need community input about them.
+Please file enhancement requests at the `issue tracker`_.
+
+
+Installation
+------------
+Installing from PyPI using ``pip``:
+
+.. code-block:: bash
+
+ $ pip install watchdog
+
+Installing from PyPI using ``easy_install``:
+
+.. code-block:: bash
+
+ $ easy_install watchdog
+
+Installing from source:
+
+.. code-block:: bash
+
+ $ python setup.py install
+
+
+Installation Caveats
+~~~~~~~~~~~~~~~~~~~~
+The ``watchmedo`` script depends on PyYAML_ which links with LibYAML_,
+which brings a performance boost to the PyYAML parser. However, installing
+LibYAML_ is optional but recommended. On Mac OS X, you can use homebrew_
+to install LibYAML:
+
+.. code-block:: bash
+
+ $ brew install libyaml
+
+On Linux, use your favorite package manager to install LibYAML. Here's how you
+do it on Ubuntu:
+
+.. code-block:: bash
+
+ $ sudo aptitude install libyaml-dev
+
+On Windows, please install PyYAML_ using the binaries they provide.
+
+Documentation
+-------------
+You can browse the latest release documentation_ online.
+
+Contribute
+----------
+Fork the `repository`_ on GitHub and send a pull request, or file an issue
+ticket at the `issue tracker`_. For general help and questions use the official
+`mailing list`_ or ask on `stackoverflow`_ with tag `python-watchdog`.
+
+Create and activate your virtual environment, then::
+
+ pip install pytest
+ pip install -e .
+ py.test tests
+
+
+Supported Platforms
+-------------------
+* Linux 2.6 (inotify)
+* Mac OS X (FSEvents, kqueue)
+* FreeBSD/BSD (kqueue)
+* Windows (ReadDirectoryChangesW with I/O completion ports;
+ ReadDirectoryChangesW worker threads)
+* OS-independent (polling the disk for directory snapshots and comparing them
+ periodically; slow and not recommended)
+
+Note that when using watchdog with kqueue, you need the
+number of file descriptors allowed to be opened by programs
+running on your system to be increased to more than the
+number of files that you will be monitoring. The easiest way
+to do that is to edit your ``~/.profile`` file and add
+a line similar to::
+
+ ulimit -n 1024
+
+This is an inherent problem with kqueue because it uses
+file descriptors to monitor files. That plus the enormous
+amount of bookkeeping that watchdog needs to do in order
+to monitor file descriptors just makes this a painful way
+to monitor files and directories. In essence, kqueue is
+not a very scalable way to monitor a deeply nested
+directory of files and directories with a large number of
+files.
+
+About using watchdog with editors like Vim
+------------------------------------------
+Vim does not modify files unless directed to do so.
+It creates backup files and then swaps them in to replace
+the files you are editing on the disk. This means that
+if you use Vim to edit your files, the on-modified events
+for those files will not be triggered by watchdog.
+You may need to configure Vim to appropriately to disable
+this feature.
+
+
+Dependencies
+------------
+1. Python 2.6 or above.
+2. pathtools_
+3. select_backport_ (select.kqueue replacement for 2.6 on BSD/Mac OS X)
+4. XCode_ (only on Mac OS X)
+5. PyYAML_ (only for ``watchmedo`` script)
+6. argh_ (only for ``watchmedo`` script)
+
+
+Licensing
+---------
+Watchdog is licensed under the terms of the `Apache License, version 2.0`_.
+
+Copyright 2011 `Yesudeep Mangalapilly`_.
+
+Copyright 2012 Google, Inc.
+
+Project `source code`_ is available at Github. Please report bugs and file
+enhancement requests at the `issue tracker`_.
+
+Why Watchdog?
+-------------
+Too many people tried to do the same thing and none did what I needed Python
+to do:
+
+* pnotify_
+* `unison fsmonitor`_
+* fsmonitor_
+* guard_
+* pyinotify_
+* `inotify-tools`_
+* jnotify_
+* treewalker_
+* `file.monitor`_
+* pyfilesystem_
+
+.. links:
+.. _Yesudeep Mangalapilly: yesudeep@gmail.com
+.. _source code: http://github.com/gorakhargosh/watchdog
+.. _issue tracker: http://github.com/gorakhargosh/watchdog/issues
+.. _Apache License, version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+.. _documentation: http://packages.python.org/watchdog/
+.. _stackoverflow: http://stackoverflow.com/questions/tagged/python-watchdog
+.. _mailing list: http://groups.google.com/group/watchdog-python
+.. _repository: http://github.com/gorakhargosh/watchdog
+.. _issue tracker: http://github.com/gorakhargosh/watchdog/issues
+
+.. _homebrew: http://mxcl.github.com/homebrew/
+.. _select_backport: http://pypi.python.org/pypi/select_backport
+.. _argh: http://pypi.python.org/pypi/argh
+.. _PyYAML: http://www.pyyaml.org/
+.. _XCode: http://developer.apple.com/technologies/tools/xcode.html
+.. _LibYAML: http://pyyaml.org/wiki/LibYAML
+.. _pathtools: http://github.com/gorakhargosh/pathtools
+
+.. _pnotify: http://mark.heily.com/pnotify
+.. _unison fsmonitor: https://webdav.seas.upenn.edu/viewvc/unison/trunk/src/fsmonitor.py?view=markup&pathrev=471
+.. _fsmonitor: http://github.com/shaurz/fsmonitor
+.. _guard: http://github.com/guard/guard
+.. _pyinotify: http://github.com/seb-m/pyinotify
+.. _inotify-tools: http://github.com/rvoicilas/inotify-tools
+.. _jnotify: http://jnotify.sourceforge.net/
+.. _treewalker: http://github.com/jbd/treewatcher
+.. _file.monitor: http://github.com/pke/file.monitor
+.. _pyfilesystem: http://code.google.com/p/pyfilesystem
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/__init__.py b/script.service.kodi.callbacks/resources/lib/watchdog/__init__.py
new file mode 100644
index 0000000000..1a641ff98f
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/__init__.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/events.py b/script.service.kodi.callbacks/resources/lib/watchdog/events.py
new file mode 100644
index 0000000000..7c1f075096
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/events.py
@@ -0,0 +1,615 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.events
+:synopsis: File system events and event handlers.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+
+Event Classes
+-------------
+.. autoclass:: FileSystemEvent
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+.. autoclass:: FileSystemMovedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: FileMovedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: DirMovedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: FileModifiedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: DirModifiedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: FileCreatedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: DirCreatedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: FileDeletedEvent
+ :members:
+ :show-inheritance:
+
+.. autoclass:: DirDeletedEvent
+ :members:
+ :show-inheritance:
+
+
+Event Handler Classes
+---------------------
+.. autoclass:: FileSystemEventHandler
+ :members:
+ :show-inheritance:
+
+.. autoclass:: PatternMatchingEventHandler
+ :members:
+ :show-inheritance:
+
+.. autoclass:: RegexMatchingEventHandler
+ :members:
+ :show-inheritance:
+
+.. autoclass:: LoggingEventHandler
+ :members:
+ :show-inheritance:
+
+"""
+
+import os.path
+import logging
+import re
+from pathtools.patterns import match_any_paths
+from watchdog.utils import has_attribute
+from watchdog.utils import unicode_paths
+
+
+EVENT_TYPE_MOVED = 'moved'
+EVENT_TYPE_DELETED = 'deleted'
+EVENT_TYPE_CREATED = 'created'
+EVENT_TYPE_MODIFIED = 'modified'
+
+
+class FileSystemEvent(object):
+ """
+ Immutable type that represents a file system event that is triggered
+ when a change occurs on the monitored file system.
+
+ All FileSystemEvent objects are required to be immutable and hence
+ can be used as keys in dictionaries or be added to sets.
+ """
+
+ event_type = None
+ """The type of the event as a string."""
+
+ is_directory = False
+ """True if event was emitted for a directory; False otherwise."""
+
+ def __init__(self, src_path):
+ self._src_path = src_path
+
+ @property
+ def src_path(self):
+ """Source path of the file system object that triggered this event."""
+ return self._src_path
+
+ def __str__(self):
+ return self.__repr__()
+
+ def __repr__(self):
+ return ("<%(class_name)s: event_type=%(event_type)s, "
+ "src_path=%(src_path)r, "
+ "is_directory=%(is_directory)s>"
+ ) % (dict(
+ class_name=self.__class__.__name__,
+ event_type=self.event_type,
+ src_path=self.src_path,
+ is_directory=self.is_directory))
+
+ # Used for comparison of events.
+ @property
+ def key(self):
+ return (self.event_type, self.src_path, self.is_directory)
+
+ def __eq__(self, event):
+ return self.key == event.key
+
+ def __ne__(self, event):
+ return self.key != event.key
+
+ def __hash__(self):
+ return hash(self.key)
+
+
+class FileSystemMovedEvent(FileSystemEvent):
+ """
+ File system event representing any kind of file system movement.
+ """
+
+ event_type = EVENT_TYPE_MOVED
+
+ def __init__(self, src_path, dest_path):
+ super(FileSystemMovedEvent, self).__init__(src_path)
+ self._dest_path = dest_path
+
+ @property
+ def dest_path(self):
+ """The destination path of the move event."""
+ return self._dest_path
+
+ # Used for hashing this as an immutable object.
+ @property
+ def key(self):
+ return (self.event_type, self.src_path, self.dest_path, self.is_directory)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r, "
+ "dest_path=%(dest_path)r, "
+ "is_directory=%(is_directory)s>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path,
+ dest_path=self.dest_path,
+ is_directory=self.is_directory))
+
+
+# File events.
+
+
+class FileDeletedEvent(FileSystemEvent):
+ """File system event representing file deletion on the file system."""
+
+ event_type = EVENT_TYPE_DELETED
+
+ def __init__(self, src_path):
+ super(FileDeletedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return "<%(class_name)s: src_path=%(src_path)r>" %\
+ dict(class_name=self.__class__.__name__,
+ src_path=self.src_path)
+
+
+class FileModifiedEvent(FileSystemEvent):
+ """File system event representing file modification on the file system."""
+
+ event_type = EVENT_TYPE_MODIFIED
+
+ def __init__(self, src_path):
+ super(FileModifiedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path))
+
+
+class FileCreatedEvent(FileSystemEvent):
+ """File system event representing file creation on the file system."""
+
+ event_type = EVENT_TYPE_CREATED
+
+ def __init__(self, src_path):
+ super(FileCreatedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path))
+
+
+class FileMovedEvent(FileSystemMovedEvent):
+ """File system event representing file movement on the file system."""
+
+ def __init__(self, src_path, dest_path):
+ super(FileMovedEvent, self).__init__(src_path, dest_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r, "
+ "dest_path=%(dest_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path,
+ dest_path=self.dest_path))
+
+
+# Directory events.
+
+
+class DirDeletedEvent(FileSystemEvent):
+ """File system event representing directory deletion on the file system."""
+
+ event_type = EVENT_TYPE_DELETED
+ is_directory = True
+
+ def __init__(self, src_path):
+ super(DirDeletedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path))
+
+
+class DirModifiedEvent(FileSystemEvent):
+ """
+ File system event representing directory modification on the file system.
+ """
+
+ event_type = EVENT_TYPE_MODIFIED
+ is_directory = True
+
+ def __init__(self, src_path):
+ super(DirModifiedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path))
+
+
+class DirCreatedEvent(FileSystemEvent):
+ """File system event representing directory creation on the file system."""
+
+ event_type = EVENT_TYPE_CREATED
+ is_directory = True
+
+ def __init__(self, src_path):
+ super(DirCreatedEvent, self).__init__(src_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path))
+
+
+class DirMovedEvent(FileSystemMovedEvent):
+ """File system event representing directory movement on the file system."""
+
+ is_directory = True
+
+ def __init__(self, src_path, dest_path):
+ super(DirMovedEvent, self).__init__(src_path, dest_path)
+
+ def __repr__(self):
+ return ("<%(class_name)s: src_path=%(src_path)r, "
+ "dest_path=%(dest_path)r>"
+ ) % (dict(class_name=self.__class__.__name__,
+ src_path=self.src_path,
+ dest_path=self.dest_path))
+
+
+class FileSystemEventHandler(object):
+ """
+ Base file system event handler that you can override methods from.
+ """
+
+ def dispatch(self, event):
+ """Dispatches events to the appropriate methods.
+
+ :param event:
+ The event object representing the file system event.
+ :type event:
+ :class:`FileSystemEvent`
+ """
+ self.on_any_event(event)
+ _method_map = {
+ EVENT_TYPE_MODIFIED: self.on_modified,
+ EVENT_TYPE_MOVED: self.on_moved,
+ EVENT_TYPE_CREATED: self.on_created,
+ EVENT_TYPE_DELETED: self.on_deleted,
+ }
+ event_type = event.event_type
+ _method_map[event_type](event)
+
+ def on_any_event(self, event):
+ """Catch-all event handler.
+
+ :param event:
+ The event object representing the file system event.
+ :type event:
+ :class:`FileSystemEvent`
+ """
+
+ def on_moved(self, event):
+ """Called when a file or a directory is moved or renamed.
+
+ :param event:
+ Event representing file/directory movement.
+ :type event:
+ :class:`DirMovedEvent` or :class:`FileMovedEvent`
+ """
+
+ def on_created(self, event):
+ """Called when a file or directory is created.
+
+ :param event:
+ Event representing file/directory creation.
+ :type event:
+ :class:`DirCreatedEvent` or :class:`FileCreatedEvent`
+ """
+
+ def on_deleted(self, event):
+ """Called when a file or directory is deleted.
+
+ :param event:
+ Event representing file/directory deletion.
+ :type event:
+ :class:`DirDeletedEvent` or :class:`FileDeletedEvent`
+ """
+
+ def on_modified(self, event):
+ """Called when a file or directory is modified.
+
+ :param event:
+ Event representing file/directory modification.
+ :type event:
+ :class:`DirModifiedEvent` or :class:`FileModifiedEvent`
+ """
+
+
+class PatternMatchingEventHandler(FileSystemEventHandler):
+ """
+ Matches given patterns with file paths associated with occurring events.
+ """
+
+ def __init__(self, patterns=None, ignore_patterns=None,
+ ignore_directories=False, case_sensitive=False):
+ super(PatternMatchingEventHandler, self).__init__()
+
+ self._patterns = patterns
+ self._ignore_patterns = ignore_patterns
+ self._ignore_directories = ignore_directories
+ self._case_sensitive = case_sensitive
+
+ @property
+ def patterns(self):
+ """
+ (Read-only)
+ Patterns to allow matching event paths.
+ """
+ return self._patterns
+
+ @property
+ def ignore_patterns(self):
+ """
+ (Read-only)
+ Patterns to ignore matching event paths.
+ """
+ return self._ignore_patterns
+
+ @property
+ def ignore_directories(self):
+ """
+ (Read-only)
+ ``True`` if directories should be ignored; ``False`` otherwise.
+ """
+ return self._ignore_directories
+
+ @property
+ def case_sensitive(self):
+ """
+ (Read-only)
+ ``True`` if path names should be matched sensitive to case; ``False``
+ otherwise.
+ """
+ return self._case_sensitive
+
+ def dispatch(self, event):
+ """Dispatches events to the appropriate methods.
+
+ :param event:
+ The event object representing the file system event.
+ :type event:
+ :class:`FileSystemEvent`
+ """
+ if self.ignore_directories and event.is_directory:
+ return
+
+ paths = []
+ if has_attribute(event, 'dest_path'):
+ paths.append(unicode_paths.decode(event.dest_path))
+ if event.src_path:
+ paths.append(unicode_paths.decode(event.src_path))
+
+ if match_any_paths(paths,
+ included_patterns=self.patterns,
+ excluded_patterns=self.ignore_patterns,
+ case_sensitive=self.case_sensitive):
+ self.on_any_event(event)
+ _method_map = {
+ EVENT_TYPE_MODIFIED: self.on_modified,
+ EVENT_TYPE_MOVED: self.on_moved,
+ EVENT_TYPE_CREATED: self.on_created,
+ EVENT_TYPE_DELETED: self.on_deleted,
+ }
+ event_type = event.event_type
+ _method_map[event_type](event)
+
+
+class RegexMatchingEventHandler(FileSystemEventHandler):
+ """
+ Matches given regexes with file paths associated with occurring events.
+ """
+
+ def __init__(self, regexes=[r".*"], ignore_regexes=[],
+ ignore_directories=False, case_sensitive=False):
+ super(RegexMatchingEventHandler, self).__init__()
+
+ if case_sensitive:
+ self._regexes = [re.compile(r) for r in regexes]
+ self._ignore_regexes = [re.compile(r) for r in ignore_regexes]
+ else:
+ self._regexes = [re.compile(r, re.I) for r in regexes]
+ self._ignore_regexes = [re.compile(r, re.I) for r in ignore_regexes]
+ self._ignore_directories = ignore_directories
+ self._case_sensitive = case_sensitive
+
+ @property
+ def regexes(self):
+ """
+ (Read-only)
+ Regexes to allow matching event paths.
+ """
+ return self._regexes
+
+ @property
+ def ignore_regexes(self):
+ """
+ (Read-only)
+ Regexes to ignore matching event paths.
+ """
+ return self._ignore_regexes
+
+ @property
+ def ignore_directories(self):
+ """
+ (Read-only)
+ ``True`` if directories should be ignored; ``False`` otherwise.
+ """
+ return self._ignore_directories
+
+ @property
+ def case_sensitive(self):
+ """
+ (Read-only)
+ ``True`` if path names should be matched sensitive to case; ``False``
+ otherwise.
+ """
+ return self._case_sensitive
+
+ def dispatch(self, event):
+ """Dispatches events to the appropriate methods.
+
+ :param event:
+ The event object representing the file system event.
+ :type event:
+ :class:`FileSystemEvent`
+ """
+ if self.ignore_directories and event.is_directory:
+ return
+
+ paths = []
+ if has_attribute(event, 'dest_path'):
+ paths.append(unicode_paths.decode(event.dest_path))
+ if event.src_path:
+ paths.append(unicode_paths.decode(event.src_path))
+
+ if any(r.match(p) for r in self.ignore_regexes for p in paths):
+ return
+
+ if any(r.match(p) for r in self.regexes for p in paths):
+ self.on_any_event(event)
+ _method_map = {
+ EVENT_TYPE_MODIFIED: self.on_modified,
+ EVENT_TYPE_MOVED: self.on_moved,
+ EVENT_TYPE_CREATED: self.on_created,
+ EVENT_TYPE_DELETED: self.on_deleted,
+ }
+ event_type = event.event_type
+ _method_map[event_type](event)
+
+
+class LoggingEventHandler(FileSystemEventHandler):
+ """Logs all the events captured."""
+
+ def on_moved(self, event):
+ super(LoggingEventHandler, self).on_moved(event)
+
+ what = 'directory' if event.is_directory else 'file'
+ logging.info("Moved %s: from %s to %s", what, event.src_path,
+ event.dest_path)
+
+ def on_created(self, event):
+ super(LoggingEventHandler, self).on_created(event)
+
+ what = 'directory' if event.is_directory else 'file'
+ logging.info("Created %s: %s", what, event.src_path)
+
+ def on_deleted(self, event):
+ super(LoggingEventHandler, self).on_deleted(event)
+
+ what = 'directory' if event.is_directory else 'file'
+ logging.info("Deleted %s: %s", what, event.src_path)
+
+ def on_modified(self, event):
+ super(LoggingEventHandler, self).on_modified(event)
+
+ what = 'directory' if event.is_directory else 'file'
+ logging.info("Modified %s: %s", what, event.src_path)
+
+
+class LoggingFileSystemEventHandler(LoggingEventHandler):
+ """
+ For backwards-compatibility. Please use :class:`LoggingEventHandler`
+ instead.
+ """
+
+
+def generate_sub_moved_events(src_dir_path, dest_dir_path):
+ """Generates an event list of :class:`DirMovedEvent` and
+ :class:`FileMovedEvent` objects for all the files and directories within
+ the given moved directory that were moved along with the directory.
+
+ :param src_dir_path:
+ The source path of the moved directory.
+ :param dest_dir_path:
+ The destination path of the moved directory.
+ :returns:
+ An iterable of file system events of type :class:`DirMovedEvent` and
+ :class:`FileMovedEvent`.
+ """
+ for root, directories, filenames in os.walk(dest_dir_path):
+ for directory in directories:
+ full_path = os.path.join(root, directory)
+ renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else None
+ yield DirMovedEvent(renamed_path, full_path)
+ for filename in filenames:
+ full_path = os.path.join(root, filename)
+ renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else None
+ yield FileMovedEvent(renamed_path, full_path)
+
+
+def generate_sub_created_events(src_dir_path):
+ """Generates an event list of :class:`DirCreatedEvent` and
+ :class:`FileCreatedEvent` objects for all the files and directories within
+ the given moved directory that were moved along with the directory.
+
+ :param src_dir_path:
+ The source path of the created directory.
+ :returns:
+ An iterable of file system events of type :class:`DirCreatedEvent` and
+ :class:`FileCreatedEvent`.
+ """
+ for root, directories, filenames in os.walk(src_dir_path):
+ for directory in directories:
+ yield DirCreatedEvent(os.path.join(root, directory))
+ for filename in filenames:
+ yield FileCreatedEvent(os.path.join(root, filename))
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/__init__.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/__init__.py
new file mode 100644
index 0000000000..d38efc8a73
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/__init__.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.observers
+:synopsis: Observer that picks a native implementation if available.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+
+
+Classes
+=======
+.. autoclass:: Observer
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+Observer thread that schedules watching directories and dispatches
+calls to event handlers.
+
+You can also import platform specific classes directly and use it instead
+of :class:`Observer`. Here is a list of implemented observer classes.:
+
+============== ================================ ==============================
+Class Platforms Note
+============== ================================ ==============================
+|Inotify| Linux 2.6.13+ ``inotify(7)`` based observer
+|FSEvents| Mac OS X FSEvents based observer
+|Kqueue| Mac OS X and BSD with kqueue(2) ``kqueue(2)`` based observer
+|WinApi| MS Windows Windows API-based observer
+|Polling| Any fallback implementation
+============== ================================ ==============================
+
+.. |Inotify| replace:: :class:`.inotify.InotifyObserver`
+.. |FSEvents| replace:: :class:`.fsevents.FSEventsObserver`
+.. |Kqueue| replace:: :class:`.kqueue.KqueueObserver`
+.. |WinApi| replace:: :class:`.read_directory_changes.WindowsApiObserver`
+.. |WinApiAsync| replace:: :class:`.read_directory_changes_async.WindowsApiAsyncObserver`
+.. |Polling| replace:: :class:`.polling.PollingObserver`
+
+"""
+
+import warnings
+try:
+ from watchdog.utils import platform
+except:
+ from resources.lib.watchdog.utils import platform
+try:
+ from watchdog.utils import UnsupportedLibc
+except:
+ from resources.lib.watchdog.utils import UnsupportedLibc
+
+if platform.is_linux():
+ try:
+ from .inotify import InotifyObserver as Observer
+ except UnsupportedLibc:
+ from .polling import PollingObserver as Observer
+
+elif platform.is_darwin():
+ # FIXME: catching too broad. Error prone
+ try:
+ from .fsevents import FSEventsObserver as Observer
+ except:
+ try:
+ from .kqueue import KqueueObserver as Observer
+ warnings.warn("Failed to import fsevents. Fall back to kqueue")
+ except:
+ from .polling import PollingObserver as Observer
+ warnings.warn("Failed to import fsevents and kqueue. Fall back to polling.")
+
+elif platform.is_bsd():
+ from .kqueue import KqueueObserver as Observer
+
+elif platform.is_windows():
+ # TODO: find a reliable way of checking Windows version and import
+ # polling explicitly for Windows XP
+ try:
+ from .read_directory_changes import WindowsApiObserver as Observer
+ except:
+ from .polling import PollingObserver as Observer
+ warnings.warn("Failed to import read_directory_changes. Fall back to polling.")
+
+else:
+ from .polling import PollingObserver as Observer
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/api.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/api.py
new file mode 100644
index 0000000000..30f2e9a35d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/api.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import with_statement
+import threading
+from watchdog.utils import BaseThread
+from watchdog.utils.compat import queue
+from watchdog.utils.bricks import SkipRepeatsQueue
+
+DEFAULT_EMITTER_TIMEOUT = 1 # in seconds.
+DEFAULT_OBSERVER_TIMEOUT = 1 # in seconds.
+
+
+# Collection classes
+class EventQueue(SkipRepeatsQueue):
+ """Thread-safe event queue based on a special queue that skips adding
+ the same event (:class:`FileSystemEvent`) multiple times consecutively.
+ Thus avoiding dispatching multiple event handling
+ calls when multiple identical events are produced quicker than an observer
+ can consume them.
+ """
+
+
+class ObservedWatch(object):
+ """An scheduled watch.
+
+ :param path:
+ Path string.
+ :param recursive:
+ ``True`` if watch is recursive; ``False`` otherwise.
+ """
+
+ def __init__(self, path, recursive):
+ self._path = path
+ self._is_recursive = recursive
+
+ @property
+ def path(self):
+ """The path that this watch monitors."""
+ return self._path
+
+ @property
+ def is_recursive(self):
+ """Determines whether subdirectories are watched for the path."""
+ return self._is_recursive
+
+ @property
+ def key(self):
+ return self.path, self.is_recursive
+
+ def __eq__(self, watch):
+ return self.key == watch.key
+
+ def __ne__(self, watch):
+ return self.key != watch.key
+
+ def __hash__(self):
+ return hash(self.key)
+
+ def __repr__(self):
+ return "" % (
+ self.path, self.is_recursive)
+
+
+# Observer classes
+class EventEmitter(BaseThread):
+ """
+ Producer thread base class subclassed by event emitters
+ that generate events and populate a queue with them.
+
+ :param event_queue:
+ The event queue to populate with generated events.
+ :type event_queue:
+ :class:`watchdog.events.EventQueue`
+ :param watch:
+ The watch to observe and produce events for.
+ :type watch:
+ :class:`ObservedWatch`
+ :param timeout:
+ Timeout (in seconds) between successive attempts at reading events.
+ :type timeout:
+ ``float``
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ BaseThread.__init__(self)
+ self._event_queue = event_queue
+ self._watch = watch
+ self._timeout = timeout
+
+ @property
+ def timeout(self):
+ """
+ Blocking timeout for reading events.
+ """
+ return self._timeout
+
+ @property
+ def watch(self):
+ """
+ The watch associated with this emitter.
+ """
+ return self._watch
+
+ def queue_event(self, event):
+ """
+ Queues a single event.
+
+ :param event:
+ Event to be queued.
+ :type event:
+ An instance of :class:`watchdog.events.FileSystemEvent`
+ or a subclass.
+ """
+ self._event_queue.put((event, self.watch))
+
+ def queue_events(self, timeout):
+ """Override this method to populate the event queue with events
+ per interval period.
+
+ :param timeout:
+ Timeout (in seconds) between successive attempts at
+ reading events.
+ :type timeout:
+ ``float``
+ """
+
+ def run(self):
+ try:
+ while self.should_keep_running():
+ self.queue_events(self.timeout)
+ finally:
+ pass
+
+
+class EventDispatcher(BaseThread):
+ """
+ Consumer thread base class subclassed by event observer threads
+ that dispatch events from an event queue to appropriate event handlers.
+
+ :param timeout:
+ Event queue blocking timeout (in seconds).
+ :type timeout:
+ ``float``
+ """
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseThread.__init__(self)
+ self._event_queue = EventQueue()
+ self._timeout = timeout
+
+ @property
+ def timeout(self):
+ """Event queue block timeout."""
+ return self._timeout
+
+ @property
+ def event_queue(self):
+ """The event queue which is populated with file system events
+ by emitters and from which events are dispatched by a dispatcher
+ thread."""
+ return self._event_queue
+
+ def dispatch_events(self, event_queue, timeout):
+ """Override this method to consume events from an event queue, blocking
+ on the queue for the specified timeout before raising :class:`queue.Empty`.
+
+ :param event_queue:
+ Event queue to populate with one set of events.
+ :type event_queue:
+ :class:`EventQueue`
+ :param timeout:
+ Interval period (in seconds) to wait before timing out on the
+ event queue.
+ :type timeout:
+ ``float``
+ :raises:
+ :class:`queue.Empty`
+ """
+
+ def run(self):
+ while self.should_keep_running():
+ try:
+ self.dispatch_events(self.event_queue, self.timeout)
+ except queue.Empty:
+ continue
+
+
+class BaseObserver(EventDispatcher):
+ """Base observer."""
+
+ def __init__(self, emitter_class, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ EventDispatcher.__init__(self, timeout)
+ self._emitter_class = emitter_class
+ self._lock = threading.RLock()
+ self._watches = set()
+ self._handlers = dict()
+ self._emitters = set()
+ self._emitter_for_watch = dict()
+
+ def _add_emitter(self, emitter):
+ self._emitter_for_watch[emitter.watch] = emitter
+ self._emitters.add(emitter)
+
+ def _remove_emitter(self, emitter):
+ del self._emitter_for_watch[emitter.watch]
+ self._emitters.remove(emitter)
+ emitter.stop()
+ try:
+ emitter.join()
+ except RuntimeError:
+ pass
+
+ def _clear_emitters(self):
+ for emitter in self._emitters:
+ emitter.stop()
+ for emitter in self._emitters:
+ try:
+ emitter.join()
+ except RuntimeError:
+ pass
+ self._emitters.clear()
+ self._emitter_for_watch.clear()
+
+ def _add_handler_for_watch(self, event_handler, watch):
+ if watch not in self._handlers:
+ self._handlers[watch] = set()
+ self._handlers[watch].add(event_handler)
+
+ def _remove_handlers_for_watch(self, watch):
+ del self._handlers[watch]
+
+ @property
+ def emitters(self):
+ """Returns event emitter created by this observer."""
+ return self._emitters
+
+ def start(self):
+ for emitter in self._emitters:
+ emitter.start()
+ super(BaseObserver, self).start()
+
+ def schedule(self, event_handler, path, recursive=False):
+ """
+ Schedules watching a path and calls appropriate methods specified
+ in the given event handler in response to file system events.
+
+ :param event_handler:
+ An event handler instance that has appropriate event handling
+ methods which will be called by the observer in response to
+ file system events.
+ :type event_handler:
+ :class:`watchdog.events.FileSystemEventHandler` or a subclass
+ :param path:
+ Directory path that will be monitored.
+ :type path:
+ ``str``
+ :param recursive:
+ ``True`` if events will be emitted for sub-directories
+ traversed recursively; ``False`` otherwise.
+ :type recursive:
+ ``bool``
+ :return:
+ An :class:`ObservedWatch` object instance representing
+ a watch.
+ """
+ with self._lock:
+ watch = ObservedWatch(path, recursive)
+ self._add_handler_for_watch(event_handler, watch)
+
+ # If we don't have an emitter for this watch already, create it.
+ if self._emitter_for_watch.get(watch) is None:
+ emitter = self._emitter_class(event_queue=self.event_queue,
+ watch=watch,
+ timeout=self.timeout)
+ self._add_emitter(emitter)
+ if self.is_alive():
+ emitter.start()
+ self._watches.add(watch)
+ return watch
+
+ def add_handler_for_watch(self, event_handler, watch):
+ """Adds a handler for the given watch.
+
+ :param event_handler:
+ An event handler instance that has appropriate event handling
+ methods which will be called by the observer in response to
+ file system events.
+ :type event_handler:
+ :class:`watchdog.events.FileSystemEventHandler` or a subclass
+ :param watch:
+ The watch to add a handler for.
+ :type watch:
+ An instance of :class:`ObservedWatch` or a subclass of
+ :class:`ObservedWatch`
+ """
+ with self._lock:
+ self._add_handler_for_watch(event_handler, watch)
+
+ def remove_handler_for_watch(self, event_handler, watch):
+ """Removes a handler for the given watch.
+
+ :param event_handler:
+ An event handler instance that has appropriate event handling
+ methods which will be called by the observer in response to
+ file system events.
+ :type event_handler:
+ :class:`watchdog.events.FileSystemEventHandler` or a subclass
+ :param watch:
+ The watch to remove a handler for.
+ :type watch:
+ An instance of :class:`ObservedWatch` or a subclass of
+ :class:`ObservedWatch`
+ """
+ with self._lock:
+ self._handlers[watch].remove(event_handler)
+
+ def unschedule(self, watch):
+ """Unschedules a watch.
+
+ :param watch:
+ The watch to unschedule.
+ :type watch:
+ An instance of :class:`ObservedWatch` or a subclass of
+ :class:`ObservedWatch`
+ """
+ with self._lock:
+ emitter = self._emitter_for_watch[watch]
+ del self._handlers[watch]
+ self._remove_emitter(emitter)
+ self._watches.remove(watch)
+
+ def unschedule_all(self):
+ """Unschedules all watches and detaches all associated event
+ handlers."""
+ with self._lock:
+ self._handlers.clear()
+ self._clear_emitters()
+ self._watches.clear()
+
+ def on_thread_stop(self):
+ self.unschedule_all()
+
+ def dispatch_events(self, event_queue, timeout):
+ event, watch = event_queue.get(block=True, timeout=timeout)
+
+ with self._lock:
+ # To allow unschedule/stop and safe removal of event handlers
+ # within event handlers itself, check if the handler is still
+ # registered after every dispatch.
+ for handler in list(self._handlers.get(watch, [])):
+ if handler in self._handlers.get(watch, []):
+ handler.dispatch(event)
+ event_queue.task_done()
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents.py
new file mode 100644
index 0000000000..6748148007
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.observers.fsevents
+:synopsis: FSEvents based emitter implementation.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+:platforms: Mac OS X
+"""
+
+from __future__ import with_statement
+
+import sys
+import threading
+import unicodedata
+import _watchdog_fsevents as _fsevents
+
+from watchdog.events import (
+ FileDeletedEvent,
+ FileModifiedEvent,
+ FileCreatedEvent,
+ FileMovedEvent,
+ DirDeletedEvent,
+ DirModifiedEvent,
+ DirCreatedEvent,
+ DirMovedEvent
+)
+
+from watchdog.utils.dirsnapshot import DirectorySnapshot
+from watchdog.observers.api import (
+ BaseObserver,
+ EventEmitter,
+ DEFAULT_EMITTER_TIMEOUT,
+ DEFAULT_OBSERVER_TIMEOUT
+)
+
+
+class FSEventsEmitter(EventEmitter):
+
+ """
+ Mac OS X FSEvents Emitter class.
+
+ :param event_queue:
+ The event queue to fill with events.
+ :param watch:
+ A watch object representing the directory to monitor.
+ :type watch:
+ :class:`watchdog.observers.api.ObservedWatch`
+ :param timeout:
+ Read events blocking timeout (in seconds).
+ :type timeout:
+ ``float``
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+ self._lock = threading.Lock()
+ self.snapshot = DirectorySnapshot(watch.path, watch.is_recursive)
+
+ def on_thread_stop(self):
+ _fsevents.remove_watch(self.watch)
+ _fsevents.stop(self)
+
+ def queue_events(self, timeout):
+ with self._lock:
+ if not self.watch.is_recursive\
+ and self.watch.path not in self.pathnames:
+ return
+ new_snapshot = DirectorySnapshot(self.watch.path,
+ self.watch.is_recursive)
+ events = new_snapshot - self.snapshot
+ self.snapshot = new_snapshot
+
+ # Files.
+ for src_path in events.files_deleted:
+ self.queue_event(FileDeletedEvent(src_path))
+ for src_path in events.files_modified:
+ self.queue_event(FileModifiedEvent(src_path))
+ for src_path in events.files_created:
+ self.queue_event(FileCreatedEvent(src_path))
+ for src_path, dest_path in events.files_moved:
+ self.queue_event(FileMovedEvent(src_path, dest_path))
+
+ # Directories.
+ for src_path in events.dirs_deleted:
+ self.queue_event(DirDeletedEvent(src_path))
+ for src_path in events.dirs_modified:
+ self.queue_event(DirModifiedEvent(src_path))
+ for src_path in events.dirs_created:
+ self.queue_event(DirCreatedEvent(src_path))
+ for src_path, dest_path in events.dirs_moved:
+ self.queue_event(DirMovedEvent(src_path, dest_path))
+
+ def run(self):
+ try:
+ def callback(pathnames, flags, emitter=self):
+ emitter.queue_events(emitter.timeout)
+
+ # for pathname, flag in zip(pathnames, flags):
+ # if emitter.watch.is_recursive: # and pathname != emitter.watch.path:
+ # new_sub_snapshot = DirectorySnapshot(pathname, True)
+ # old_sub_snapshot = self.snapshot.copy(pathname)
+ # diff = new_sub_snapshot - old_sub_snapshot
+ # self.snapshot += new_subsnapshot
+ # else:
+ # new_snapshot = DirectorySnapshot(emitter.watch.path, False)
+ # diff = new_snapshot - emitter.snapshot
+ # emitter.snapshot = new_snapshot
+
+ # INFO: FSEvents reports directory notifications recursively
+ # by default, so we do not need to add subdirectory paths.
+ #pathnames = set([self.watch.path])
+ # if self.watch.is_recursive:
+ # for root, directory_names, _ in os.walk(self.watch.path):
+ # for directory_name in directory_names:
+ # full_path = absolute_path(
+ # os.path.join(root, directory_name))
+ # pathnames.add(full_path)
+ self.pathnames = [self.watch.path]
+ _fsevents.add_watch(self,
+ self.watch,
+ callback,
+ self.pathnames)
+ _fsevents.read_events(self)
+ except Exception as e:
+ pass
+
+
+class FSEventsObserver(BaseObserver):
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=FSEventsEmitter,
+ timeout=timeout)
+
+ def schedule(self, event_handler, path, recursive=False):
+ # Python 2/3 compat
+ try:
+ str_class = unicode
+ except NameError:
+ str_class = str
+
+ # Fix for issue #26: Trace/BPT error when given a unicode path
+ # string. https://github.com/gorakhargosh/watchdog/issues#issue/26
+ if isinstance(path, str_class):
+ #path = unicode(path, 'utf-8')
+ path = unicodedata.normalize('NFC', path)
+ # We only encode the path in Python 2 for backwards compatibility.
+ # On Python 3 we want the path to stay as unicode if possible for
+ # the sake of path matching not having to be rewritten to use the
+ # bytes API instead of strings. The _watchdog_fsevent.so code for
+ # Python 3 can handle both str and bytes paths, which is why we
+ # do not HAVE to encode it with Python 3. The Python 2 code in
+ # _watchdog_fsevents.so was not changed for the sake of backwards
+ # compatibility.
+ if sys.version_info < (3,):
+ path = path.encode('utf-8')
+ return BaseObserver.schedule(self, event_handler, path, recursive)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents2.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents2.py
new file mode 100644
index 0000000000..9b6ebcba71
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents2.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.observers.fsevents2
+:synopsis: FSEvents based emitter implementation.
+:platforms: Mac OS X
+"""
+
+import os
+import logging
+import unicodedata
+from threading import Thread
+from watchdog.utils.compat import queue
+
+from watchdog.events import (
+ FileDeletedEvent,
+ FileModifiedEvent,
+ FileCreatedEvent,
+ FileMovedEvent,
+ DirDeletedEvent,
+ DirModifiedEvent,
+ DirCreatedEvent,
+ DirMovedEvent
+)
+from watchdog.observers.api import (
+ BaseObserver,
+ EventEmitter,
+ DEFAULT_EMITTER_TIMEOUT,
+ DEFAULT_OBSERVER_TIMEOUT,
+)
+
+# pyobjc
+import AppKit
+from FSEvents import (
+ FSEventStreamCreate,
+ CFRunLoopGetCurrent,
+ FSEventStreamScheduleWithRunLoop,
+ FSEventStreamStart,
+ CFRunLoopRun,
+ CFRunLoopStop,
+ FSEventStreamStop,
+ FSEventStreamInvalidate,
+ FSEventStreamRelease,
+)
+
+from FSEvents import (
+ kCFAllocatorDefault,
+ kCFRunLoopDefaultMode,
+ kFSEventStreamEventIdSinceNow,
+ kFSEventStreamCreateFlagNoDefer,
+ kFSEventStreamCreateFlagFileEvents,
+ kFSEventStreamEventFlagItemCreated,
+ kFSEventStreamEventFlagItemRemoved,
+ kFSEventStreamEventFlagItemInodeMetaMod,
+ kFSEventStreamEventFlagItemRenamed,
+ kFSEventStreamEventFlagItemModified,
+ kFSEventStreamEventFlagItemFinderInfoMod,
+ kFSEventStreamEventFlagItemChangeOwner,
+ kFSEventStreamEventFlagItemXattrMod,
+ kFSEventStreamEventFlagItemIsFile,
+ kFSEventStreamEventFlagItemIsDir,
+ kFSEventStreamEventFlagItemIsSymlink,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class FSEventsQueue(Thread):
+ """ Low level FSEvents client. """
+
+ def __init__(self, path):
+ Thread.__init__(self)
+ self._queue = queue.Queue()
+ self._run_loop = None
+
+ if isinstance(path, bytes):
+ path = path.decode('utf-8')
+ self._path = unicodedata.normalize('NFC', path)
+
+ context = None
+ latency = 1.0
+ self._stream_ref = FSEventStreamCreate(
+ kCFAllocatorDefault, self._callback, context, [self._path],
+ kFSEventStreamEventIdSinceNow, latency,
+ kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents)
+ if self._stream_ref is None:
+ raise IOError("FSEvents. Could not create stream.")
+
+ def run(self):
+ pool = AppKit.NSAutoreleasePool.alloc().init()
+ self._run_loop = CFRunLoopGetCurrent()
+ FSEventStreamScheduleWithRunLoop(
+ self._stream_ref, self._run_loop, kCFRunLoopDefaultMode)
+ if not FSEventStreamStart(self._stream_ref):
+ FSEventStreamInvalidate(self._stream_ref)
+ FSEventStreamRelease(self._stream_ref)
+ raise IOError("FSEvents. Could not start stream.")
+
+ CFRunLoopRun()
+ FSEventStreamStop(self._stream_ref)
+ FSEventStreamInvalidate(self._stream_ref)
+ FSEventStreamRelease(self._stream_ref)
+ del pool
+ # Make sure waiting thread is notified
+ self._queue.put(None)
+
+ def stop(self):
+ if self._run_loop is not None:
+ CFRunLoopStop(self._run_loop)
+
+ def _callback(self, streamRef, clientCallBackInfo, numEvents, eventPaths, eventFlags, eventIDs):
+ events = [NativeEvent(path, flags, _id) for path, flags, _id in
+ zip(eventPaths, eventFlags, eventIDs)]
+ logger.debug("FSEvents callback. Got %d events:" % numEvents)
+ for e in events:
+ logger.debug(e)
+ self._queue.put(events)
+
+ def read_events(self):
+ """
+ Returns a list or one or more events, or None if there are no more
+ events to be read.
+ """
+ if not self.is_alive():
+ return None
+ return self._queue.get()
+
+
+class NativeEvent(object):
+ def __init__(self, path, flags, event_id):
+ self.path = path
+ self.flags = flags
+ self.event_id = event_id
+ self.is_created = bool(flags & kFSEventStreamEventFlagItemCreated)
+ self.is_removed = bool(flags & kFSEventStreamEventFlagItemRemoved)
+ self.is_renamed = bool(flags & kFSEventStreamEventFlagItemRenamed)
+ self.is_modified = bool(flags & kFSEventStreamEventFlagItemModified)
+ self.is_change_owner = bool(flags & kFSEventStreamEventFlagItemChangeOwner)
+ self.is_inode_meta_mod = bool(flags & kFSEventStreamEventFlagItemInodeMetaMod)
+ self.is_finder_info_mod = bool(flags & kFSEventStreamEventFlagItemFinderInfoMod)
+ self.is_xattr_mod = bool(flags & kFSEventStreamEventFlagItemXattrMod)
+ self.is_symlink = bool(flags & kFSEventStreamEventFlagItemIsSymlink)
+ self.is_directory = bool(flags & kFSEventStreamEventFlagItemIsDir)
+
+ @property
+ def _event_type(self):
+ if self.is_created: return "Created"
+ if self.is_removed: return "Removed"
+ if self.is_renamed: return "Renamed"
+ if self.is_modified: return "Modified"
+ if self.is_inode_meta_mod: return "InodeMetaMod"
+ if self.is_xattr_mod: return "XattrMod"
+ return "Unknown"
+
+ def __repr__(self):
+ s =""
+ return s % (repr(self.path), self._event_type, self.is_directory, hex(self.flags), self.event_id)
+
+
+class FSEventsEmitter(EventEmitter):
+ """
+ FSEvents based event emitter. Handles conversion of native events.
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+ self._fsevents = FSEventsQueue(watch.path)
+ self._fsevents.start()
+
+ def on_thread_stop(self):
+ self._fsevents.stop()
+
+ def queue_events(self, timeout):
+ events = self._fsevents.read_events()
+ if events is None:
+ return
+ i = 0
+ while i < len(events):
+ event = events[i]
+
+ # For some reason the create and remove flags are sometimes also
+ # set for rename and modify type events, so let those take
+ # precedence.
+ if event.is_renamed:
+ # Internal moves appears to always be consecutive in the same
+ # buffer and have IDs differ by exactly one (while others
+ # don't) making it possible to pair up the two events coming
+ # from a singe move operation. (None of this is documented!)
+ # Otherwise, guess whether file was moved in or out.
+ #TODO: handle id wrapping
+ if (i+1 < len(events) and events[i+1].is_renamed and
+ events[i+1].event_id == event.event_id + 1):
+ cls = DirMovedEvent if event.is_directory else FileMovedEvent
+ self.queue_event(cls(event.path, events[i+1].path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(event.path)))
+ self.queue_event(DirModifiedEvent(os.path.dirname(events[i+1].path)))
+ i += 1
+ elif os.path.exists(event.path):
+ cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
+ self.queue_event(cls(event.path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(event.path)))
+ else:
+ cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
+ self.queue_event(cls(event.path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(event.path)))
+ #TODO: generate events for tree
+
+ elif event.is_modified or event.is_inode_meta_mod or event.is_xattr_mod :
+ cls = DirModifiedEvent if event.is_directory else FileModifiedEvent
+ self.queue_event(cls(event.path))
+
+ elif event.is_created:
+ cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
+ self.queue_event(cls(event.path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(event.path)))
+
+ elif event.is_removed:
+ cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
+ self.queue_event(cls(event.path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(event.path)))
+ i += 1
+
+
+class FSEventsObserver2(BaseObserver):
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=FSEventsEmitter, timeout=timeout)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify.py
new file mode 100644
index 0000000000..65fa7d51cd
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.observers.inotify
+:synopsis: ``inotify(7)`` based emitter implementation.
+:author: Sebastien Martini
+:author: Luke McCarthy
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+:author: Tim Cuthbertson
+:platforms: Linux 2.6.13+.
+
+.. ADMONITION:: About system requirements
+
+ Recommended minimum kernel version: 2.6.25.
+
+ Quote from the inotify(7) man page:
+
+ "Inotify was merged into the 2.6.13 Linux kernel. The required library
+ interfaces were added to glibc in version 2.4. (IN_DONT_FOLLOW,
+ IN_MASK_ADD, and IN_ONLYDIR were only added in version 2.5.)"
+
+ Therefore, you must ensure the system is running at least these versions
+ appropriate libraries and the kernel.
+
+.. ADMONITION:: About recursiveness, event order, and event coalescing
+
+ Quote from the inotify(7) man page:
+
+ If successive output inotify events produced on the inotify file
+ descriptor are identical (same wd, mask, cookie, and name) then they
+ are coalesced into a single event if the older event has not yet been
+ read (but see BUGS).
+
+ The events returned by reading from an inotify file descriptor form
+ an ordered queue. Thus, for example, it is guaranteed that when
+ renaming from one directory to another, events will be produced in
+ the correct order on the inotify file descriptor.
+
+ ...
+
+ Inotify monitoring of directories is not recursive: to monitor
+ subdirectories under a directory, additional watches must be created.
+
+ This emitter implementation therefore automatically adds watches for
+ sub-directories if running in recursive mode.
+
+Some extremely useful articles and documentation:
+
+.. _inotify FAQ: http://inotify.aiken.cz/?section=inotify&page=faq&lang=en
+.. _intro to inotify: http://www.linuxjournal.com/article/8478
+
+"""
+
+from __future__ import with_statement
+
+import os
+import threading
+from .inotify_buffer import InotifyBuffer
+
+from watchdog.observers.api import (
+ EventEmitter,
+ BaseObserver,
+ DEFAULT_EMITTER_TIMEOUT,
+ DEFAULT_OBSERVER_TIMEOUT
+)
+
+from watchdog.events import (
+ DirDeletedEvent,
+ DirModifiedEvent,
+ DirMovedEvent,
+ DirCreatedEvent,
+ FileDeletedEvent,
+ FileModifiedEvent,
+ FileMovedEvent,
+ FileCreatedEvent,
+ generate_sub_moved_events,
+ generate_sub_created_events,
+)
+from watchdog.utils import unicode_paths
+
+
+class InotifyEmitter(EventEmitter):
+ """
+ inotify(7)-based event emitter.
+
+ :param event_queue:
+ The event queue to fill with events.
+ :param watch:
+ A watch object representing the directory to monitor.
+ :type watch:
+ :class:`watchdog.observers.api.ObservedWatch`
+ :param timeout:
+ Read events blocking timeout (in seconds).
+ :type timeout:
+ ``float``
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+ self._lock = threading.Lock()
+ self._inotify = None
+
+ def on_thread_start(self):
+ path = unicode_paths.encode(self.watch.path)
+ self._inotify = InotifyBuffer(path, self.watch.is_recursive)
+
+ def on_thread_stop(self):
+ if self._inotify:
+ self._inotify.close()
+
+ def queue_events(self, timeout):
+ with self._lock:
+ event = self._inotify.read_event()
+ if event is None:
+ return
+ if isinstance(event, tuple):
+ move_from, move_to = event
+ src_path = self._decode_path(move_from.src_path)
+ dest_path = self._decode_path(move_to.src_path)
+ cls = DirMovedEvent if move_from.is_directory else FileMovedEvent
+ self.queue_event(cls(src_path, dest_path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
+ self.queue_event(DirModifiedEvent(os.path.dirname(dest_path)))
+ if move_from.is_directory and self.watch.is_recursive:
+ for sub_event in generate_sub_moved_events(src_path, dest_path):
+ self.queue_event(sub_event)
+ return
+
+ src_path = self._decode_path(event.src_path)
+ if event.is_moved_to:
+ cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
+ self.queue_event(cls(src_path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
+ if event.is_directory and self.watch.is_recursive:
+ for sub_event in generate_sub_created_events(src_path):
+ self.queue_event(sub_event)
+ elif event.is_attrib:
+ cls = DirModifiedEvent if event.is_directory else FileModifiedEvent
+ self.queue_event(cls(src_path))
+ elif event.is_modify:
+ cls = DirModifiedEvent if event.is_directory else FileModifiedEvent
+ self.queue_event(cls(src_path))
+ elif event.is_delete_self:
+ cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
+ self.queue_event(cls(src_path))
+ elif event.is_delete or event.is_moved_from:
+ cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
+ self.queue_event(cls(src_path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
+ elif event.is_create:
+ cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
+ self.queue_event(cls(src_path))
+ self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
+
+ def _decode_path(self, path):
+ """ Decode path only if unicode string was passed to this emitter. """
+ if isinstance(self.watch.path, bytes):
+ return path
+ return unicode_paths.decode(path)
+
+
+class InotifyObserver(BaseObserver):
+ """
+ Observer thread that schedules watching directories and dispatches
+ calls to event handlers.
+ """
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=InotifyEmitter,
+ timeout=timeout)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_buffer.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_buffer.py
new file mode 100644
index 0000000000..010bbb8ed9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_buffer.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+from watchdog.utils import BaseThread
+from watchdog.utils.delayed_queue import DelayedQueue
+from watchdog.observers.inotify_c import Inotify
+
+logger = logging.getLogger(__name__)
+
+
+class InotifyBuffer(BaseThread):
+ """A wrapper for `Inotify` that holds events for `delay` seconds. During
+ this time, IN_MOVED_FROM and IN_MOVED_TO events are paired.
+ """
+
+ delay = 0.5
+
+ def __init__(self, path, recursive=False):
+ BaseThread.__init__(self)
+ self._queue = DelayedQueue(self.delay)
+ self._inotify = Inotify(path, recursive)
+ self.start()
+
+ def read_event(self):
+ """Returns a single event or a tuple of from/to events in case of a
+ paired move event. If this buffer has been closed, immediately return
+ None.
+ """
+ return self._queue.get()
+
+ def on_thread_stop(self):
+ self._inotify.close()
+ self._queue.close()
+
+ def close(self):
+ self.stop()
+ self.join()
+
+ def run(self):
+ """Read event from `inotify` and add them to `queue`. When reading a
+ IN_MOVE_TO event, remove the previous added matching IN_MOVE_FROM event
+ and add them back to the queue as a tuple.
+ """
+ while self.should_keep_running():
+ inotify_events = self._inotify.read_events()
+ for inotify_event in inotify_events:
+ logger.debug("in-event %s", inotify_event)
+ if inotify_event.is_moved_to:
+
+ def matching_from_event(event):
+ return (not isinstance(event, tuple) and event.is_moved_from
+ and event.cookie == inotify_event.cookie)
+
+ from_event = self._queue.remove(matching_from_event)
+ if from_event is not None:
+ self._queue.put((from_event, inotify_event))
+ else:
+ logger.debug("could not find matching move_from event")
+ self._queue.put(inotify_event)
+ else:
+ self._queue.put(inotify_event)
+
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_c.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_c.py
new file mode 100644
index 0000000000..ec795cd29b
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_c.py
@@ -0,0 +1,564 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import with_statement
+import os
+import errno
+import struct
+import threading
+import ctypes
+import ctypes.util
+from functools import reduce
+from ctypes import c_int, c_char_p, c_uint32
+from watchdog.utils import has_attribute
+from watchdog.utils import UnsupportedLibc
+
+
+def _load_libc():
+ libc_path = None
+ try:
+ libc_path = ctypes.util.find_library('c')
+ except (OSError, IOError):
+ # Note: find_library will on some platforms raise these undocumented
+ # errors, e.g.on android IOError "No usable temporary directory found"
+ # will be raised.
+ pass
+
+ if libc_path is not None:
+ return ctypes.CDLL(libc_path)
+
+ # Fallbacks
+ try:
+ return ctypes.CDLL('libc.so')
+ except (OSError, IOError):
+ return ctypes.CDLL('libc.so.6')
+
+libc = _load_libc()
+
+if not has_attribute(libc, 'inotify_init') or \
+ not has_attribute(libc, 'inotify_add_watch') or \
+ not has_attribute(libc, 'inotify_rm_watch'):
+ raise UnsupportedLibc("Unsupported libc version found: %s" % libc._name)
+
+inotify_add_watch = ctypes.CFUNCTYPE(c_int, c_int, c_char_p, c_uint32, use_errno=True)(
+ ("inotify_add_watch", libc))
+
+inotify_rm_watch = ctypes.CFUNCTYPE(c_int, c_int, c_uint32, use_errno=True)(
+ ("inotify_rm_watch", libc))
+
+inotify_init = ctypes.CFUNCTYPE(c_int, use_errno=True)(
+ ("inotify_init", libc))
+
+
+class InotifyConstants(object):
+ # User-space events
+ IN_ACCESS = 0x00000001 # File was accessed.
+ IN_MODIFY = 0x00000002 # File was modified.
+ IN_ATTRIB = 0x00000004 # Meta-data changed.
+ IN_CLOSE_WRITE = 0x00000008 # Writable file was closed.
+ IN_CLOSE_NOWRITE = 0x00000010 # Unwritable file closed.
+ IN_OPEN = 0x00000020 # File was opened.
+ IN_MOVED_FROM = 0x00000040 # File was moved from X.
+ IN_MOVED_TO = 0x00000080 # File was moved to Y.
+ IN_CREATE = 0x00000100 # Subfile was created.
+ IN_DELETE = 0x00000200 # Subfile was deleted.
+ IN_DELETE_SELF = 0x00000400 # Self was deleted.
+ IN_MOVE_SELF = 0x00000800 # Self was moved.
+
+ # Helper user-space events.
+ IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE # Close.
+ IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO # Moves.
+
+ # Events sent by the kernel to a watch.
+ IN_UNMOUNT = 0x00002000 # Backing file system was unmounted.
+ IN_Q_OVERFLOW = 0x00004000 # Event queued overflowed.
+ IN_IGNORED = 0x00008000 # File was ignored.
+
+ # Special flags.
+ IN_ONLYDIR = 0x01000000 # Only watch the path if it's a directory.
+ IN_DONT_FOLLOW = 0x02000000 # Do not follow a symbolic link.
+ IN_EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects
+ IN_MASK_ADD = 0x20000000 # Add to the mask of an existing watch.
+ IN_ISDIR = 0x40000000 # Event occurred against directory.
+ IN_ONESHOT = 0x80000000 # Only send event once.
+
+ # All user-space events.
+ IN_ALL_EVENTS = reduce(
+ lambda x, y: x | y, [
+ IN_ACCESS,
+ IN_MODIFY,
+ IN_ATTRIB,
+ IN_CLOSE_WRITE,
+ IN_CLOSE_NOWRITE,
+ IN_OPEN,
+ IN_MOVED_FROM,
+ IN_MOVED_TO,
+ IN_DELETE,
+ IN_CREATE,
+ IN_DELETE_SELF,
+ IN_MOVE_SELF,
+ ])
+
+ # Flags for ``inotify_init1``
+ IN_CLOEXEC = 0x02000000
+ IN_NONBLOCK = 0x00004000
+
+
+# Watchdog's API cares only about these events.
+WATCHDOG_ALL_EVENTS = reduce(
+ lambda x, y: x | y, [
+ InotifyConstants.IN_MODIFY,
+ InotifyConstants.IN_ATTRIB,
+ InotifyConstants.IN_MOVED_FROM,
+ InotifyConstants.IN_MOVED_TO,
+ InotifyConstants.IN_CREATE,
+ InotifyConstants.IN_DELETE,
+ InotifyConstants.IN_DELETE_SELF,
+ InotifyConstants.IN_DONT_FOLLOW,
+ ])
+
+
+class inotify_event_struct(ctypes.Structure):
+ """
+ Structure representation of the inotify_event structure
+ (used in buffer size calculations)::
+
+ struct inotify_event {
+ __s32 wd; /* watch descriptor */
+ __u32 mask; /* watch mask */
+ __u32 cookie; /* cookie to synchronize two events */
+ __u32 len; /* length (including nulls) of name */
+ char name[0]; /* stub for possible name */
+ };
+ """
+ _fields_ = [('wd', c_int),
+ ('mask', c_uint32),
+ ('cookie', c_uint32),
+ ('len', c_uint32),
+ ('name', c_char_p)]
+
+
+EVENT_SIZE = ctypes.sizeof(inotify_event_struct)
+DEFAULT_NUM_EVENTS = 2048
+DEFAULT_EVENT_BUFFER_SIZE = DEFAULT_NUM_EVENTS * (EVENT_SIZE + 16)
+
+
+class Inotify(object):
+ """
+ Linux inotify(7) API wrapper class.
+
+ :param path:
+ The directory path for which we want an inotify object.
+ :type path:
+ :class:`bytes`
+ :param recursive:
+ ``True`` if subdirectories should be monitored; ``False`` otherwise.
+ """
+
+ def __init__(self, path, recursive=False, event_mask=WATCHDOG_ALL_EVENTS):
+ # The file descriptor associated with the inotify instance.
+ inotify_fd = inotify_init()
+ if inotify_fd == -1:
+ Inotify._raise_error()
+ self._inotify_fd = inotify_fd
+ self._lock = threading.Lock()
+
+ # Stores the watch descriptor for a given path.
+ self._wd_for_path = dict()
+ self._path_for_wd = dict()
+
+ self._path = path
+ self._event_mask = event_mask
+ self._is_recursive = recursive
+ self._add_dir_watch(path, recursive, event_mask)
+ self._moved_from_events = dict()
+
+ @property
+ def event_mask(self):
+ """The event mask for this inotify instance."""
+ return self._event_mask
+
+ @property
+ def path(self):
+ """The path associated with the inotify instance."""
+ return self._path
+
+ @property
+ def is_recursive(self):
+ """Whether we are watching directories recursively."""
+ return self._is_recursive
+
+ @property
+ def fd(self):
+ """The file descriptor associated with the inotify instance."""
+ return self._inotify_fd
+
+ def clear_move_records(self):
+ """Clear cached records of MOVED_FROM events"""
+ self._moved_from_events = dict()
+
+ def source_for_move(self, destination_event):
+ """
+ The source path corresponding to the given MOVED_TO event.
+
+ If the source path is outside the monitored directories, None
+ is returned instead.
+ """
+ if destination_event.cookie in self._moved_from_events:
+ return self._moved_from_events[destination_event.cookie].src_path
+ else:
+ return None
+
+ def remember_move_from_event(self, event):
+ """
+ Save this event as the source event for future MOVED_TO events to
+ reference.
+ """
+ self._moved_from_events[event.cookie] = event
+
+ def add_watch(self, path):
+ """
+ Adds a watch for the given path.
+
+ :param path:
+ Path to begin monitoring.
+ """
+ with self._lock:
+ self._add_watch(path, self._event_mask)
+
+ def remove_watch(self, path):
+ """
+ Removes a watch for the given path.
+
+ :param path:
+ Path string for which the watch will be removed.
+ """
+ with self._lock:
+ wd = self._remove_watch_bookkeeping(path)
+ if inotify_rm_watch(self._inotify_fd, wd) == -1:
+ Inotify._raise_error()
+
+ def close(self):
+ """
+ Closes the inotify instance and removes all associated watches.
+ """
+ with self._lock:
+ wd = self._wd_for_path[self._path]
+ inotify_rm_watch(self._inotify_fd, wd)
+ os.close(self._inotify_fd)
+
+ def read_events(self, event_buffer_size=DEFAULT_EVENT_BUFFER_SIZE):
+ """
+ Reads events from inotify and yields them.
+ """
+ # HACK: We need to traverse the directory path
+ # recursively and simulate events for newly
+ # created subdirectories/files. This will handle
+ # mkdir -p foobar/blah/bar; touch foobar/afile
+
+ def _recursive_simulate(src_path):
+ events = []
+ for root, dirnames, filenames in os.walk(src_path):
+ for dirname in dirnames:
+ try:
+ full_path = os.path.join(root, dirname)
+ wd_dir = self._add_watch(full_path, self._event_mask)
+ e = InotifyEvent(
+ wd_dir, InotifyConstants.IN_CREATE | InotifyConstants.IN_ISDIR, 0, dirname, full_path)
+ events.append(e)
+ except OSError:
+ pass
+ for filename in filenames:
+ full_path = os.path.join(root, filename)
+ wd_parent_dir = self._wd_for_path[os.path.dirname(full_path)]
+ e = InotifyEvent(
+ wd_parent_dir, InotifyConstants.IN_CREATE, 0, filename, full_path)
+ events.append(e)
+ return events
+
+ event_buffer = None
+ while True:
+ try:
+ event_buffer = os.read(self._inotify_fd, event_buffer_size)
+ except OSError as e:
+ if e.errno == errno.EINTR:
+ continue
+ break
+
+ with self._lock:
+ event_list = []
+ for wd, mask, cookie, name in Inotify._parse_event_buffer(event_buffer):
+ if wd == -1:
+ continue
+ wd_path = self._path_for_wd[wd]
+ src_path = os.path.join(wd_path, name) if name else wd_path #avoid trailing slash
+ inotify_event = InotifyEvent(wd, mask, cookie, name, src_path)
+
+ if inotify_event.is_moved_from:
+ self.remember_move_from_event(inotify_event)
+ elif inotify_event.is_moved_to:
+ move_src_path = self.source_for_move(inotify_event)
+ if move_src_path in self._wd_for_path:
+ moved_wd = self._wd_for_path[move_src_path]
+ del self._wd_for_path[move_src_path]
+ self._wd_for_path[inotify_event.src_path] = moved_wd
+ self._path_for_wd[moved_wd] = inotify_event.src_path
+ src_path = os.path.join(wd_path, name)
+ inotify_event = InotifyEvent(wd, mask, cookie, name, src_path)
+
+ if inotify_event.is_ignored:
+ # Clean up book-keeping for deleted watches.
+ self._remove_watch_bookkeeping(src_path)
+ continue
+
+ event_list.append(inotify_event)
+
+ if (self.is_recursive and inotify_event.is_directory and
+ inotify_event.is_create):
+
+ # TODO: When a directory from another part of the
+ # filesystem is moved into a watched directory, this
+ # will not generate events for the directory tree.
+ # We need to coalesce IN_MOVED_TO events and those
+ # IN_MOVED_TO events which don't pair up with
+ # IN_MOVED_FROM events should be marked IN_CREATE
+ # instead relative to this directory.
+ try:
+ self._add_watch(src_path, self._event_mask)
+ except OSError:
+ continue
+
+ event_list.extend(_recursive_simulate(src_path))
+
+ return event_list
+
+ # Non-synchronized methods.
+ def _add_dir_watch(self, path, recursive, mask):
+ """
+ Adds a watch (optionally recursively) for the given directory path
+ to monitor events specified by the mask.
+
+ :param path:
+ Path to monitor
+ :param recursive:
+ ``True`` to monitor recursively.
+ :param mask:
+ Event bit mask.
+ """
+ if not os.path.isdir(path):
+ raise OSError('Path is not a directory')
+ self._add_watch(path, mask)
+ if recursive:
+ for root, dirnames, _ in os.walk(path):
+ for dirname in dirnames:
+ full_path = os.path.join(root, dirname)
+ if os.path.islink(full_path):
+ continue
+ self._add_watch(full_path, mask)
+
+ def _add_watch(self, path, mask):
+ """
+ Adds a watch for the given path to monitor events specified by the
+ mask.
+
+ :param path:
+ Path to monitor
+ :param mask:
+ Event bit mask.
+ """
+ wd = inotify_add_watch(self._inotify_fd, path, mask)
+ if wd == -1:
+ Inotify._raise_error()
+ self._wd_for_path[path] = wd
+ self._path_for_wd[wd] = path
+ return wd
+
+ def _remove_watch_bookkeeping(self, path):
+ wd = self._wd_for_path.pop(path)
+ del self._path_for_wd[wd]
+ return wd
+
+ @staticmethod
+ def _raise_error():
+ """
+ Raises errors for inotify failures.
+ """
+ err = ctypes.get_errno()
+ if err == errno.ENOSPC:
+ raise OSError("inotify watch limit reached")
+ elif err == errno.EMFILE:
+ raise OSError("inotify instance limit reached")
+ else:
+ raise OSError(os.strerror(err))
+
+ @staticmethod
+ def _parse_event_buffer(event_buffer):
+ """
+ Parses an event buffer of ``inotify_event`` structs returned by
+ inotify::
+
+ struct inotify_event {
+ __s32 wd; /* watch descriptor */
+ __u32 mask; /* watch mask */
+ __u32 cookie; /* cookie to synchronize two events */
+ __u32 len; /* length (including nulls) of name */
+ char name[0]; /* stub for possible name */
+ };
+
+ The ``cookie`` member of this struct is used to pair two related
+ events, for example, it pairs an IN_MOVED_FROM event with an
+ IN_MOVED_TO event.
+ """
+ i = 0
+ while i + 16 <= len(event_buffer):
+ wd, mask, cookie, length = struct.unpack_from('iIII', event_buffer, i)
+ name = event_buffer[i + 16:i + 16 + length].rstrip(b'\0')
+ i += 16 + length
+ yield wd, mask, cookie, name
+
+
+class InotifyEvent(object):
+ """
+ Inotify event struct wrapper.
+
+ :param wd:
+ Watch descriptor
+ :param mask:
+ Event mask
+ :param cookie:
+ Event cookie
+ :param name:
+ Event name.
+ :param src_path:
+ Event source path
+ """
+
+ def __init__(self, wd, mask, cookie, name, src_path):
+ self._wd = wd
+ self._mask = mask
+ self._cookie = cookie
+ self._name = name
+ self._src_path = src_path
+
+ @property
+ def src_path(self):
+ return self._src_path
+
+ @property
+ def wd(self):
+ return self._wd
+
+ @property
+ def mask(self):
+ return self._mask
+
+ @property
+ def cookie(self):
+ return self._cookie
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def is_modify(self):
+ return self._mask & InotifyConstants.IN_MODIFY > 0
+
+ @property
+ def is_close_write(self):
+ return self._mask & InotifyConstants.IN_CLOSE_WRITE > 0
+
+ @property
+ def is_close_nowrite(self):
+ return self._mask & InotifyConstants.IN_CLOSE_NOWRITE > 0
+
+ @property
+ def is_access(self):
+ return self._mask & InotifyConstants.IN_ACCESS > 0
+
+ @property
+ def is_delete(self):
+ return self._mask & InotifyConstants.IN_DELETE > 0
+
+ @property
+ def is_delete_self(self):
+ return self._mask & InotifyConstants.IN_DELETE_SELF > 0
+
+ @property
+ def is_create(self):
+ return self._mask & InotifyConstants.IN_CREATE > 0
+
+ @property
+ def is_moved_from(self):
+ return self._mask & InotifyConstants.IN_MOVED_FROM > 0
+
+ @property
+ def is_moved_to(self):
+ return self._mask & InotifyConstants.IN_MOVED_TO > 0
+
+ @property
+ def is_move(self):
+ return self._mask & InotifyConstants.IN_MOVE > 0
+
+ @property
+ def is_move_self(self):
+ return self._mask & InotifyConstants.IN_MOVE_SELF > 0
+
+ @property
+ def is_attrib(self):
+ return self._mask & InotifyConstants.IN_ATTRIB > 0
+
+ @property
+ def is_ignored(self):
+ return self._mask & InotifyConstants.IN_IGNORED > 0
+
+ @property
+ def is_directory(self):
+ # It looks like the kernel does not provide this information for
+ # IN_DELETE_SELF and IN_MOVE_SELF. In this case, assume it's a dir.
+ # See also: https://github.com/seb-m/pyinotify/blob/2c7e8f8/python2/pyinotify.py#L897
+ return (self.is_delete_self or self.is_move_self or
+ self._mask & InotifyConstants.IN_ISDIR > 0)
+
+ @property
+ def key(self):
+ return self._src_path, self._wd, self._mask, self._cookie, self._name
+
+ def __eq__(self, inotify_event):
+ return self.key == inotify_event.key
+
+ def __ne__(self, inotify_event):
+ return self.key == inotify_event.key
+
+ def __hash__(self):
+ return hash(self.key)
+
+ @staticmethod
+ def _get_mask_string(mask):
+ masks = []
+ for c in dir(InotifyConstants):
+ if c.startswith('IN_') and c not in ['IN_ALL_EVENTS', 'IN_CLOSE', 'IN_MOVE']:
+ c_val = getattr(InotifyConstants, c)
+ if mask & c_val:
+ masks.append(c)
+ mask_string = '|'.join(masks)
+ return mask_string
+
+ def __repr__(self):
+ mask_string = self._get_mask_string(self.mask)
+ s = ""
+ return s % (self.src_path, self.wd, mask_string, self.cookie, self.name)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/kqueue.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/kqueue.py
new file mode 100644
index 0000000000..9ace923213
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/kqueue.py
@@ -0,0 +1,726 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.observers.kqueue
+:synopsis: ``kqueue(2)`` based emitter implementation.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+:platforms: Mac OS X and BSD with kqueue(2).
+
+.. WARNING:: kqueue is a very heavyweight way to monitor file systems.
+ Each kqueue-detected directory modification triggers
+ a full directory scan. Traversing the entire directory tree
+ and opening file descriptors for all files will create
+ performance problems. We need to find a way to re-scan
+ only those directories which report changes and do a diff
+ between two sub-DirectorySnapshots perhaps.
+
+.. ADMONITION:: About ``select.kqueue`` and Python versions
+
+ * Python 2.5 does not ship with ``select.kqueue``
+ * Python 2.6 ships with a broken ``select.kqueue`` that cannot take
+ multiple events in the event list passed to ``kqueue.control``.
+ * Python 2.7 ships with a working ``select.kqueue``
+ implementation.
+
+ I have backported the Python 2.7 implementation to Python 2.5 and 2.6
+ in the ``select_backport`` package available on PyPI.
+
+.. ADMONITION:: About OS X performance guidelines
+
+ Quote from the `Mac OS X File System Performance Guidelines`_:
+
+ "When you only want to track changes on a file or directory, be sure to
+ open it using the ``O_EVTONLY`` flag. This flag prevents the file or
+ directory from being marked as open or in use. This is important
+ if you are tracking files on a removable volume and the user tries to
+ unmount the volume. With this flag in place, the system knows it can
+ dismiss the volume. If you had opened the files or directories without
+ this flag, the volume would be marked as busy and would not be
+ unmounted."
+
+ ``O_EVTONLY`` is defined as ``0x8000`` in the OS X header files.
+ More information here: http://www.mlsite.net/blog/?p=2312
+
+Classes
+-------
+.. autoclass:: KqueueEmitter
+ :members:
+ :show-inheritance:
+
+Collections and Utility Classes
+-------------------------------
+.. autoclass:: KeventDescriptor
+ :members:
+ :show-inheritance:
+
+.. autoclass:: KeventDescriptorSet
+ :members:
+ :show-inheritance:
+
+.. _Mac OS X File System Performance Guidelines: http://developer.apple.com/library/ios/#documentation/Performance/Conceptual/FileSystem/Articles/TrackingChanges.html#//apple_ref/doc/uid/20001993-CJBJFIDD
+
+"""
+
+from __future__ import with_statement
+from watchdog.utils import platform
+
+import threading
+import errno
+import sys
+import stat
+import os
+
+# See the notes for this module in the documentation above ^.
+#import select
+# if not has_attribute(select, 'kqueue') or sys.version_info < (2, 7, 0):
+if sys.version_info < (2, 7, 0):
+ import select_backport as select
+else:
+ import select
+
+from pathtools.path import absolute_path
+
+from watchdog.observers.api import (
+ BaseObserver,
+ EventEmitter,
+ DEFAULT_OBSERVER_TIMEOUT,
+ DEFAULT_EMITTER_TIMEOUT
+)
+
+from watchdog.utils.dirsnapshot import DirectorySnapshot
+
+from watchdog.events import (
+ DirMovedEvent,
+ DirDeletedEvent,
+ DirCreatedEvent,
+ DirModifiedEvent,
+ FileMovedEvent,
+ FileDeletedEvent,
+ FileCreatedEvent,
+ FileModifiedEvent,
+ EVENT_TYPE_MOVED,
+ EVENT_TYPE_DELETED,
+ EVENT_TYPE_CREATED
+)
+
+# Maximum number of events to process.
+MAX_EVENTS = 4096
+
+# O_EVTONLY value from the header files for OS X only.
+O_EVTONLY = 0x8000
+
+# Pre-calculated values for the kevent filter, flags, and fflags attributes.
+if platform.is_darwin():
+ WATCHDOG_OS_OPEN_FLAGS = O_EVTONLY
+else:
+ WATCHDOG_OS_OPEN_FLAGS = os.O_RDONLY | os.O_NONBLOCK
+WATCHDOG_KQ_FILTER = select.KQ_FILTER_VNODE
+WATCHDOG_KQ_EV_FLAGS = select.KQ_EV_ADD | select.KQ_EV_ENABLE | select.KQ_EV_CLEAR
+WATCHDOG_KQ_FFLAGS = (
+ select.KQ_NOTE_DELETE |
+ select.KQ_NOTE_WRITE |
+ select.KQ_NOTE_EXTEND |
+ select.KQ_NOTE_ATTRIB |
+ select.KQ_NOTE_LINK |
+ select.KQ_NOTE_RENAME |
+ select.KQ_NOTE_REVOKE
+)
+
+# Flag tests.
+
+
+def is_deleted(kev):
+ """Determines whether the given kevent represents deletion."""
+ return kev.fflags & select.KQ_NOTE_DELETE
+
+
+def is_modified(kev):
+ """Determines whether the given kevent represents modification."""
+ fflags = kev.fflags
+ return (fflags & select.KQ_NOTE_EXTEND) or (fflags & select.KQ_NOTE_WRITE)
+
+
+def is_attrib_modified(kev):
+ """Determines whether the given kevent represents attribute modification."""
+ return kev.fflags & select.KQ_NOTE_ATTRIB
+
+
+def is_renamed(kev):
+ """Determines whether the given kevent represents movement."""
+ return kev.fflags & select.KQ_NOTE_RENAME
+
+
+class KeventDescriptorSet(object):
+
+ """
+ Thread-safe kevent descriptor collection.
+ """
+
+ def __init__(self):
+ # Set of KeventDescriptor
+ self._descriptors = set()
+
+ # Descriptor for a given path.
+ self._descriptor_for_path = dict()
+
+ # Descriptor for a given fd.
+ self._descriptor_for_fd = dict()
+
+ # List of kevent objects.
+ self._kevents = list()
+
+ self._lock = threading.Lock()
+
+ @property
+ def kevents(self):
+ """
+ List of kevents monitored.
+ """
+ with self._lock:
+ return self._kevents
+
+ @property
+ def paths(self):
+ """
+ List of paths for which kevents have been created.
+ """
+ with self._lock:
+ return list(self._descriptor_for_path.keys())
+
+ def get_for_fd(self, fd):
+ """
+ Given a file descriptor, returns the kevent descriptor object
+ for it.
+
+ :param fd:
+ OS file descriptor.
+ :type fd:
+ ``int``
+ :returns:
+ A :class:`KeventDescriptor` object.
+ """
+ with self._lock:
+ return self._descriptor_for_fd[fd]
+
+ def get(self, path):
+ """
+ Obtains a :class:`KeventDescriptor` object for the specified path.
+
+ :param path:
+ Path for which the descriptor will be obtained.
+ """
+ with self._lock:
+ path = absolute_path(path)
+ return self._get(path)
+
+ def __contains__(self, path):
+ """
+ Determines whether a :class:`KeventDescriptor has been registered
+ for the specified path.
+
+ :param path:
+ Path for which the descriptor will be obtained.
+ """
+ with self._lock:
+ path = absolute_path(path)
+ return self._has_path(path)
+
+ def add(self, path, is_directory):
+ """
+ Adds a :class:`KeventDescriptor` to the collection for the given
+ path.
+
+ :param path:
+ The path for which a :class:`KeventDescriptor` object will be
+ added.
+ :param is_directory:
+ ``True`` if the path refers to a directory; ``False`` otherwise.
+ :type is_directory:
+ ``bool``
+ """
+ with self._lock:
+ path = absolute_path(path)
+ if not self._has_path(path):
+ self._add_descriptor(KeventDescriptor(path, is_directory))
+
+ def remove(self, path):
+ """
+ Removes the :class:`KeventDescriptor` object for the given path
+ if it already exists.
+
+ :param path:
+ Path for which the :class:`KeventDescriptor` object will be
+ removed.
+ """
+ with self._lock:
+ path = absolute_path(path)
+ if self._has_path(path):
+ self._remove_descriptor(self._get(path))
+
+ def clear(self):
+ """
+ Clears the collection and closes all open descriptors.
+ """
+ with self._lock:
+ for descriptor in self._descriptors:
+ descriptor.close()
+ self._descriptors.clear()
+ self._descriptor_for_fd.clear()
+ self._descriptor_for_path.clear()
+ self._kevents = []
+
+ # Thread-unsafe methods. Locking is provided at a higher level.
+ def _get(self, path):
+ """Returns a kevent descriptor for a given path."""
+ return self._descriptor_for_path[path]
+
+ def _has_path(self, path):
+ """Determines whether a :class:`KeventDescriptor` for the specified
+ path exists already in the collection."""
+ return path in self._descriptor_for_path
+
+ def _add_descriptor(self, descriptor):
+ """
+ Adds a descriptor to the collection.
+
+ :param descriptor:
+ An instance of :class:`KeventDescriptor` to be added.
+ """
+ self._descriptors.add(descriptor)
+ self._kevents.append(descriptor.kevent)
+ self._descriptor_for_path[descriptor.path] = descriptor
+ self._descriptor_for_fd[descriptor.fd] = descriptor
+
+ def _remove_descriptor(self, descriptor):
+ """
+ Removes a descriptor from the collection.
+
+ :param descriptor:
+ An instance of :class:`KeventDescriptor` to be removed.
+ """
+ self._descriptors.remove(descriptor)
+ del self._descriptor_for_fd[descriptor.fd]
+ del self._descriptor_for_path[descriptor.path]
+ self._kevents.remove(descriptor.kevent)
+ descriptor.close()
+
+
+class KeventDescriptor(object):
+
+ """
+ A kevent descriptor convenience data structure to keep together:
+
+ * kevent
+ * directory status
+ * path
+ * file descriptor
+
+ :param path:
+ Path string for which a kevent descriptor will be created.
+ :param is_directory:
+ ``True`` if the path refers to a directory; ``False`` otherwise.
+ :type is_directory:
+ ``bool``
+ """
+
+ def __init__(self, path, is_directory):
+ self._path = absolute_path(path)
+ self._is_directory = is_directory
+ self._fd = os.open(path, WATCHDOG_OS_OPEN_FLAGS)
+ self._kev = select.kevent(self._fd,
+ filter=WATCHDOG_KQ_FILTER,
+ flags=WATCHDOG_KQ_EV_FLAGS,
+ fflags=WATCHDOG_KQ_FFLAGS)
+
+ @property
+ def fd(self):
+ """OS file descriptor for the kevent descriptor."""
+ return self._fd
+
+ @property
+ def path(self):
+ """The path associated with the kevent descriptor."""
+ return self._path
+
+ @property
+ def kevent(self):
+ """The kevent object associated with the kevent descriptor."""
+ return self._kev
+
+ @property
+ def is_directory(self):
+ """Determines whether the kevent descriptor refers to a directory.
+
+ :returns:
+ ``True`` or ``False``
+ """
+ return self._is_directory
+
+ def close(self):
+ """
+ Closes the file descriptor associated with a kevent descriptor.
+ """
+ try:
+ os.close(self.fd)
+ except OSError:
+ pass
+
+ @property
+ def key(self):
+ return (self.path, self.is_directory)
+
+ def __eq__(self, descriptor):
+ return self.key == descriptor.key
+
+ def __ne__(self, descriptor):
+ return self.key != descriptor.key
+
+ def __hash__(self):
+ return hash(self.key)
+
+ def __repr__(self):
+ return ""\
+ % (self.path, self.is_directory)
+
+
+class KqueueEmitter(EventEmitter):
+
+ """
+ kqueue(2)-based event emitter.
+
+ .. ADMONITION:: About ``kqueue(2)`` behavior and this implementation
+
+ ``kqueue(2)`` monitors file system events only for
+ open descriptors, which means, this emitter does a lot of
+ book-keeping behind the scenes to keep track of open
+ descriptors for every entry in the monitored directory tree.
+
+ This also means the number of maximum open file descriptors
+ on your system must be increased **manually**.
+ Usually, issuing a call to ``ulimit`` should suffice::
+
+ ulimit -n 1024
+
+ Ensure that you pick a number that is larger than the
+ number of files you expect to be monitored.
+
+ ``kqueue(2)`` does not provide enough information about the
+ following things:
+
+ * The destination path of a file or directory that is renamed.
+ * Creation of a file or directory within a directory; in this
+ case, ``kqueue(2)`` only indicates a modified event on the
+ parent directory.
+
+ Therefore, this emitter takes a snapshot of the directory
+ tree when ``kqueue(2)`` detects a change on the file system
+ to be able to determine the above information.
+
+ :param event_queue:
+ The event queue to fill with events.
+ :param watch:
+ A watch object representing the directory to monitor.
+ :type watch:
+ :class:`watchdog.observers.api.ObservedWatch`
+ :param timeout:
+ Read events blocking timeout (in seconds).
+ :type timeout:
+ ``float``
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+
+ self._kq = select.kqueue()
+ self._lock = threading.RLock()
+
+ # A collection of KeventDescriptor.
+ self._descriptors = KeventDescriptorSet()
+
+ def walker_callback(path, stat_info, self=self):
+ self._register_kevent(path, stat.S_ISDIR(stat_info.st_mode))
+
+ self._snapshot = DirectorySnapshot(watch.path,
+ watch.is_recursive,
+ walker_callback)
+
+ def _register_kevent(self, path, is_directory):
+ """
+ Registers a kevent descriptor for the given path.
+
+ :param path:
+ Path for which a kevent descriptor will be created.
+ :param is_directory:
+ ``True`` if the path refers to a directory; ``False`` otherwise.
+ :type is_directory:
+ ``bool``
+ """
+ try:
+ self._descriptors.add(path, is_directory)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ # Probably dealing with a temporary file that was created
+ # and then quickly deleted before we could open
+ # a descriptor for it. Therefore, simply queue a sequence
+ # of created and deleted events for the path.
+ #path = absolute_path(path)
+ # if is_directory:
+ # self.queue_event(DirCreatedEvent(path))
+ # self.queue_event(DirDeletedEvent(path))
+ # else:
+ # self.queue_event(FileCreatedEvent(path))
+ # self.queue_event(FileDeletedEvent(path))
+
+ # TODO: We could simply ignore these files.
+ # Locked files cause the python process to die with
+ # a bus error when we handle temporary files.
+ # eg. .git/index.lock when running tig operations.
+ # I don't fully understand this at the moment.
+ pass
+ else:
+ # All other errors are propagated.
+ raise
+
+ def _unregister_kevent(self, path):
+ """
+ Convenience function to close the kevent descriptor for a
+ specified kqueue-monitored path.
+
+ :param path:
+ Path for which the kevent descriptor will be closed.
+ """
+ self._descriptors.remove(path)
+
+ def queue_event(self, event):
+ """
+ Handles queueing a single event object.
+
+ :param event:
+ An instance of :class:`watchdog.events.FileSystemEvent`
+ or a subclass.
+ """
+ # Handles all the book keeping for queued events.
+ # We do not need to fire moved/deleted events for all subitems in
+ # a directory tree here, because this function is called by kqueue
+ # for all those events anyway.
+ EventEmitter.queue_event(self, event)
+ if event.event_type == EVENT_TYPE_CREATED:
+ self._register_kevent(event.src_path, event.is_directory)
+ elif event.event_type == EVENT_TYPE_MOVED:
+ self._unregister_kevent(event.src_path)
+ self._register_kevent(event.dest_path, event.is_directory)
+ elif event.event_type == EVENT_TYPE_DELETED:
+ self._unregister_kevent(event.src_path)
+
+ def _queue_dirs_modified(self,
+ dirs_modified,
+ ref_snapshot,
+ new_snapshot):
+ """
+ Queues events for directory modifications by scanning the directory
+ for changes.
+
+ A scan is a comparison between two snapshots of the same directory
+ taken at two different times. This also determines whether files
+ or directories were created, which updated the modified timestamp
+ for the directory.
+ """
+ if dirs_modified:
+ for dir_modified in dirs_modified:
+ self.queue_event(DirModifiedEvent(dir_modified))
+ diff_events = new_snapshot - ref_snapshot
+ for file_created in diff_events.files_created:
+ self.queue_event(FileCreatedEvent(file_created))
+ for directory_created in diff_events.dirs_created:
+ self.queue_event(DirCreatedEvent(directory_created))
+
+ def _queue_events_except_renames_and_dir_modifications(self, event_list):
+ """
+ Queues events from the kevent list returned from the call to
+ :meth:`select.kqueue.control`.
+
+ .. NOTE:: Queues only the deletions, file modifications,
+ attribute modifications. The other events, namely,
+ file creation, directory modification, file rename,
+ directory rename, directory creation, etc. are
+ determined by comparing directory snapshots.
+ """
+ files_renamed = set()
+ dirs_renamed = set()
+ dirs_modified = set()
+
+ for kev in event_list:
+ descriptor = self._descriptors.get_for_fd(kev.ident)
+ src_path = descriptor.path
+
+ if is_deleted(kev):
+ if descriptor.is_directory:
+ self.queue_event(DirDeletedEvent(src_path))
+ else:
+ self.queue_event(FileDeletedEvent(src_path))
+ elif is_attrib_modified(kev):
+ if descriptor.is_directory:
+ self.queue_event(DirModifiedEvent(src_path))
+ else:
+ self.queue_event(FileModifiedEvent(src_path))
+ elif is_modified(kev):
+ if descriptor.is_directory:
+ # When a directory is modified, it may be due to
+ # sub-file/directory renames or new file/directory
+ # creation. We determine all this by comparing
+ # snapshots later.
+ dirs_modified.add(src_path)
+ else:
+ self.queue_event(FileModifiedEvent(src_path))
+ elif is_renamed(kev):
+ # Kqueue does not specify the destination names for renames
+ # to, so we have to process these after taking a snapshot
+ # of the directory.
+ if descriptor.is_directory:
+ dirs_renamed.add(src_path)
+ else:
+ files_renamed.add(src_path)
+ return files_renamed, dirs_renamed, dirs_modified
+
+ def _queue_renamed(self,
+ src_path,
+ is_directory,
+ ref_snapshot,
+ new_snapshot):
+ """
+ Compares information from two directory snapshots (one taken before
+ the rename operation and another taken right after) to determine the
+ destination path of the file system object renamed, and adds
+ appropriate events to the event queue.
+ """
+ try:
+ ref_stat_info = ref_snapshot.stat_info(src_path)
+ except KeyError:
+ # Probably caught a temporary file/directory that was renamed
+ # and deleted. Fires a sequence of created and deleted events
+ # for the path.
+ if is_directory:
+ self.queue_event(DirCreatedEvent(src_path))
+ self.queue_event(DirDeletedEvent(src_path))
+ else:
+ self.queue_event(FileCreatedEvent(src_path))
+ self.queue_event(FileDeletedEvent(src_path))
+ # We don't process any further and bail out assuming
+ # the event represents deletion/creation instead of movement.
+ return
+
+ try:
+ dest_path = absolute_path(
+ new_snapshot.path_for_inode(ref_stat_info.st_ino))
+ if is_directory:
+ event = DirMovedEvent(src_path, dest_path)
+ # TODO: Do we need to fire moved events for the items
+ # inside the directory tree? Does kqueue does this
+ # all by itself? Check this and then enable this code
+ # only if it doesn't already.
+ # A: It doesn't. So I've enabled this block.
+ if self.watch.is_recursive:
+ for sub_event in event.sub_moved_events():
+ self.queue_event(sub_event)
+ self.queue_event(event)
+ else:
+ self.queue_event(FileMovedEvent(src_path, dest_path))
+ except KeyError:
+ # If the new snapshot does not have an inode for the
+ # old path, we haven't found the new name. Therefore,
+ # we mark it as deleted and remove unregister the path.
+ if is_directory:
+ self.queue_event(DirDeletedEvent(src_path))
+ else:
+ self.queue_event(FileDeletedEvent(src_path))
+
+ def _read_events(self, timeout=None):
+ """
+ Reads events from a call to the blocking
+ :meth:`select.kqueue.control()` method.
+
+ :param timeout:
+ Blocking timeout for reading events.
+ :type timeout:
+ ``float`` (seconds)
+ """
+ return self._kq.control(self._descriptors.kevents,
+ MAX_EVENTS,
+ timeout)
+
+ def queue_events(self, timeout):
+ """
+ Queues events by reading them from a call to the blocking
+ :meth:`select.kqueue.control()` method.
+
+ :param timeout:
+ Blocking timeout for reading events.
+ :type timeout:
+ ``float`` (seconds)
+ """
+ with self._lock:
+ try:
+ event_list = self._read_events(timeout)
+ files_renamed, dirs_renamed, dirs_modified = (
+ self._queue_events_except_renames_and_dir_modifications(event_list))
+
+ # Take a fresh snapshot of the directory and update the
+ # saved snapshot.
+ new_snapshot = DirectorySnapshot(self.watch.path,
+ self.watch.is_recursive)
+ ref_snapshot = self._snapshot
+ self._snapshot = new_snapshot
+
+ if files_renamed or dirs_renamed or dirs_modified:
+ for src_path in files_renamed:
+ self._queue_renamed(src_path,
+ False,
+ ref_snapshot,
+ new_snapshot)
+ for src_path in dirs_renamed:
+ self._queue_renamed(src_path,
+ True,
+ ref_snapshot,
+ new_snapshot)
+ self._queue_dirs_modified(dirs_modified,
+ ref_snapshot,
+ new_snapshot)
+ except OSError as e:
+ if e.errno == errno.EBADF:
+ # logging.debug(e)
+ pass
+ else:
+ raise
+
+ def on_thread_stop(self):
+ # Clean up.
+ with self._lock:
+ self._descriptors.clear()
+ self._kq.close()
+
+
+class KqueueObserver(BaseObserver):
+
+ """
+ Observer thread that schedules watching directories and dispatches
+ calls to event handlers.
+ """
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=KqueueEmitter, timeout=timeout)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/polling.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/polling.py
new file mode 100644
index 0000000000..153d874d74
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/polling.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+:module: watchdog.observers.polling
+:synopsis: Polling emitter implementation.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+
+Classes
+-------
+.. autoclass:: PollingObserver
+ :members:
+ :show-inheritance:
+
+.. autoclass:: PollingObserverVFS
+ :members:
+ :show-inheritance:
+ :special-members:
+"""
+
+from __future__ import with_statement
+import os
+import threading
+from functools import partial
+from watchdog.utils import stat as default_stat
+from watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff
+from watchdog.observers.api import (
+ EventEmitter,
+ BaseObserver,
+ DEFAULT_OBSERVER_TIMEOUT,
+ DEFAULT_EMITTER_TIMEOUT
+)
+
+from watchdog.events import (
+ DirMovedEvent,
+ DirDeletedEvent,
+ DirCreatedEvent,
+ DirModifiedEvent,
+ FileMovedEvent,
+ FileDeletedEvent,
+ FileCreatedEvent,
+ FileModifiedEvent
+)
+
+
+class PollingEmitter(EventEmitter):
+ """
+ Platform-independent emitter that polls a directory to detect file
+ system changes.
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT,
+ stat=default_stat, listdir=os.listdir):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+ self._snapshot = None
+ self._lock = threading.Lock()
+ self._take_snapshot = lambda: DirectorySnapshot(
+ self.watch.path, self.watch.is_recursive, stat=stat, listdir=listdir)
+
+ def on_thread_start(self):
+ self._snapshot = self._take_snapshot()
+
+ def queue_events(self, timeout):
+ # We don't want to hit the disk continuously.
+ # timeout behaves like an interval for polling emitters.
+ if self.stopped_event.wait(timeout):
+ return
+
+ with self._lock:
+ if not self.should_keep_running():
+ return
+
+ # Get event diff between fresh snapshot and previous snapshot.
+ # Update snapshot.
+ new_snapshot = self._take_snapshot()
+ events = DirectorySnapshotDiff(self._snapshot, new_snapshot)
+ self._snapshot = new_snapshot
+
+ # Files.
+ for src_path in events.files_deleted:
+ self.queue_event(FileDeletedEvent(src_path))
+ for src_path in events.files_modified:
+ self.queue_event(FileModifiedEvent(src_path))
+ for src_path in events.files_created:
+ self.queue_event(FileCreatedEvent(src_path))
+ for src_path, dest_path in events.files_moved:
+ self.queue_event(FileMovedEvent(src_path, dest_path))
+
+ # Directories.
+ for src_path in events.dirs_deleted:
+ self.queue_event(DirDeletedEvent(src_path))
+ for src_path in events.dirs_modified:
+ self.queue_event(DirModifiedEvent(src_path))
+ for src_path in events.dirs_created:
+ self.queue_event(DirCreatedEvent(src_path))
+ for src_path, dest_path in events.dirs_moved:
+ self.queue_event(DirMovedEvent(src_path, dest_path))
+
+
+class PollingObserver(BaseObserver):
+ """
+ Platform-independent observer that polls a directory to detect file
+ system changes.
+ """
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=PollingEmitter, timeout=timeout)
+
+
+class PollingObserverVFS(BaseObserver):
+ """
+ File system independent observer that polls a directory to detect changes.
+ """
+
+ def __init__(self, stat, listdir, polling_interval=1):
+ """
+ :param stat: stat function. See ``os.stat`` for details.
+ :param listdir: listdir function. See ``os.listdir`` for details.
+ :type polling_interval: float
+ :param polling_interval: interval in seconds between polling the file system.
+ """
+ emitter_cls = partial(PollingEmitter, stat=stat, listdir=listdir)
+ BaseObserver.__init__(self, emitter_class=emitter_cls, timeout=polling_interval)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/read_directory_changes.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/read_directory_changes.py
new file mode 100644
index 0000000000..623a23563d
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/read_directory_changes.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import with_statement
+
+import ctypes
+import threading
+import os.path
+import time
+
+from watchdog.events import (
+ DirCreatedEvent,
+ DirDeletedEvent,
+ DirMovedEvent,
+ DirModifiedEvent,
+ FileCreatedEvent,
+ FileDeletedEvent,
+ FileMovedEvent,
+ FileModifiedEvent,
+ generate_sub_moved_events,
+ generate_sub_created_events,
+)
+
+from watchdog.observers.api import (
+ EventEmitter,
+ BaseObserver,
+ DEFAULT_OBSERVER_TIMEOUT,
+ DEFAULT_EMITTER_TIMEOUT
+)
+
+from watchdog.observers.winapi import (
+ read_events,
+ get_directory_handle,
+ close_directory_handle,
+)
+
+
+# HACK:
+WATCHDOG_TRAVERSE_MOVED_DIR_DELAY = 1 # seconds
+
+
+class WindowsApiEmitter(EventEmitter):
+ """
+ Windows API-based emitter that uses ReadDirectoryChangesW
+ to detect file system changes for a watch.
+ """
+
+ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT):
+ EventEmitter.__init__(self, event_queue, watch, timeout)
+ self._lock = threading.Lock()
+ self._handle = None
+
+ def on_thread_start(self):
+ self._handle = get_directory_handle(self.watch.path)
+
+ def on_thread_stop(self):
+ if self._handle:
+ close_directory_handle(self._handle)
+
+ def queue_events(self, timeout):
+ winapi_events = read_events(self._handle, self.watch.is_recursive)
+ with self._lock:
+ last_renamed_src_path = ""
+ for winapi_event in winapi_events:
+ src_path = os.path.join(self.watch.path, winapi_event.src_path)
+
+ if winapi_event.is_renamed_old:
+ last_renamed_src_path = src_path
+ elif winapi_event.is_renamed_new:
+ dest_path = src_path
+ src_path = last_renamed_src_path
+ if os.path.isdir(dest_path):
+ event = DirMovedEvent(src_path, dest_path)
+ if self.watch.is_recursive:
+ # HACK: We introduce a forced delay before
+ # traversing the moved directory. This will read
+ # only file movement that finishes within this
+ # delay time.
+ time.sleep(WATCHDOG_TRAVERSE_MOVED_DIR_DELAY)
+ # The following block of code may not
+ # obtain moved events for the entire tree if
+ # the I/O is not completed within the above
+ # delay time. So, it's not guaranteed to work.
+ # TODO: Come up with a better solution, possibly
+ # a way to wait for I/O to complete before
+ # queuing events.
+ for sub_moved_event in generate_sub_moved_events(src_path, dest_path):
+ self.queue_event(sub_moved_event)
+ self.queue_event(event)
+ else:
+ self.queue_event(FileMovedEvent(src_path, dest_path))
+ elif winapi_event.is_modified:
+ cls = DirModifiedEvent if os.path.isdir(src_path) else FileModifiedEvent
+ self.queue_event(cls(src_path))
+ elif winapi_event.is_added:
+ isdir = os.path.isdir(src_path)
+ cls = DirCreatedEvent if isdir else FileCreatedEvent
+ self.queue_event(cls(src_path))
+ if isdir:
+ # If a directory is moved from outside the watched folder to inside it
+ # we only get a created directory event out of it, not any events for its children
+ # so use the same hack as for file moves to get the child events
+ time.sleep(WATCHDOG_TRAVERSE_MOVED_DIR_DELAY)
+ sub_events = generate_sub_created_events(src_path)
+ for sub_created_event in sub_events:
+ self.queue_event(sub_created_event)
+ elif winapi_event.is_removed:
+ self.queue_event(FileDeletedEvent(src_path))
+
+
+class WindowsApiObserver(BaseObserver):
+ """
+ Observer thread that schedules watching directories and dispatches
+ calls to event handlers.
+ """
+
+ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT):
+ BaseObserver.__init__(self, emitter_class=WindowsApiEmitter,
+ timeout=timeout)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/observers/winapi.py b/script.service.kodi.callbacks/resources/lib/watchdog/observers/winapi.py
new file mode 100644
index 0000000000..eb5f372995
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/observers/winapi.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# winapi.py: Windows API-Python interface (removes dependency on pywin32)
+#
+# Copyright (C) 2007 Thomas Heller
+# Copyright (C) 2010 Will McGugan
+# Copyright (C) 2010 Ryan Kelly
+# Copyright (C) 2010 Yesudeep Mangalapilly
+# Copyright (C) 2014 Thomas Amland
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and / or other materials provided with the distribution.
+# * Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Portions of this code were taken from pyfilesystem, which uses the above
+# new BSD license.
+
+from __future__ import with_statement
+
+import ctypes.wintypes
+import struct
+from functools import reduce
+
+try:
+ LPVOID = ctypes.wintypes.LPVOID
+except AttributeError:
+ # LPVOID wasn't defined in Py2.5, guess it was introduced in Py2.6
+ LPVOID = ctypes.c_void_p
+
+# Invalid handle value.
+INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
+
+# File notification contants.
+FILE_NOTIFY_CHANGE_FILE_NAME = 0x01
+FILE_NOTIFY_CHANGE_DIR_NAME = 0x02
+FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x04
+FILE_NOTIFY_CHANGE_SIZE = 0x08
+FILE_NOTIFY_CHANGE_LAST_WRITE = 0x010
+FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x020
+FILE_NOTIFY_CHANGE_CREATION = 0x040
+FILE_NOTIFY_CHANGE_SECURITY = 0x0100
+
+FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+FILE_FLAG_OVERLAPPED = 0x40000000
+FILE_LIST_DIRECTORY = 0x01
+FILE_SHARE_READ = 0x01
+FILE_SHARE_WRITE = 0x02
+FILE_SHARE_DELETE = 0x04
+OPEN_EXISTING = 3
+
+# File action constants.
+FILE_ACTION_CREATED = 1
+FILE_ACTION_DELETED = 2
+FILE_ACTION_MODIFIED = 3
+FILE_ACTION_RENAMED_OLD_NAME = 4
+FILE_ACTION_RENAMED_NEW_NAME = 5
+FILE_ACTION_OVERFLOW = 0xFFFF
+
+# Aliases
+FILE_ACTION_ADDED = FILE_ACTION_CREATED
+FILE_ACTION_REMOVED = FILE_ACTION_DELETED
+
+THREAD_TERMINATE = 0x0001
+
+# IO waiting constants.
+WAIT_ABANDONED = 0x00000080
+WAIT_IO_COMPLETION = 0x000000C0
+WAIT_OBJECT_0 = 0x00000000
+WAIT_TIMEOUT = 0x00000102
+
+# Error codes
+ERROR_OPERATION_ABORTED = 995
+
+
+class OVERLAPPED(ctypes.Structure):
+ _fields_ = [('Internal', LPVOID),
+ ('InternalHigh', LPVOID),
+ ('Offset', ctypes.wintypes.DWORD),
+ ('OffsetHigh', ctypes.wintypes.DWORD),
+ ('Pointer', LPVOID),
+ ('hEvent', ctypes.wintypes.HANDLE),
+ ]
+
+
+def _errcheck_bool(value, func, args):
+ if not value:
+ raise ctypes.WinError()
+ return args
+
+
+def _errcheck_handle(value, func, args):
+ if not value:
+ raise ctypes.WinError()
+ if value == INVALID_HANDLE_VALUE:
+ raise ctypes.WinError()
+ return args
+
+
+def _errcheck_dword(value, func, args):
+ if value == 0xFFFFFFFF:
+ raise ctypes.WinError()
+ return args
+
+
+ReadDirectoryChangesW = ctypes.windll.kernel32.ReadDirectoryChangesW
+ReadDirectoryChangesW.restype = ctypes.wintypes.BOOL
+ReadDirectoryChangesW.errcheck = _errcheck_bool
+ReadDirectoryChangesW.argtypes = (
+ ctypes.wintypes.HANDLE, # hDirectory
+ LPVOID, # lpBuffer
+ ctypes.wintypes.DWORD, # nBufferLength
+ ctypes.wintypes.BOOL, # bWatchSubtree
+ ctypes.wintypes.DWORD, # dwNotifyFilter
+ ctypes.POINTER(ctypes.wintypes.DWORD), # lpBytesReturned
+ ctypes.POINTER(OVERLAPPED), # lpOverlapped
+ LPVOID # FileIOCompletionRoutine # lpCompletionRoutine
+)
+
+CreateFileW = ctypes.windll.kernel32.CreateFileW
+CreateFileW.restype = ctypes.wintypes.HANDLE
+CreateFileW.errcheck = _errcheck_handle
+CreateFileW.argtypes = (
+ ctypes.wintypes.LPCWSTR, # lpFileName
+ ctypes.wintypes.DWORD, # dwDesiredAccess
+ ctypes.wintypes.DWORD, # dwShareMode
+ LPVOID, # lpSecurityAttributes
+ ctypes.wintypes.DWORD, # dwCreationDisposition
+ ctypes.wintypes.DWORD, # dwFlagsAndAttributes
+ ctypes.wintypes.HANDLE # hTemplateFile
+)
+
+CloseHandle = ctypes.windll.kernel32.CloseHandle
+CloseHandle.restype = ctypes.wintypes.BOOL
+CloseHandle.argtypes = (
+ ctypes.wintypes.HANDLE, # hObject
+)
+
+CancelIoEx = ctypes.windll.kernel32.CancelIoEx
+CancelIoEx.restype = ctypes.wintypes.BOOL
+CancelIoEx.errcheck = _errcheck_bool
+CancelIoEx.argtypes = (
+ ctypes.wintypes.HANDLE, # hObject
+ ctypes.POINTER(OVERLAPPED) # lpOverlapped
+)
+
+CreateEvent = ctypes.windll.kernel32.CreateEventW
+CreateEvent.restype = ctypes.wintypes.HANDLE
+CreateEvent.errcheck = _errcheck_handle
+CreateEvent.argtypes = (
+ LPVOID, # lpEventAttributes
+ ctypes.wintypes.BOOL, # bManualReset
+ ctypes.wintypes.BOOL, # bInitialState
+ ctypes.wintypes.LPCWSTR, # lpName
+)
+
+SetEvent = ctypes.windll.kernel32.SetEvent
+SetEvent.restype = ctypes.wintypes.BOOL
+SetEvent.errcheck = _errcheck_bool
+SetEvent.argtypes = (
+ ctypes.wintypes.HANDLE, # hEvent
+)
+
+WaitForSingleObjectEx = ctypes.windll.kernel32.WaitForSingleObjectEx
+WaitForSingleObjectEx.restype = ctypes.wintypes.DWORD
+WaitForSingleObjectEx.errcheck = _errcheck_dword
+WaitForSingleObjectEx.argtypes = (
+ ctypes.wintypes.HANDLE, # hObject
+ ctypes.wintypes.DWORD, # dwMilliseconds
+ ctypes.wintypes.BOOL, # bAlertable
+)
+
+CreateIoCompletionPort = ctypes.windll.kernel32.CreateIoCompletionPort
+CreateIoCompletionPort.restype = ctypes.wintypes.HANDLE
+CreateIoCompletionPort.errcheck = _errcheck_handle
+CreateIoCompletionPort.argtypes = (
+ ctypes.wintypes.HANDLE, # FileHandle
+ ctypes.wintypes.HANDLE, # ExistingCompletionPort
+ LPVOID, # CompletionKey
+ ctypes.wintypes.DWORD, # NumberOfConcurrentThreads
+)
+
+GetQueuedCompletionStatus = ctypes.windll.kernel32.GetQueuedCompletionStatus
+GetQueuedCompletionStatus.restype = ctypes.wintypes.BOOL
+GetQueuedCompletionStatus.errcheck = _errcheck_bool
+GetQueuedCompletionStatus.argtypes = (
+ ctypes.wintypes.HANDLE, # CompletionPort
+ LPVOID, # lpNumberOfBytesTransferred
+ LPVOID, # lpCompletionKey
+ ctypes.POINTER(OVERLAPPED), # lpOverlapped
+ ctypes.wintypes.DWORD, # dwMilliseconds
+)
+
+PostQueuedCompletionStatus = ctypes.windll.kernel32.PostQueuedCompletionStatus
+PostQueuedCompletionStatus.restype = ctypes.wintypes.BOOL
+PostQueuedCompletionStatus.errcheck = _errcheck_bool
+PostQueuedCompletionStatus.argtypes = (
+ ctypes.wintypes.HANDLE, # CompletionPort
+ ctypes.wintypes.DWORD, # lpNumberOfBytesTransferred
+ ctypes.wintypes.DWORD, # lpCompletionKey
+ ctypes.POINTER(OVERLAPPED), # lpOverlapped
+)
+
+
+class FILE_NOTIFY_INFORMATION(ctypes.Structure):
+ _fields_ = [("NextEntryOffset", ctypes.wintypes.DWORD),
+ ("Action", ctypes.wintypes.DWORD),
+ ("FileNameLength", ctypes.wintypes.DWORD),
+ #("FileName", (ctypes.wintypes.WCHAR * 1))]
+ ("FileName", (ctypes.c_char * 1))]
+
+LPFNI = ctypes.POINTER(FILE_NOTIFY_INFORMATION)
+
+
+# We don't need to recalculate these flags every time a call is made to
+# the win32 API functions.
+WATCHDOG_FILE_FLAGS = FILE_FLAG_BACKUP_SEMANTICS
+WATCHDOG_FILE_SHARE_FLAGS = reduce(
+ lambda x, y: x | y, [
+ FILE_SHARE_READ,
+ FILE_SHARE_WRITE,
+ FILE_SHARE_DELETE,
+ ])
+WATCHDOG_FILE_NOTIFY_FLAGS = reduce(
+ lambda x, y: x | y, [
+ FILE_NOTIFY_CHANGE_FILE_NAME,
+ FILE_NOTIFY_CHANGE_DIR_NAME,
+ FILE_NOTIFY_CHANGE_ATTRIBUTES,
+ FILE_NOTIFY_CHANGE_SIZE,
+ FILE_NOTIFY_CHANGE_LAST_WRITE,
+ FILE_NOTIFY_CHANGE_SECURITY,
+ FILE_NOTIFY_CHANGE_LAST_ACCESS,
+ FILE_NOTIFY_CHANGE_CREATION,
+ ])
+
+BUFFER_SIZE = 2048
+
+
+def _parse_event_buffer(readBuffer, nBytes):
+ results = []
+ while nBytes > 0:
+ fni = ctypes.cast(readBuffer, LPFNI)[0]
+ ptr = ctypes.addressof(fni) + FILE_NOTIFY_INFORMATION.FileName.offset
+ #filename = ctypes.wstring_at(ptr, fni.FileNameLength)
+ filename = ctypes.string_at(ptr, fni.FileNameLength)
+ results.append((fni.Action, filename.decode('utf-16')))
+ numToSkip = fni.NextEntryOffset
+ if numToSkip <= 0:
+ break
+ readBuffer = readBuffer[numToSkip:]
+ nBytes -= numToSkip # numToSkip is long. nBytes should be long too.
+ return results
+
+
+def get_directory_handle(path):
+ """Returns a Windows handle to the specified directory path."""
+ return CreateFileW(path, FILE_LIST_DIRECTORY, WATCHDOG_FILE_SHARE_FLAGS,
+ None, OPEN_EXISTING, WATCHDOG_FILE_FLAGS, None)
+
+
+def close_directory_handle(handle):
+ try:
+ CancelIoEx(handle, None) # force ReadDirectoryChangesW to return
+ CloseHandle(handle) # close directory handle
+ except WindowsError:
+ try:
+ CloseHandle(handle) # close directory handle
+ except:
+ return
+
+
+def read_directory_changes(handle, recursive):
+ """Read changes to the directory using the specified directory handle.
+
+ http://timgolden.me.uk/pywin32-docs/win32file__ReadDirectoryChangesW_meth.html
+ """
+ event_buffer = ctypes.create_string_buffer(BUFFER_SIZE)
+ nbytes = ctypes.wintypes.DWORD()
+ try:
+ ReadDirectoryChangesW(handle, ctypes.byref(event_buffer),
+ len(event_buffer), recursive,
+ WATCHDOG_FILE_NOTIFY_FLAGS,
+ ctypes.byref(nbytes), None, None)
+ except WindowsError as e:
+ if e.winerror == ERROR_OPERATION_ABORTED:
+ return [], 0
+ raise e
+
+ # Python 2/3 compat
+ try:
+ int_class = long
+ except NameError:
+ int_class = int
+ return event_buffer.raw, int_class(nbytes.value)
+
+
+class WinAPINativeEvent(object):
+ def __init__(self, action, src_path):
+ self.action = action
+ self.src_path = src_path
+
+ @property
+ def is_added(self):
+ return self.action == FILE_ACTION_CREATED
+
+ @property
+ def is_removed(self):
+ return self.action == FILE_ACTION_REMOVED
+
+ @property
+ def is_modified(self):
+ return self.action == FILE_ACTION_MODIFIED
+
+ @property
+ def is_renamed_old(self):
+ return self.action == FILE_ACTION_RENAMED_OLD_NAME
+
+ @property
+ def is_renamed_new(self):
+ return self.action == FILE_ACTION_RENAMED_NEW_NAME
+
+ def __repr__(self):
+ return ("" % (self.action, self.src_path))
+
+
+def read_events(handle, recursive):
+ buf, nbytes = read_directory_changes(handle, recursive)
+ events = _parse_event_buffer(buf, nbytes)
+ return [WinAPINativeEvent(action, path) for action, path in events]
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/tricks/__init__.py b/script.service.kodi.callbacks/resources/lib/watchdog/tricks/__init__.py
new file mode 100644
index 0000000000..7e1c9fe271
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/tricks/__init__.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import signal
+import subprocess
+import time
+
+from watchdog.utils import echo, has_attribute
+from watchdog.events import PatternMatchingEventHandler
+
+
+class Trick(PatternMatchingEventHandler):
+
+ """Your tricks should subclass this class."""
+
+ @classmethod
+ def generate_yaml(cls):
+ context = dict(module_name=cls.__module__,
+ klass_name=cls.__name__)
+ template_yaml = """- %(module_name)s.%(klass_name)s:
+ args:
+ - argument1
+ - argument2
+ kwargs:
+ patterns:
+ - "*.py"
+ - "*.js"
+ ignore_patterns:
+ - "version.py"
+ ignore_directories: false
+"""
+ return template_yaml % context
+
+
+class LoggerTrick(Trick):
+
+ """A simple trick that does only logs events."""
+
+ def on_any_event(self, event):
+ pass
+
+ @echo.echo
+ def on_modified(self, event):
+ pass
+
+ @echo.echo
+ def on_deleted(self, event):
+ pass
+
+ @echo.echo
+ def on_created(self, event):
+ pass
+
+ @echo.echo
+ def on_moved(self, event):
+ pass
+
+
+class ShellCommandTrick(Trick):
+
+ """Executes shell commands in response to matched events."""
+
+ def __init__(self, shell_command=None, patterns=None, ignore_patterns=None,
+ ignore_directories=False, wait_for_process=False,
+ drop_during_process=False):
+ super(ShellCommandTrick, self).__init__(patterns, ignore_patterns,
+ ignore_directories)
+ self.shell_command = shell_command
+ self.wait_for_process = wait_for_process
+ self.drop_during_process = drop_during_process
+ self.process = None
+
+ def on_any_event(self, event):
+ from string import Template
+
+ if self.drop_during_process and self.process and self.process.poll() is None:
+ return
+
+ if event.is_directory:
+ object_type = 'directory'
+ else:
+ object_type = 'file'
+
+ context = {
+ 'watch_src_path': event.src_path,
+ 'watch_dest_path': '',
+ 'watch_event_type': event.event_type,
+ 'watch_object': object_type,
+ }
+
+ if self.shell_command is None:
+ if has_attribute(event, 'dest_path'):
+ context.update({'dest_path': event.dest_path})
+ command = 'echo "${watch_event_type} ${watch_object} from ${watch_src_path} to ${watch_dest_path}"'
+ else:
+ command = 'echo "${watch_event_type} ${watch_object} ${watch_src_path}"'
+ else:
+ if has_attribute(event, 'dest_path'):
+ context.update({'watch_dest_path': event.dest_path})
+ command = self.shell_command
+
+ command = Template(command).safe_substitute(**context)
+ self.process = subprocess.Popen(command, shell=True)
+ if self.wait_for_process:
+ self.process.wait()
+
+
+class AutoRestartTrick(Trick):
+
+ """Starts a long-running subprocess and restarts it on matched events.
+
+ The command parameter is a list of command arguments, such as
+ ['bin/myserver', '-c', 'etc/myconfig.ini'].
+
+ Call start() after creating the Trick. Call stop() when stopping
+ the process.
+ """
+
+ def __init__(self, command, patterns=None, ignore_patterns=None,
+ ignore_directories=False, stop_signal=signal.SIGINT,
+ kill_after=10):
+ super(AutoRestartTrick, self).__init__(
+ patterns, ignore_patterns, ignore_directories)
+ self.command = command
+ self.stop_signal = stop_signal
+ self.kill_after = kill_after
+ self.process = None
+
+ def start(self):
+ self.process = subprocess.Popen(self.command, preexec_fn=os.setsid)
+
+ def stop(self):
+ if self.process is None:
+ return
+ try:
+ os.killpg(os.getpgid(self.process.pid), self.stop_signal)
+ except OSError:
+ # Process is already gone
+ pass
+ else:
+ kill_time = time.time() + self.kill_after
+ while time.time() < kill_time:
+ if self.process.poll() is not None:
+ break
+ time.sleep(0.25)
+ else:
+ try:
+ os.killpg(os.getpgid(self.process.pid), 9)
+ except OSError:
+ # Process is already gone
+ pass
+ self.process = None
+
+ @echo.echo
+ def on_any_event(self, event):
+ self.stop()
+ self.start()
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/__init__.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/__init__.py
new file mode 100644
index 0000000000..8e6acb1640
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/__init__.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+:module: watchdog.utils
+:synopsis: Utility classes and functions.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+
+Classes
+-------
+.. autoclass:: BaseThread
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+"""
+import os
+import sys
+import threading
+try:
+ import watchdog.utils.platform
+ from watchdog.utils import platform
+except Exception:
+ from resources.lib.watchdog.utils import platform
+try:
+ from watchdog.utils.compat import Event
+except Exception:
+ from resources.lib.watchdog.utils.compat import Event
+
+from collections import namedtuple
+
+
+if sys.version_info[0] == 2 and platform.is_windows():
+ # st_ino is not implemented in os.stat on this platform
+ import win32stat
+ stat = win32stat.stat
+else:
+ stat = os.stat
+
+
+def has_attribute(ob, attribute):
+ """
+ :func:`hasattr` swallows exceptions. :func:`has_attribute` tests a Python object for the
+ presence of an attribute.
+
+ :param ob:
+ object to inspect
+ :param attribute:
+ ``str`` for the name of the attribute.
+ """
+ return getattr(ob, attribute, None) is not None
+
+
+class UnsupportedLibc(Exception):
+ pass
+
+
+class BaseThread(threading.Thread):
+ """ Convenience class for creating stoppable threads. """
+
+ def __init__(self):
+ threading.Thread.__init__(self)
+ if has_attribute(self, 'daemon'):
+ self.daemon = True
+ else:
+ self.setDaemon(True)
+ self._stopped_event = Event()
+
+ if not has_attribute(self._stopped_event, 'is_set'):
+ self._stopped_event.is_set = self._stopped_event.isSet
+
+ @property
+ def stopped_event(self):
+ return self._stopped_event
+
+ def should_keep_running(self):
+ """Determines whether the thread should continue running."""
+ return not self._stopped_event.is_set()
+
+ def on_thread_stop(self):
+ """Override this method instead of :meth:`stop()`.
+ :meth:`stop()` calls this method.
+
+ This method is called immediately after the thread is signaled to stop.
+ """
+ pass
+
+ def stop(self):
+ """Signals the thread to stop."""
+ self._stopped_event.set()
+ self.on_thread_stop()
+
+ def on_thread_start(self):
+ """Override this method instead of :meth:`start()`. :meth:`start()`
+ calls this method.
+
+ This method is called right before this thread is started and this
+ object’s run() method is invoked.
+ """
+ pass
+
+ def start(self):
+ self.on_thread_start()
+ threading.Thread.start(self)
+
+
+def load_module(module_name):
+ """Imports a module given its name and returns a handle to it."""
+ try:
+ __import__(module_name)
+ except ImportError:
+ raise ImportError('No module named %s' % module_name)
+ return sys.modules[module_name]
+
+
+def load_class(dotted_path):
+ """Loads and returns a class definition provided a dotted path
+ specification the last part of the dotted path is the class name
+ and there is at least one module name preceding the class name.
+
+ Notes:
+ You will need to ensure that the module you are trying to load
+ exists in the Python path.
+
+ Examples:
+ - module.name.ClassName # Provided module.name is in the Python path.
+ - module.ClassName # Provided module is in the Python path.
+
+ What won't work:
+ - ClassName
+ - modle.name.ClassName # Typo in module name.
+ - module.name.ClasNam # Typo in classname.
+ """
+ dotted_path_split = dotted_path.split('.')
+ if len(dotted_path_split) > 1:
+ klass_name = dotted_path_split[-1]
+ module_name = '.'.join(dotted_path_split[:-1])
+
+ module = load_module(module_name)
+ if has_attribute(module, klass_name):
+ klass = getattr(module, klass_name)
+ return klass
+ # Finally create and return an instance of the class
+ # return klass(*args, **kwargs)
+ else:
+ raise AttributeError('Module %s does not have class attribute %s' % (
+ module_name, klass_name))
+ else:
+ raise ValueError(
+ 'Dotted module path %s must contain a module name and a classname' % dotted_path)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/bricks.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/bricks.py
new file mode 100644
index 0000000000..b6d6516eb9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/bricks.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Utility collections or "bricks".
+
+:module: watchdog.utils.bricks
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+:author: lalinsky@gmail.com (Lukáš Lalinský)
+:author: python@rcn.com (Raymond Hettinger)
+
+Classes
+=======
+.. autoclass:: OrderedSetQueue
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+.. autoclass:: OrderedSet
+
+"""
+
+import sys
+import collections
+from .compat import queue
+
+class SkipRepeatsQueue(queue.Queue):
+
+ """Thread-safe implementation of an special queue where a
+ put of the last-item put'd will be dropped.
+
+ The implementation leverages locking already implemented in the base class
+ redefining only the primitives.
+
+ Queued items must be immutable and hashable so that they can be used
+ as dictionary keys. You must implement **only read-only properties** and
+ the :meth:`Item.__hash__()`, :meth:`Item.__eq__()`, and
+ :meth:`Item.__ne__()` methods for items to be hashable.
+
+ An example implementation follows::
+
+ class Item(object):
+ def __init__(self, a, b):
+ self._a = a
+ self._b = b
+
+ @property
+ def a(self):
+ return self._a
+
+ @property
+ def b(self):
+ return self._b
+
+ def _key(self):
+ return (self._a, self._b)
+
+ def __eq__(self, item):
+ return self._key() == item._key()
+
+ def __ne__(self, item):
+ return self._key() != item._key()
+
+ def __hash__(self):
+ return hash(self._key())
+
+ based on the OrderedSetQueue below
+ """
+
+ def _init(self, maxsize):
+ queue.Queue._init(self, maxsize)
+ self._last_item = None
+
+ def _put(self, item):
+ if item != self._last_item:
+ queue.Queue._put(self, item)
+ self._last_item = item
+ else:
+ # `put` increments `unfinished_tasks` even if we did not put
+ # anything into the queue here
+ self.unfinished_tasks -= 1
+
+ def _get(self):
+ item = queue.Queue._get(self)
+ if item is self._last_item:
+ self._last_item = None
+ return item
+
+
+class OrderedSetQueue(queue.Queue):
+
+ """Thread-safe implementation of an ordered set queue.
+
+ Disallows adding a duplicate item while maintaining the
+ order of items in the queue. The implementation leverages
+ locking already implemented in the base class
+ redefining only the primitives. Since the internal queue
+ is not replaced, the order is maintained. The set is used
+ merely to check for the existence of an item.
+
+ Queued items must be immutable and hashable so that they can be used
+ as dictionary keys. You must implement **only read-only properties** and
+ the :meth:`Item.__hash__()`, :meth:`Item.__eq__()`, and
+ :meth:`Item.__ne__()` methods for items to be hashable.
+
+ An example implementation follows::
+
+ class Item(object):
+ def __init__(self, a, b):
+ self._a = a
+ self._b = b
+
+ @property
+ def a(self):
+ return self._a
+
+ @property
+ def b(self):
+ return self._b
+
+ def _key(self):
+ return (self._a, self._b)
+
+ def __eq__(self, item):
+ return self._key() == item._key()
+
+ def __ne__(self, item):
+ return self._key() != item._key()
+
+ def __hash__(self):
+ return hash(self._key())
+
+ :author: lalinsky@gmail.com (Lukáš Lalinský)
+ :url: http://stackoverflow.com/questions/1581895/how-check-if-a-task-is-already-in-python-queue
+ """
+
+ def _init(self, maxsize):
+ queue.Queue._init(self, maxsize)
+ self._set_of_items = set()
+
+ def _put(self, item):
+ if item not in self._set_of_items:
+ queue.Queue._put(self, item)
+ self._set_of_items.add(item)
+ else:
+ # `put` increments `unfinished_tasks` even if we did not put
+ # anything into the queue here
+ self.unfinished_tasks -= 1
+
+ def _get(self):
+ item = queue.Queue._get(self)
+ self._set_of_items.remove(item)
+ return item
+
+
+if sys.version_info >= (2, 6, 0):
+ KEY, PREV, NEXT = list(range(3))
+
+ class OrderedSet(collections.MutableSet):
+
+ """
+ Implementation based on a doubly-linked link and an internal dictionary.
+ This design gives :class:`OrderedSet` the same big-Oh running times as
+ regular sets including O(1) adds, removes, and lookups as well as
+ O(n) iteration.
+
+ .. ADMONITION:: Implementation notes
+
+ Runs on Python 2.6 or later (and runs on Python 3.0 or later
+ without any modifications).
+
+ :author: python@rcn.com (Raymond Hettinger)
+ :url: http://code.activestate.com/recipes/576694/
+ """
+
+ def __init__(self, iterable=None):
+ self.end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.map = {} # key --> [key, prev, next]
+ if iterable is not None:
+ self |= iterable
+
+ def __len__(self):
+ return len(self.map)
+
+ def __contains__(self, key):
+ return key in self.map
+
+ def add(self, key):
+ if key not in self.map:
+ end = self.end
+ curr = end[PREV]
+ curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
+
+ def discard(self, key):
+ if key in self.map:
+ key, prev, _next = self.map.pop(key)
+ prev[NEXT] = _next
+ _next[PREV] = prev
+
+ def __iter__(self):
+ end = self.end
+ curr = end[NEXT]
+ while curr is not end:
+ yield curr[KEY]
+ curr = curr[NEXT]
+
+ def __reversed__(self):
+ end = self.end
+ curr = end[PREV]
+ while curr is not end:
+ yield curr[KEY]
+ curr = curr[PREV]
+
+ def pop(self, last=True):
+ if not self:
+ raise KeyError('set is empty')
+ key = next(reversed(self)) if last else next(iter(self))
+ self.discard(key)
+ return key
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, list(self))
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedSet):
+ return len(self) == len(other) and list(self) == list(other)
+ return set(self) == set(other)
+
+ def __del__(self):
+ self.clear() # remove circular references
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/compat.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/compat.py
new file mode 100644
index 0000000000..0f6e7947b9
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/compat.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import sys
+
+__all__ = ['queue', 'Event']
+
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+if sys.version_info < (2, 7):
+ from watchdog.utils.event_backport import Event
+else:
+ from threading import Event
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/decorators.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/decorators.py
new file mode 100644
index 0000000000..abb325c1c1
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/decorators.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Most of this code was obtained from the Python documentation online.
+
+"""Decorator utility functions.
+
+decorators:
+- synchronized
+- propertyx
+- accepts
+- returns
+- singleton
+- attrs
+- deprecated
+"""
+
+import functools
+import warnings
+import threading
+import sys
+
+
+def synchronized(lock=None):
+ """Decorator that synchronizes a method or a function with a mutex lock.
+
+ Example usage:
+
+ @synchronized()
+ def operation(self, a, b):
+ ...
+ """
+ if lock is None:
+ lock = threading.Lock()
+
+ def wrapper(function):
+ def new_function(*args, **kwargs):
+ lock.acquire()
+ try:
+ return function(*args, **kwargs)
+ finally:
+ lock.release()
+
+ return new_function
+
+ return wrapper
+
+
+def propertyx(function):
+ """Decorator to easily create properties in classes.
+
+ Example:
+
+ class Angle(object):
+ def __init__(self, rad):
+ self._rad = rad
+
+ @property
+ def rad():
+ def fget(self):
+ return self._rad
+ def fset(self, angle):
+ if isinstance(angle, Angle):
+ angle = angle.rad
+ self._rad = float(angle)
+
+ Arguments:
+ - `function`: The function to be decorated.
+ """
+ keys = ('fget', 'fset', 'fdel')
+ func_locals = {'doc': function.__doc__}
+
+ def probe_func(frame, event, arg):
+ if event == 'return':
+ locals = frame.f_locals
+ func_locals.update(dict((k, locals.get(k)) for k in keys))
+ sys.settrace(None)
+ return probe_func
+
+ sys.settrace(probe_func)
+ function()
+ return property(**func_locals)
+
+
+def accepts(*types):
+ """Decorator to ensure that the decorated function accepts the given types as arguments.
+
+ Example:
+ @accepts(int, (int,float))
+ @returns((int,float))
+ def func(arg1, arg2):
+ return arg1 * arg2
+ """
+
+ def check_accepts(f):
+ assert len(types) == f.__code__.co_argcount
+
+ def new_f(*args, **kwds):
+ for (a, t) in zip(args, types):
+ assert isinstance(a, t),\
+ "arg %r does not match %s" % (a, t)
+ return f(*args, **kwds)
+
+ new_f.__name__ = f.__name__
+ return new_f
+
+ return check_accepts
+
+
+def returns(rtype):
+ """Decorator to ensure that the decorated function returns the given
+ type as argument.
+
+ Example:
+ @accepts(int, (int,float))
+ @returns((int,float))
+ def func(arg1, arg2):
+ return arg1 * arg2
+ """
+
+ def check_returns(f):
+ def new_f(*args, **kwds):
+ result = f(*args, **kwds)
+ assert isinstance(result, rtype),\
+ "return value %r does not match %s" % (result, rtype)
+ return result
+
+ new_f.__name__ = f.__name__
+ return new_f
+
+ return check_returns
+
+
+def singleton(cls):
+ """Decorator to ensures a class follows the singleton pattern.
+
+ Example:
+ @singleton
+ class MyClass:
+ ...
+ """
+ instances = {}
+
+ def getinstance():
+ if cls not in instances:
+ instances[cls] = cls()
+ return instances[cls]
+
+ return getinstance
+
+
+def attrs(**kwds):
+ """Decorator to add attributes to a function.
+
+ Example:
+
+ @attrs(versionadded="2.2",
+ author="Guido van Rossum")
+ def mymethod(f):
+ ...
+ """
+
+ def decorate(f):
+ for k in kwds:
+ setattr(f, k, kwds[k])
+ return f
+
+ return decorate
+
+
+def deprecated(func):
+ """This is a decorator which can be used to mark functions
+ as deprecated. It will result in a warning being emitted
+ when the function is used.
+
+ ## Usage examples ##
+ @deprecated
+ def my_func():
+ pass
+
+ @other_decorators_must_be_upper
+ @deprecated
+ def my_func():
+ pass
+ """
+
+ @functools.wraps(func)
+ def new_func(*args, **kwargs):
+ warnings.warn_explicit(
+ "Call to deprecated function %(funcname)s." % {
+ 'funcname': func.__name__,
+ },
+ category=DeprecationWarning,
+ filename=func.__code__.co_filename,
+ lineno=func.__code__.co_firstlineno + 1
+ )
+ return func(*args, **kwargs)
+
+ return new_func
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/delayed_queue.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/delayed_queue.py
new file mode 100644
index 0000000000..6d98a50469
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/delayed_queue.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+import threading
+from collections import deque
+
+
+class DelayedQueue(object):
+
+ def __init__(self, delay):
+ self.delay = delay
+ self._lock = threading.Lock()
+ self._not_empty = threading.Condition(self._lock)
+ self._queue = deque()
+ self._closed = False
+
+ def put(self, element):
+ """Add element to queue."""
+ self._lock.acquire()
+ self._queue.append((element, time.time()))
+ self._not_empty.notify()
+ self._lock.release()
+
+ def close(self):
+ """Close queue, indicating no more items will be added."""
+ self._closed = True
+ # Interrupt the blocking _not_empty.wait() call in get
+ self._not_empty.acquire()
+ self._not_empty.notify()
+ self._not_empty.release()
+
+ def get(self):
+ """Remove and return an element from the queue, or this queue has been
+ closed raise the Closed exception.
+ """
+ while True:
+ # wait for element to be added to queue
+ self._not_empty.acquire()
+ while len(self._queue) == 0 and not self._closed:
+ self._not_empty.wait()
+
+ if self._closed:
+ self._not_empty.release()
+ return None
+ head, insert_time = self._queue[0]
+ self._not_empty.release()
+
+ # wait for delay
+ time_left = insert_time + self.delay - time.time()
+ while time_left > 0:
+ time.sleep(time_left)
+ time_left = insert_time + self.delay - time.time()
+
+ # return element if it's still in the queue
+ self._lock.acquire()
+ try:
+ if len(self._queue) > 0 and self._queue[0][0] is head:
+ self._queue.popleft()
+ return head
+ finally:
+ self._lock.release()
+
+ def remove(self, predicate):
+ """Remove and return the first items for which predicate is True,
+ ignoring delay."""
+ try:
+ self._lock.acquire()
+ for i, (elem, t) in enumerate(self._queue):
+ if predicate(elem):
+ del self._queue[i]
+ return elem
+ finally:
+ self._lock.release()
+ return None
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/dirsnapshot.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/dirsnapshot.py
new file mode 100644
index 0000000000..122e3cfc7a
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/dirsnapshot.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.utils.dirsnapshot
+:synopsis: Directory snapshots and comparison.
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+
+.. ADMONITION:: Where are the moved events? They "disappeared"
+
+ This implementation does not take partition boundaries
+ into consideration. It will only work when the directory
+ tree is entirely on the same file system. More specifically,
+ any part of the code that depends on inode numbers can
+ break if partition boundaries are crossed. In these cases,
+ the snapshot diff will represent file/directory movement as
+ created and deleted events.
+
+Classes
+-------
+.. autoclass:: DirectorySnapshot
+ :members:
+ :show-inheritance:
+
+.. autoclass:: DirectorySnapshotDiff
+ :members:
+ :show-inheritance:
+
+"""
+
+import errno
+import os
+from stat import S_ISDIR
+from resources.lib.watchdog.utils import platform
+from resources.lib.watchdog.utils import stat as default_stat
+
+
+class DirectorySnapshotDiff(object):
+ """
+ Compares two directory snapshots and creates an object that represents
+ the difference between the two snapshots.
+
+ :param ref:
+ The reference directory snapshot.
+ :type ref:
+ :class:`DirectorySnapshot`
+ :param snapshot:
+ The directory snapshot which will be compared
+ with the reference snapshot.
+ :type snapshot:
+ :class:`DirectorySnapshot`
+ """
+
+ def __init__(self, ref, snapshot):
+ created = snapshot.paths - ref.paths
+ deleted = ref.paths - snapshot.paths
+
+ # check that all unchanged paths have the same inode
+ for path in ref.paths & snapshot.paths:
+ if ref.inode(path) != snapshot.inode(path):
+ created.add(path)
+ deleted.add(path)
+
+ # find moved paths
+ moved = set()
+ for path in set(deleted):
+ inode = ref.inode(path)
+ new_path = snapshot.path(inode)
+ if new_path:
+ # file is not deleted but moved
+ deleted.remove(path)
+ moved.add((path, new_path))
+
+ for path in set(created):
+ inode = snapshot.inode(path)
+ old_path = ref.path(inode)
+ if old_path:
+ created.remove(path)
+ moved.add((old_path, path))
+
+ # find modified paths
+ # first check paths that have not moved
+ modified = set()
+ for path in ref.paths & snapshot.paths:
+ if ref.inode(path) == snapshot.inode(path):
+ if ref.mtime(path) != snapshot.mtime(path):
+ modified.add(path)
+
+ for (old_path, new_path) in moved:
+ if ref.mtime(old_path) != snapshot.mtime(new_path):
+ modified.add(old_path)
+
+ self._dirs_created = [path for path in created if snapshot.isdir(path)]
+ self._dirs_deleted = [path for path in deleted if ref.isdir(path)]
+ self._dirs_modified = [path for path in modified if ref.isdir(path)]
+ self._dirs_moved = [(frm, to) for (frm, to) in moved if ref.isdir(frm)]
+
+ self._files_created = list(created - set(self._dirs_created))
+ self._files_deleted = list(deleted - set(self._dirs_deleted))
+ self._files_modified = list(modified - set(self._dirs_modified))
+ self._files_moved = list(moved - set(self._dirs_moved))
+
+ @property
+ def files_created(self):
+ """List of files that were created."""
+ return self._files_created
+
+ @property
+ def files_deleted(self):
+ """List of files that were deleted."""
+ return self._files_deleted
+
+ @property
+ def files_modified(self):
+ """List of files that were modified."""
+ return self._files_modified
+
+ @property
+ def files_moved(self):
+ """
+ List of files that were moved.
+
+ Each event is a two-tuple the first item of which is the path
+ that has been renamed to the second item in the tuple.
+ """
+ return self._files_moved
+
+ @property
+ def dirs_modified(self):
+ """
+ List of directories that were modified.
+ """
+ return self._dirs_modified
+
+ @property
+ def dirs_moved(self):
+ """
+ List of directories that were moved.
+
+ Each event is a two-tuple the first item of which is the path
+ that has been renamed to the second item in the tuple.
+ """
+ return self._dirs_moved
+
+ @property
+ def dirs_deleted(self):
+ """
+ List of directories that were deleted.
+ """
+ return self._dirs_deleted
+
+ @property
+ def dirs_created(self):
+ """
+ List of directories that were created.
+ """
+ return self._dirs_created
+
+class DirectorySnapshot(object):
+ """
+ A snapshot of stat information of files in a directory.
+
+ :param path:
+ The directory path for which a snapshot should be taken.
+ :type path:
+ ``str``
+ :param recursive:
+ ``True`` if the entire directory tree should be included in the
+ snapshot; ``False`` otherwise.
+ :type recursive:
+ ``bool``
+ :param walker_callback:
+ .. deprecated:: 0.7.2
+ :param stat:
+ Use custom stat function that returns a stat structure for path.
+ Currently only st_dev, st_ino, st_mode and st_mtime are needed.
+
+ A function with the signature ``walker_callback(path, stat_info)``
+ which will be called for every entry in the directory tree.
+ :param listdir:
+ Use custom listdir function. See ``os.listdir`` for details.
+ """
+
+ def __init__(self, path, recursive=True,
+ walker_callback=(lambda p, s: None),
+ stat=default_stat,
+ listdir=os.listdir):
+ self._stat_info = {}
+ self._inode_to_path = {}
+
+ st = stat(path)
+ self._stat_info[path] = st
+ self._inode_to_path[(st.st_ino, st.st_dev)] = path
+
+ def walk(root):
+ try:
+ paths = [os.path.join(root, name) for name in listdir(root)]
+ except OSError as e:
+ # Directory may have been deleted between finding it in the directory
+ # list of its parent and trying to delete its contents. If this
+ # happens we treat it as empty.
+ if e.errno == errno.ENOENT:
+ return
+ else:
+ raise
+ entries = []
+ for p in paths:
+ try:
+ entries.append((p, stat(p)))
+ except OSError:
+ continue
+ for _ in entries:
+ yield _
+ if recursive:
+ for path, st in entries:
+ if S_ISDIR(st.st_mode):
+ for _ in walk(path):
+ yield _
+
+ for p, st in walk(path):
+ i = (st.st_ino, st.st_dev)
+ self._inode_to_path[i] = p
+ self._stat_info[p] = st
+ walker_callback(p, st)
+
+ @property
+ def paths(self):
+ """
+ Set of file/directory paths in the snapshot.
+ """
+ return set(self._stat_info.keys())
+
+ def path(self, id):
+ """
+ Returns path for id. None if id is unknown to this snapshot.
+ """
+ return self._inode_to_path.get(id)
+
+ def inode(self, path):
+ """ Returns an id for path. """
+ st = self._stat_info[path]
+ return (st.st_ino, st.st_dev)
+
+ def isdir(self, path):
+ return S_ISDIR(self._stat_info[path].st_mode)
+
+ def mtime(self, path):
+ return self._stat_info[path].st_mtime
+
+ def stat_info(self, path):
+ """
+ Returns a stat information object for the specified path from
+ the snapshot.
+
+ Attached information is subject to change. Do not use unless
+ you specify `stat` in constructor. Use :func:`inode`, :func:`mtime`,
+ :func:`isdir` instead.
+
+ :param path:
+ The path for which stat information should be obtained
+ from a snapshot.
+ """
+ return self._stat_info[path]
+
+ def __sub__(self, previous_dirsnap):
+ """Allow subtracting a DirectorySnapshot object instance from
+ another.
+
+ :returns:
+ A :class:`DirectorySnapshotDiff` object.
+ """
+ return DirectorySnapshotDiff(previous_dirsnap, self)
+
+ def __str__(self):
+ return self.__repr__()
+
+ def __repr__(self):
+ return str(self._stat_info)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/echo.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/echo.py
new file mode 100644
index 0000000000..18a35891d7
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/echo.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# echo.py: Tracing function calls using Python decorators.
+#
+# Written by Thomas Guest
+# Please see http://wordaligned.org/articles/echo
+#
+# Place into the public domain.
+
+""" Echo calls made to functions and methods in a module.
+
+"Echoing" a function call means printing out the name of the function
+and the values of its arguments before making the call (which is more
+commonly referred to as "tracing", but Python already has a trace module).
+
+Example: to echo calls made to functions in "my_module" do:
+
+ import echo
+ import my_module
+ echo.echo_module(my_module)
+
+Example: to echo calls made to functions in "my_module.my_class" do:
+
+ echo.echo_class(my_module.my_class)
+
+Alternatively, echo.echo can be used to decorate functions. Calls to the
+decorated function will be echoed.
+
+Example:
+
+ @echo.echo
+ def my_function(args):
+ pass
+"""
+import inspect
+import sys
+
+
+def name(item):
+ " Return an item's name. "
+ return item.__name__
+
+
+def is_classmethod(instancemethod):
+ " Determine if an instancemethod is a classmethod. "
+ return instancemethod.__self__ is not None
+
+
+def is_class_private_name(name):
+ " Determine if a name is a class private name. "
+ # Exclude system defined names such as __init__, __add__ etc
+ return name.startswith("__") and not name.endswith("__")
+
+
+def method_name(method):
+ """ Return a method's name.
+
+ This function returns the name the method is accessed by from
+ outside the class (i.e. it prefixes "private" methods appropriately).
+ """
+ mname = name(method)
+ if is_class_private_name(mname):
+ mname = "_%s%s" % (name(method.__self__.__class__), mname)
+ return mname
+
+
+def format_arg_value(arg_val):
+ """ Return a string representing a (name, value) pair.
+
+ >>> format_arg_value(('x', (1, 2, 3)))
+ 'x=(1, 2, 3)'
+ """
+ arg, val = arg_val
+ return "%s=%r" % (arg, val)
+
+
+def echo(fn, write=sys.stdout.write):
+ """ Echo calls to a function.
+
+ Returns a decorated version of the input function which "echoes" calls
+ made to it by writing out the function's name and the arguments it was
+ called with.
+ """
+ import functools
+ # Unpack function's arg count, arg names, arg defaults
+ code = fn.__code__
+ argcount = code.co_argcount
+ argnames = code.co_varnames[:argcount]
+ fn_defaults = fn.__defaults__ or list()
+ argdefs = dict(list(zip(argnames[-len(fn_defaults):], fn_defaults)))
+
+ @functools.wraps(fn)
+ def wrapped(*v, **k):
+ # Collect function arguments by chaining together positional,
+ # defaulted, extra positional and keyword arguments.
+ positional = list(map(format_arg_value, list(zip(argnames, v))))
+ defaulted = [format_arg_value((a, argdefs[a]))
+ for a in argnames[len(v):] if a not in k]
+ nameless = list(map(repr, v[argcount:]))
+ keyword = list(map(format_arg_value, list(k.items())))
+ args = positional + defaulted + nameless + keyword
+ write("%s(%s)\n" % (name(fn), ", ".join(args)))
+ return fn(*v, **k)
+
+ return wrapped
+
+
+def echo_instancemethod(klass, method, write=sys.stdout.write):
+ """ Change an instancemethod so that calls to it are echoed.
+
+ Replacing a classmethod is a little more tricky.
+ See: http://www.python.org/doc/current/ref/types.html
+ """
+ mname = method_name(method)
+ never_echo = "__str__", "__repr__", # Avoid recursion printing method calls
+ if mname in never_echo:
+ pass
+ elif is_classmethod(method):
+ setattr(klass, mname, classmethod(echo(method.__func__, write)))
+ else:
+ setattr(klass, mname, echo(method, write))
+
+
+def echo_class(klass, write=sys.stdout.write):
+ """ Echo calls to class methods and static functions
+ """
+ for _, method in inspect.getmembers(klass, inspect.ismethod):
+ echo_instancemethod(klass, method, write)
+ for _, fn in inspect.getmembers(klass, inspect.isfunction):
+ setattr(klass, name(fn), staticmethod(echo(fn, write)))
+
+
+def echo_module(mod, write=sys.stdout.write):
+ """ Echo calls to functions and methods in a module.
+ """
+ for fname, fn in inspect.getmembers(mod, inspect.isfunction):
+ setattr(mod, fname, echo(fn, write))
+ for _, klass in inspect.getmembers(mod, inspect.isclass):
+ echo_class(klass, write)
+
+if __name__ == "__main__":
+ import doctest
+
+ optionflags = doctest.ELLIPSIS
+ doctest.testfile('echoexample.txt', optionflags=optionflags)
+ doctest.testmod(optionflags=optionflags)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/event_backport.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/event_backport.py
new file mode 100644
index 0000000000..5c136e46d5
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/event_backport.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Backport of Event from py2.7 (method wait in py2.6 returns None)
+
+from threading import Condition, Lock
+
+
+class Event(object):
+
+ def __init__(self,):
+ self.__cond = Condition(Lock())
+ self.__flag = False
+
+ def isSet(self):
+ return self.__flag
+
+ is_set = isSet
+
+ def set(self):
+ self.__cond.acquire()
+ try:
+ self.__flag = True
+ self.__cond.notify_all()
+ finally:
+ self.__cond.release()
+
+ def clear(self):
+ self.__cond.acquire()
+ try:
+ self.__flag = False
+ finally:
+ self.__cond.release()
+
+ def wait(self, timeout=None):
+ self.__cond.acquire()
+ try:
+ if not self.__flag:
+ self.__cond.wait(timeout)
+ return self.__flag
+ finally:
+ self.__cond.release()
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/importlib2.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/importlib2.py
new file mode 100644
index 0000000000..5ad3ec5720
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/importlib2.py
@@ -0,0 +1,40 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2013 Peter M. Elias
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE
+
+
+def import_module(target, relative_to=None):
+ target_parts = target.split('.')
+ target_depth = target_parts.count('')
+ target_path = target_parts[target_depth:]
+ target = target[target_depth:]
+ fromlist = [target]
+ if target_depth and relative_to:
+ relative_parts = relative_to.split('.')
+ relative_to = '.'.join(relative_parts[:-(target_depth - 1) or None])
+ if len(target_path) > 1:
+ relative_to = '.'.join(filter(None, [relative_to]) + target_path[:-1])
+ fromlist = target_path[-1:]
+ target = fromlist[0]
+ elif not relative_to:
+ fromlist = []
+ mod = __import__(relative_to or target, globals(), locals(), fromlist)
+ return getattr(mod, target, mod)
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/platform.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/platform.py
new file mode 100644
index 0000000000..cc46192fe2
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/platform.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import sys
+
+PLATFORM_WINDOWS = 'windows'
+PLATFORM_LINUX = 'linux'
+PLATFORM_BSD = 'bsd'
+PLATFORM_DARWIN = 'darwin'
+PLATFORM_UNKNOWN = 'unknown'
+
+
+def get_platform_name():
+ if sys.platform.startswith("win"):
+ return PLATFORM_WINDOWS
+ elif sys.platform.startswith('darwin'):
+ return PLATFORM_DARWIN
+ elif sys.platform.startswith('linux'):
+ return PLATFORM_LINUX
+ elif sys.platform.startswith('bsd'):
+ return PLATFORM_BSD
+ else:
+ return PLATFORM_UNKNOWN
+
+__platform__ = get_platform_name()
+
+
+def is_linux():
+ return __platform__ == PLATFORM_LINUX
+
+
+def is_bsd():
+ return __platform__ == PLATFORM_BSD
+
+
+def is_darwin():
+ return __platform__ == PLATFORM_DARWIN
+
+
+def is_windows():
+ return __platform__ == PLATFORM_WINDOWS
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/unicode_paths.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/unicode_paths.py
new file mode 100644
index 0000000000..501a2f1512
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/unicode_paths.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Will Bond
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+
+import sys
+
+from watchdog.utils import platform
+
+try:
+ # Python 2
+ str_cls = unicode
+ bytes_cls = str
+except NameError:
+ # Python 3
+ str_cls = str
+ bytes_cls = bytes
+
+
+# This is used by Linux when the locale seems to be improperly set. UTF-8 tends
+# to be the encoding used by all distros, so this is a good fallback.
+fs_fallback_encoding = 'utf-8'
+fs_encoding = sys.getfilesystemencoding() or fs_fallback_encoding
+
+
+def encode(path):
+ if isinstance(path, str_cls):
+ try:
+ path = path.encode(fs_encoding, 'strict')
+ except UnicodeEncodeError:
+ if not platform.is_linux():
+ raise
+ path = path.encode(fs_fallback_encoding, 'strict')
+ return path
+
+
+def decode(path):
+ if isinstance(path, bytes_cls):
+ try:
+ path = path.decode(fs_encoding, 'strict')
+ except UnicodeDecodeError:
+ if not platform.is_linux():
+ raise
+ path = path.decode(fs_fallback_encoding, 'strict')
+ return path
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/utils/win32stat.py b/script.service.kodi.callbacks/resources/lib/watchdog/utils/win32stat.py
new file mode 100644
index 0000000000..c18d66f324
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/utils/win32stat.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Thomas Amland
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.utils.win32stat
+:synopsis: Implementation of stat with st_ino and st_dev support.
+
+Functions
+---------
+
+.. autofunction:: stat
+
+"""
+
+import ctypes
+import ctypes.wintypes
+import stat as stdstat
+from collections import namedtuple
+
+
+INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
+OPEN_EXISTING = 3
+FILE_READ_ATTRIBUTES = 0x80
+FILE_ATTRIBUTE_NORMAL = 0x80
+FILE_ATTRIBUTE_READONLY = 0x1
+FILE_ATTRIBUTE_DIRECTORY = 0x10
+FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
+
+
+class FILETIME(ctypes.Structure):
+ _fields_ = [("dwLowDateTime", ctypes.wintypes.DWORD),
+ ("dwHighDateTime", ctypes.wintypes.DWORD)]
+
+
+class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
+ _fields_ = [('dwFileAttributes', ctypes.wintypes.DWORD),
+ ('ftCreationTime', FILETIME),
+ ('ftLastAccessTime', FILETIME),
+ ('ftLastWriteTime', FILETIME),
+ ('dwVolumeSerialNumber', ctypes.wintypes.DWORD),
+ ('nFileSizeHigh', ctypes.wintypes.DWORD),
+ ('nFileSizeLow', ctypes.wintypes.DWORD),
+ ('nNumberOfLinks', ctypes.wintypes.DWORD),
+ ('nFileIndexHigh', ctypes.wintypes.DWORD),
+ ('nFileIndexLow', ctypes.wintypes.DWORD)]
+
+
+CreateFile = ctypes.windll.kernel32.CreateFileW
+CreateFile.restype = ctypes.wintypes.HANDLE
+CreateFile.argtypes = (
+ ctypes.c_wchar_p,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.c_void_p,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.HANDLE,
+)
+
+GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle
+GetFileInformationByHandle.restype = ctypes.wintypes.BOOL
+GetFileInformationByHandle.argtypes = (
+ ctypes.wintypes.HANDLE,
+ ctypes.wintypes.POINTER(BY_HANDLE_FILE_INFORMATION),
+)
+
+CloseHandle = ctypes.windll.kernel32.CloseHandle
+CloseHandle.restype = ctypes.wintypes.BOOL
+CloseHandle.argtypes = (ctypes.wintypes.HANDLE,)
+
+
+StatResult = namedtuple('StatResult', 'st_dev st_ino st_mode st_mtime')
+
+def _to_mode(attr):
+ m = 0
+ if (attr & FILE_ATTRIBUTE_DIRECTORY):
+ m |= stdstat.S_IFDIR | 0o111
+ else:
+ m |= stdstat.S_IFREG
+ if (attr & FILE_ATTRIBUTE_READONLY):
+ m |= 0o444
+ else:
+ m |= 0o666
+ return m
+
+def _to_unix_time(ft):
+ t = (ft.dwHighDateTime) << 32 | ft.dwLowDateTime
+ return (t / 10000000) - 11644473600
+
+def stat(path):
+ hfile = CreateFile(path,
+ FILE_READ_ATTRIBUTES,
+ 0,
+ None,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+ None)
+ if hfile == INVALID_HANDLE_VALUE:
+ raise ctypes.WinError()
+ info = BY_HANDLE_FILE_INFORMATION()
+ r = GetFileInformationByHandle(hfile, info)
+ CloseHandle(hfile)
+ if not r:
+ raise ctypes.WinError()
+ return StatResult(st_dev=info.dwVolumeSerialNumber,
+ st_ino=(info.nFileIndexHigh << 32) + info.nFileIndexLow,
+ st_mode=_to_mode(info.dwFileAttributes),
+ st_mtime=_to_unix_time(info.ftLastWriteTime)
+ )
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/version.py b/script.service.kodi.callbacks/resources/lib/watchdog/version.py
new file mode 100644
index 0000000000..92de4ee0ef
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/version.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# When updating this version number, please update the
+# ``docs/source/global.rst.inc`` file as well.
+VERSION_MAJOR = 0
+VERSION_MINOR = 8
+VERSION_BUILD = 3
+VERSION_INFO = (VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD)
+VERSION_STRING = "%d.%d.%d" % VERSION_INFO
+
+__version__ = VERSION_INFO
diff --git a/script.service.kodi.callbacks/resources/lib/watchdog/watchmedo.py b/script.service.kodi.callbacks/resources/lib/watchdog/watchmedo.py
new file mode 100644
index 0000000000..ce891f83b8
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/lib/watchdog/watchmedo.py
@@ -0,0 +1,577 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Yesudeep Mangalapilly
+# Copyright 2012 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+:module: watchdog.watchmedo
+:author: yesudeep@google.com (Yesudeep Mangalapilly)
+:synopsis: ``watchmedo`` shell script utility.
+"""
+
+import os.path
+import sys
+import yaml
+import time
+import logging
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ try:
+ from StringIO import StringIO
+ except ImportError:
+ from io import StringIO
+
+from argh import arg, aliases, ArghParser, expects_obj
+from watchdog.version import VERSION_STRING
+from watchdog.utils import load_class
+
+
+logging.basicConfig(level=logging.INFO)
+
+CONFIG_KEY_TRICKS = 'tricks'
+CONFIG_KEY_PYTHON_PATH = 'python-path'
+
+
+def path_split(pathname_spec, separator=os.path.sep):
+ """
+ Splits a pathname specification separated by an OS-dependent separator.
+
+ :param pathname_spec:
+ The pathname specification.
+ :param separator:
+ (OS Dependent) `:` on Unix and `;` on Windows or user-specified.
+ """
+ return list(pathname_spec.split(separator))
+
+
+def add_to_sys_path(pathnames, index=0):
+ """
+ Adds specified paths at specified index into the sys.path list.
+
+ :param paths:
+ A list of paths to add to the sys.path
+ :param index:
+ (Default 0) The index in the sys.path list where the paths will be
+ added.
+ """
+ for pathname in pathnames[::-1]:
+ sys.path.insert(index, pathname)
+
+
+def load_config(tricks_file_pathname):
+ """
+ Loads the YAML configuration from the specified file.
+
+ :param tricks_file_path:
+ The path to the tricks configuration file.
+ :returns:
+ A dictionary of configuration information.
+ """
+ f = open(tricks_file_pathname, 'rb')
+ content = f.read()
+ f.close()
+ config = yaml.load(content)
+ return config
+
+
+def parse_patterns(patterns_spec, ignore_patterns_spec, separator=';'):
+ """
+ Parses pattern argument specs and returns a two-tuple of
+ (patterns, ignore_patterns).
+ """
+ patterns = patterns_spec.split(separator)
+ ignore_patterns = ignore_patterns_spec.split(separator)
+ if ignore_patterns == ['']:
+ ignore_patterns = []
+ return (patterns, ignore_patterns)
+
+
+def observe_with(observer, event_handler, pathnames, recursive):
+ """
+ Single observer thread with a scheduled path and event handler.
+
+ :param observer:
+ The observer thread.
+ :param event_handler:
+ Event handler which will be called in response to file system events.
+ :param pathnames:
+ A list of pathnames to monitor.
+ :param recursive:
+ ``True`` if recursive; ``False`` otherwise.
+ """
+ for pathname in set(pathnames):
+ observer.schedule(event_handler, pathname, recursive)
+ observer.start()
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ observer.stop()
+ observer.join()
+
+
+def schedule_tricks(observer, tricks, pathname, recursive):
+ """
+ Schedules tricks with the specified observer and for the given watch
+ path.
+
+ :param observer:
+ The observer thread into which to schedule the trick and watch.
+ :param tricks:
+ A list of tricks.
+ :param pathname:
+ A path name which should be watched.
+ :param recursive:
+ ``True`` if recursive; ``False`` otherwise.
+ """
+ for trick in tricks:
+ for name, value in list(trick.items()):
+ TrickClass = load_class(name)
+ handler = TrickClass(**value)
+ trick_pathname = getattr(handler, 'source_directory', None) or pathname
+ observer.schedule(handler, trick_pathname, recursive)
+
+
+@aliases('tricks')
+@arg('files',
+ nargs='*',
+ help='perform tricks from given file')
+@arg('--python-path',
+ default='.',
+ help='paths separated by %s to add to the python path' % os.path.sep)
+@arg('--interval',
+ '--timeout',
+ dest='timeout',
+ default=1.0,
+ help='use this as the polling interval/blocking timeout')
+@arg('--recursive',
+ default=True,
+ help='recursively monitor paths')
+@expects_obj
+def tricks_from(args):
+ """
+ Subcommand to execute tricks from a tricks configuration file.
+
+ :param args:
+ Command line argument options.
+ """
+ from watchdog.observers import Observer
+
+ add_to_sys_path(path_split(args.python_path))
+ observers = []
+ for tricks_file in args.files:
+ observer = Observer(timeout=args.timeout)
+
+ if not os.path.exists(tricks_file):
+ raise IOError("cannot find tricks file: %s" % tricks_file)
+
+ config = load_config(tricks_file)
+
+ try:
+ tricks = config[CONFIG_KEY_TRICKS]
+ except KeyError:
+ raise KeyError("No `%s' key specified in %s." % (
+ CONFIG_KEY_TRICKS, tricks_file))
+
+ if CONFIG_KEY_PYTHON_PATH in config:
+ add_to_sys_path(config[CONFIG_KEY_PYTHON_PATH])
+
+ dir_path = os.path.dirname(tricks_file)
+ if not dir_path:
+ dir_path = os.path.relpath(os.getcwd())
+ schedule_tricks(observer, tricks, dir_path, args.recursive)
+ observer.start()
+ observers.append(observer)
+
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ for o in observers:
+ o.unschedule_all()
+ o.stop()
+ for o in observers:
+ o.join()
+
+
+@aliases('generate-tricks-yaml')
+@arg('trick_paths',
+ nargs='*',
+ help='Dotted paths for all the tricks you want to generate')
+@arg('--python-path',
+ default='.',
+ help='paths separated by %s to add to the python path' % os.path.sep)
+@arg('--append-to-file',
+ default=None,
+ help='appends the generated tricks YAML to a file; \
+if not specified, prints to standard output')
+@arg('-a',
+ '--append-only',
+ dest='append_only',
+ default=False,
+ help='if --append-to-file is not specified, produces output for \
+appending instead of a complete tricks yaml file.')
+@expects_obj
+def tricks_generate_yaml(args):
+ """
+ Subcommand to generate Yaml configuration for tricks named on the command
+ line.
+
+ :param args:
+ Command line argument options.
+ """
+ python_paths = path_split(args.python_path)
+ add_to_sys_path(python_paths)
+ output = StringIO()
+
+ for trick_path in args.trick_paths:
+ TrickClass = load_class(trick_path)
+ output.write(TrickClass.generate_yaml())
+
+ content = output.getvalue()
+ output.close()
+
+ header = yaml.dump({CONFIG_KEY_PYTHON_PATH: python_paths})
+ header += "%s:\n" % CONFIG_KEY_TRICKS
+ if args.append_to_file is None:
+ # Output to standard output.
+ if not args.append_only:
+ content = header + content
+ sys.stdout.write(content)
+ else:
+ if not os.path.exists(args.append_to_file):
+ content = header + content
+ output = open(args.append_to_file, 'ab')
+ output.write(content)
+ output.close()
+
+
+@arg('directories',
+ nargs='*',
+ default='.',
+ help='directories to watch.')
+@arg('-p',
+ '--pattern',
+ '--patterns',
+ dest='patterns',
+ default='*',
+ help='matches event paths with these patterns (separated by ;).')
+@arg('-i',
+ '--ignore-pattern',
+ '--ignore-patterns',
+ dest='ignore_patterns',
+ default='',
+ help='ignores event paths with these patterns (separated by ;).')
+@arg('-D',
+ '--ignore-directories',
+ dest='ignore_directories',
+ default=False,
+ help='ignores events for directories')
+@arg('-R',
+ '--recursive',
+ dest='recursive',
+ default=False,
+ help='monitors the directories recursively')
+@arg('--interval',
+ '--timeout',
+ dest='timeout',
+ default=1.0,
+ help='use this as the polling interval/blocking timeout')
+@arg('--trace',
+ default=False,
+ help='dumps complete dispatching trace')
+@arg('--debug-force-polling',
+ default=False,
+ help='[debug] forces polling')
+@arg('--debug-force-kqueue',
+ default=False,
+ help='[debug] forces BSD kqueue(2)')
+@arg('--debug-force-winapi',
+ default=False,
+ help='[debug] forces Windows API')
+@arg('--debug-force-winapi-async',
+ default=False,
+ help='[debug] forces Windows API + I/O completion')
+@arg('--debug-force-fsevents',
+ default=False,
+ help='[debug] forces Mac OS X FSEvents')
+@arg('--debug-force-inotify',
+ default=False,
+ help='[debug] forces Linux inotify(7)')
+@expects_obj
+def log(args):
+ """
+ Subcommand to log file system events to the console.
+
+ :param args:
+ Command line argument options.
+ """
+ from watchdog.utils import echo
+ from watchdog.tricks import LoggerTrick
+
+ if args.trace:
+ echo.echo_class(LoggerTrick)
+
+ patterns, ignore_patterns =\
+ parse_patterns(args.patterns, args.ignore_patterns)
+ handler = LoggerTrick(patterns=patterns,
+ ignore_patterns=ignore_patterns,
+ ignore_directories=args.ignore_directories)
+ if args.debug_force_polling:
+ from watchdog.observers.polling import PollingObserver as Observer
+ elif args.debug_force_kqueue:
+ from watchdog.observers.kqueue import KqueueObserver as Observer
+ elif args.debug_force_winapi_async:
+ from watchdog.observers.read_directory_changes_async import\
+ WindowsApiAsyncObserver as Observer
+ elif args.debug_force_winapi:
+ from watchdog.observers.read_directory_changes import\
+ WindowsApiObserver as Observer
+ elif args.debug_force_inotify:
+ from watchdog.observers.inotify import InotifyObserver as Observer
+ elif args.debug_force_fsevents:
+ from watchdog.observers.fsevents import FSEventsObserver as Observer
+ else:
+ # Automatically picks the most appropriate observer for the platform
+ # on which it is running.
+ from watchdog.observers import Observer
+ observer = Observer(timeout=args.timeout)
+ observe_with(observer, handler, args.directories, args.recursive)
+
+
+@arg('directories',
+ nargs='*',
+ default='.',
+ help='directories to watch')
+@arg('-c',
+ '--command',
+ dest='command',
+ default=None,
+ help='''shell command executed in response to matching events.
+These interpolation variables are available to your command string::
+
+ ${watch_src_path} - event source path;
+ ${watch_dest_path} - event destination path (for moved events);
+ ${watch_event_type} - event type;
+ ${watch_object} - ``file`` or ``directory``
+
+Note::
+ Please ensure you do not use double quotes (") to quote
+ your command string. That will force your shell to
+ interpolate before the command is processed by this
+ subcommand.
+
+Example option usage::
+
+ --command='echo "${watch_src_path}"'
+''')
+@arg('-p',
+ '--pattern',
+ '--patterns',
+ dest='patterns',
+ default='*',
+ help='matches event paths with these patterns (separated by ;).')
+@arg('-i',
+ '--ignore-pattern',
+ '--ignore-patterns',
+ dest='ignore_patterns',
+ default='',
+ help='ignores event paths with these patterns (separated by ;).')
+@arg('-D',
+ '--ignore-directories',
+ dest='ignore_directories',
+ default=False,
+ help='ignores events for directories')
+@arg('-R',
+ '--recursive',
+ dest='recursive',
+ default=False,
+ help='monitors the directories recursively')
+@arg('--interval',
+ '--timeout',
+ dest='timeout',
+ default=1.0,
+ help='use this as the polling interval/blocking timeout')
+@arg('-w', '--wait',
+ dest='wait_for_process',
+ action='store_true',
+ default=False,
+ help="wait for process to finish to avoid multiple simultaneous instances")
+@arg('-W', '--drop',
+ dest='drop_during_process',
+ action='store_true',
+ default=False,
+ help="Ignore events that occur while command is still being executed " \
+ "to avoid multiple simultaneous instances")
+@arg('--debug-force-polling',
+ default=False,
+ help='[debug] forces polling')
+@expects_obj
+def shell_command(args):
+ """
+ Subcommand to execute shell commands in response to file system events.
+
+ :param args:
+ Command line argument options.
+ """
+ from watchdog.tricks import ShellCommandTrick
+
+ if not args.command:
+ args.command = None
+
+ if args.debug_force_polling:
+ from watchdog.observers.polling import PollingObserver as Observer
+ else:
+ from watchdog.observers import Observer
+
+ patterns, ignore_patterns = parse_patterns(args.patterns,
+ args.ignore_patterns)
+ handler = ShellCommandTrick(shell_command=args.command,
+ patterns=patterns,
+ ignore_patterns=ignore_patterns,
+ ignore_directories=args.ignore_directories,
+ wait_for_process=args.wait_for_process,
+ drop_during_process=args.drop_during_process)
+ observer = Observer(timeout=args.timeout)
+ observe_with(observer, handler, args.directories, args.recursive)
+
+
+@arg('command',
+ help='''Long-running command to run in a subprocess.
+''')
+@arg('command_args',
+ metavar='arg',
+ nargs='*',
+ help='''Command arguments.
+
+Note: Use -- before the command arguments, otherwise watchmedo will
+try to interpret them.
+''')
+@arg('-d',
+ '--directory',
+ dest='directories',
+ metavar='directory',
+ action='append',
+ help='Directory to watch. Use another -d or --directory option '
+ 'for each directory.')
+@arg('-p',
+ '--pattern',
+ '--patterns',
+ dest='patterns',
+ default='*',
+ help='matches event paths with these patterns (separated by ;).')
+@arg('-i',
+ '--ignore-pattern',
+ '--ignore-patterns',
+ dest='ignore_patterns',
+ default='',
+ help='ignores event paths with these patterns (separated by ;).')
+@arg('-D',
+ '--ignore-directories',
+ dest='ignore_directories',
+ default=False,
+ help='ignores events for directories')
+@arg('-R',
+ '--recursive',
+ dest='recursive',
+ default=False,
+ help='monitors the directories recursively')
+@arg('--interval',
+ '--timeout',
+ dest='timeout',
+ default=1.0,
+ help='use this as the polling interval/blocking timeout')
+@arg('--signal',
+ dest='signal',
+ default='SIGINT',
+ help='stop the subprocess with this signal (default SIGINT)')
+@arg('--kill-after',
+ dest='kill_after',
+ default=10.0,
+ help='when stopping, kill the subprocess after the specified timeout '
+ '(default 10)')
+@expects_obj
+def auto_restart(args):
+ """
+ Subcommand to start a long-running subprocess and restart it
+ on matched events.
+
+ :param args:
+ Command line argument options.
+ """
+ from watchdog.observers import Observer
+ from watchdog.tricks import AutoRestartTrick
+ import signal
+ import re
+
+ if not args.directories:
+ args.directories = ['.']
+
+ # Allow either signal name or number.
+ if re.match('^SIG[A-Z]+$', args.signal):
+ stop_signal = getattr(signal, args.signal)
+ else:
+ stop_signal = int(args.signal)
+
+ # Handle SIGTERM in the same manner as SIGINT so that
+ # this program has a chance to stop the child process.
+ def handle_sigterm(_signum, _frame):
+ raise KeyboardInterrupt()
+
+ signal.signal(signal.SIGTERM, handle_sigterm)
+
+ patterns, ignore_patterns = parse_patterns(args.patterns,
+ args.ignore_patterns)
+ command = [args.command]
+ command.extend(args.command_args)
+ handler = AutoRestartTrick(command=command,
+ patterns=patterns,
+ ignore_patterns=ignore_patterns,
+ ignore_directories=args.ignore_directories,
+ stop_signal=stop_signal,
+ kill_after=args.kill_after)
+ handler.start()
+ observer = Observer(timeout=args.timeout)
+ observe_with(observer, handler, args.directories, args.recursive)
+ handler.stop()
+
+
+epilog = """Copyright 2011 Yesudeep Mangalapilly .
+Copyright 2012 Google, Inc.
+
+Licensed under the terms of the Apache license, version 2.0. Please see
+LICENSE in the source code for more information."""
+
+parser = ArghParser(epilog=epilog)
+parser.add_commands([tricks_from,
+ tricks_generate_yaml,
+ log,
+ shell_command,
+ auto_restart])
+parser.add_argument('--version',
+ action='version',
+ version='%(prog)s ' + VERSION_STRING)
+
+
+def main():
+ """Entry-point function."""
+ parser.dispatch()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/script.service.kodi.callbacks/resources/settings.xml b/script.service.kodi.callbacks/resources/settings.xml
new file mode 100644
index 0000000000..11118134ba
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/settings.xml
@@ -0,0 +1,1214 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/resources/skins/Default/720p/DialogTextBox.xml b/script.service.kodi.callbacks/resources/skins/Default/720p/DialogTextBox.xml
new file mode 100644
index 0000000000..15d67c6285
--- /dev/null
+++ b/script.service.kodi.callbacks/resources/skins/Default/720p/DialogTextBox.xml
@@ -0,0 +1,101 @@
+
+
+ 10
+ 1
+
+ 1
+ 275
+ 25
+
+
+
+ 0
+ 0
+ 730
+ 650
+ DialogBack.png
+
+
+ Dialog Header image
+ 40
+ 16
+ 650
+ 40
+ dialogheader.png
+
+
+ header label
+ 100
+ 20
+ 530
+ 30
+ font13_title
+
+ center
+ center
+ selected
+ black
+
+
+ Close Window button
+ 650
+ 15
+ 64
+ 32
+
+ -
+ PreviousMenu
+ DialogCloseButton-focus.png
+ DialogCloseButton.png
+ 110
+ 110
+ 110
+ 110
+ system.getbool(input.enablemouse)
+
+
+ text box
+ 50
+ 70
+ 600
+ 400
+ true
+ left
+ white
+ 112
+
+ font13
+
+
+ 695
+ 120
+ 25
+ 380
+ ScrollBarV.png
+ ScrollBarV_bar.png
+ ScrollBarV_bar_focus.png
+ ScrollBarNib.png
+ ScrollBarNib.png
+ 2
+ 2
+ false
+ vertical
+
+
+ OK button
+ 265
+ 575
+ 200
+ 40
+
+ font12_title
+ white
+ white
+ center
+ 110
+ 110
+ 110
+ 110
+
+
+
\ No newline at end of file
diff --git a/script.service.kodi.callbacks/restartaddon.py b/script.service.kodi.callbacks/restartaddon.py
new file mode 100644
index 0000000000..592d83b547
--- /dev/null
+++ b/script.service.kodi.callbacks/restartaddon.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+import xbmc
+import sys
+
+addonid = sys.argv[1]
+
+xbmc.executeJSONRPC('{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled", "params":{"addonid":"%s","enabled":"toggle"},"id":1}' % addonid)
+xbmc.log(msg='***** Toggling addon enabled 1: %s' % addonid)
+xbmc.sleep(1000)
+xbmc.executeJSONRPC('{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled", "params":{"addonid":"%s","enabled":"toggle"},"id":1}' % addonid)
+xbmc.log(msg='***** Toggling addon enabled 2: %s' % addonid)
diff --git a/script.service.kodi.callbacks/script.py b/script.service.kodi.callbacks/script.py
new file mode 100644
index 0000000000..5fb6b4a608
--- /dev/null
+++ b/script.service.kodi.callbacks/script.py
@@ -0,0 +1,190 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 KenV99
+#
+# 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 .
+#
+scriptdebug = False # TODO: check
+testTasks = False # TODO: check
+
+import os
+import sys
+
+import resources.lib.pubsub as PubSub_Threaded
+import xbmc
+import xbmcgui
+from default import branch
+from resources.lib.kodilogging import KodiLogger
+from resources.lib.settings import Settings
+from resources.lib.subscriberfactory import SubscriberFactory
+from resources.lib.utils.debugger import startdebugger
+from resources.lib.utils.kodipathtools import translatepath
+from resources.lib.utils.poutil import KodiPo
+
+
+def notify(msg):
+ dialog = xbmcgui.Dialog()
+ dialog.notification('Kodi Callabacks', msg, xbmcgui.NOTIFICATION_INFO, 5000)
+
+
+kodipo = KodiPo()
+_ = kodipo.getLocalizedString
+log = KodiLogger.log
+
+
+def test(key):
+ global log
+ log = KodiLogger.log
+ import resources.lib.tests.direct_test as direct_test
+ from resources.lib.events import Events
+ import traceback
+ log(msg=_('Running Test for Event: %s') % key)
+ events = Events().AllEvents
+ settings = Settings()
+ settings.getSettings()
+ if settings.general['elevate_loglevel'] is True:
+ KodiLogger.setLogLevel(xbmc.LOGNOTICE)
+ else:
+ KodiLogger.setLogLevel(xbmc.LOGDEBUG)
+ log(msg=_('Settings for test read'))
+ evtsettings = settings.events[key]
+ topic = settings.topicFromSettingsEvent(key)
+ task_key = settings.events[key]['task']
+ tasksettings = settings.tasks[task_key]
+ testlogger = direct_test.TestLogger()
+ log(msg=_('Creating subscriber for test'))
+ subscriberfactory = SubscriberFactory(settings, testlogger)
+ subscriber = subscriberfactory.createSubscriber(key)
+ if subscriber is not None:
+ log(msg=_('Test subscriber created successfully'))
+ try:
+ kwargs = events[evtsettings['type']]['expArgs']
+ except KeyError:
+ kwargs = {}
+ testRH = direct_test.TestHandler(direct_test.testMsg(subscriber.taskmanagers[0], tasksettings, kwargs))
+ subscriber.taskmanagers[0].returnHandler = testRH.testReturnHandler
+ # Run test
+ log(msg=_('Running test'))
+ nMessage = PubSub_Threaded.Message(topic=topic, **kwargs)
+ try:
+ subscriber.notify(nMessage)
+ except Exception:
+ msg = _('Unspecified error during testing')
+ e = sys.exc_info()[0]
+ if hasattr(e, 'message'):
+ msg = str(e.message)
+ msg = msg + '\n' + traceback.format_exc()
+ log(msg=msg)
+ msgList = msg.split('\n')
+ import resources.lib.dialogtb as dialogtb
+ dialogtb.show_textbox('Error', msgList)
+ else:
+ log(msg=_('Test subscriber creation failed due to errors'))
+ msgList = testlogger.retrieveLogAsList()
+ import resources.lib.dialogtb as dialogtb
+ dialogtb.show_textbox('Error', msgList)
+
+ xbmc.sleep(2000)
+
+
+if __name__ == '__main__':
+ dryrun = False
+ addonid = 'script.service.kodi.callbacks'
+
+ if len(sys.argv) > 1:
+ if scriptdebug is True:
+ startdebugger()
+ dryrun = True
+
+ if sys.argv[1] == 'regen':
+ from resources.lib.kodisettings.generate_xml import generate_settingsxml
+
+ generate_settingsxml()
+ dialog = xbmcgui.Dialog()
+ msg = _('Settings Regenerated')
+ dialog.ok(_('Kodi Callbacks'), msg)
+
+ elif sys.argv[1] == 'test':
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ from resources.lib.tests.testTasks import testTasks
+
+ tt = testTasks()
+ tt.runTests()
+ dialog = xbmcgui.Dialog()
+ msg = _('Native Task Testing Complete - see log for results')
+ dialog.notification(_('Kodi Callbacks'), msg, xbmcgui.NOTIFICATION_INFO, 5000)
+
+ elif sys.argv[1] == 'updatefromzip':
+ from resources.lib.utils.updateaddon import UpdateAddon
+
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ dialog = xbmcgui.Dialog()
+ zipfn = dialog.browse(1, _('Locate zip file'), 'files', '.zip', False, False, translatepath('~'))
+ if zipfn != translatepath('~'):
+ if os.path.isfile(zipfn):
+ ua = UpdateAddon(addonid)
+ ua.installFromZip(zipfn, updateonly=True, dryrun=dryrun)
+ else:
+ dialog.ok(_('Kodi Callbacks'), _('Incorrect path'))
+
+ elif sys.argv[1] == 'restorebackup':
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ dialog = xbmcgui.Dialog()
+ zipfn = dialog.browse(1, _('Locate backup zip file'), 'files', '.zip', False, False,
+ translatepath('special://addondata/backup/'))
+ if zipfn != translatepath('special://addondata/backup/'):
+ from resources.lib.utils.updateaddon import UpdateAddon
+
+ ua = UpdateAddon(addonid)
+ ua.installFromZip(zipfn, updateonly=False, dryrun=dryrun)
+
+ elif sys.argv[1] == 'lselector':
+ from resources.lib.utils.selector import selectordialog
+
+ try:
+ result = selectordialog(sys.argv[2:])
+ except (SyntaxError, TypeError) as e:
+ xbmc.log(msg='Error: %s' % str(e), level=xbmc.LOGERROR)
+
+ elif sys.argv[1] == 'logsettings':
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ settings = Settings()
+ settings.getSettings()
+ settings.logSettings()
+ dialog = xbmcgui.Dialog()
+ msg = _('Settings written to log')
+ dialog.ok(_('Kodi Callbacks'), msg)
+
+ elif branch != 'master' and sys.argv[1] == 'checkupdate':
+ try:
+ from resources.lib.utils.githubtools import processargs
+ except ImportError:
+ pass
+ else:
+ processargs(sys.argv)
+
+ else:
+ # Direct Event/Task Testing
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ eventId = sys.argv[1]
+ test(eventId)
+
+ elif testTasks:
+ KodiLogger.setLogLevel(KodiLogger.LOGNOTICE)
+ startdebugger()
+ from resources.lib.tests.testTasks import testTasks
+
+ tt = testTasks()
+ tt.runTests()
diff --git a/script.service.kodi.callbacks/testme.py b/script.service.kodi.callbacks/testme.py
new file mode 100644
index 0000000000..5f32bc842d
--- /dev/null
+++ b/script.service.kodi.callbacks/testme.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 KenV99
+#
+# 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 .
+#
+
+######################################################################
+#
+# Use to test scripts and arguments
+#
+######################################################################
+
+import sys
+
+
+
+def stripquotes(st):
+ if st.startswith('"') and st.endswith('"'):
+ return st[1:-1].strip()
+ else:
+ return st.strip()
+
+def showNotification(args, kwargs):
+ # Note that using execfile will not allow for module level imports!
+ try:
+ import xbmcgui
+ except ImportError:
+ pass
+ else:
+ mdialog = xbmcgui.Dialog()
+ argmsg = ", ".join(args)
+ kwargmsg = []
+ for key in kwargs.keys():
+ kwargmsg.append('%s:%s' % (key, kwargs[key]))
+ mdialog.ok('Test', 'args: %s\nkwargs:: %s' % (argmsg, ', '.join(kwargmsg)))
+
+def processargs(args, kwargs):
+ nargs = []
+ if isinstance(args, str):
+ kwargs = {}
+ args = args.split(' ')
+ nargs = []
+ for arg in args:
+ if ":" in arg:
+ tmp = arg.split(":", 1)
+ try:
+ key = tmp[0]
+ val = tmp[1]
+ val2 = stripquotes(val)
+ kwargs[key] = val2
+ except (KeyError, LookupError):
+ pass
+ else:
+ nargs.append(stripquotes(arg))
+ elif isinstance(args, list):
+ nargs = args
+ if kwargs is None:
+ kwargs = {}
+ return nargs, kwargs
+
+def run(args=None, kwargs=None):
+ args, kwargs = processargs(args, kwargs)
+ showNotification(args, kwargs)
+
+if __name__ == '__main__' or __name__ == 'Tasks':
+ xargs = []
+ kwargs = {}
+ if __name__ == '__main__':
+ sysargv = sys.argv[1:]
+ else:
+ sysargv = locals()['args'].strip().split(' ')
+ for i, xarg in enumerate(sysargv):
+ if ":" in xarg:
+ key, entry = xarg.split(':', 1)
+ kwargs[key] = stripquotes(entry)
+ else:
+ xargs.append(stripquotes(xarg))
+ run(xargs, kwargs)
+
+
diff --git a/script.service.kodi.callbacks/timestamp.json b/script.service.kodi.callbacks/timestamp.json
new file mode 100644
index 0000000000..78aaf4b64e
--- /dev/null
+++ b/script.service.kodi.callbacks/timestamp.json
@@ -0,0 +1 @@
+{"resources/lib/tests/__init__.py": "2016-01-14T15:24:21Z", "resources/lib/watchdog/observers/polling.py": "2016-01-17T07:10:59Z", "resources/__init__.py": "2016-01-14T15:24:21Z", "resources/lib/publishers/__init__.py": "2016-01-14T15:24:21Z", "resources/lib/pathtools/README.rst": "2016-01-25T10:11:22Z", "resources/lib/taskExample.py": "2016-03-07T15:48:16Z", "resources/lib/pathtools/README": "2016-01-25T10:11:22Z", "resources/skins/Default/720p/DialogTextBox.xml": "2016-01-14T15:24:21Z", "resources/lib/watchdog/utils/delayed_queue.py": "2016-01-17T07:10:59Z", "resources/lib/kodisettings/generate_xml.py": "2016-03-31T17:27:35Z", "resources/lib/watchdog/watchmedo.py": "2016-01-17T07:10:59Z", ".gitignore": "2016-04-04T07:13:26Z", "resources/lib/tests/tstScript.sh": "2016-02-27T07:40:29Z", "default.py": "2016-04-16T05:33:46Z", "resources/lib/watchdog/LICENSE.txt": "2016-01-25T10:10:48Z", "resources/lib/watchdog/utils/event_backport.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/utils/compat.py": "2016-01-17T07:10:59Z", "resources/lib/publishers/watchdogStartup.py": "2016-03-29T14:16:26Z", "resources/lib/helpers/onQuit.sh": "2016-01-22T14:59:31Z", "resources/lib/pathtools/AUTHORS": "2016-01-25T10:11:22Z", "resources/lib/tasks/taskScript.py": "2016-03-06T11:30:01Z", "resources/lib/tasks/__init__.py": "2016-01-14T15:24:21Z", "resources/lib/publishers/schedule.py": "2016-04-16T05:34:34Z", "resources/lib/tests/testTasks.py": "2016-03-29T14:16:28Z", "resources/lib/kodisettings/struct.py": "2016-04-04T07:13:25Z", "icon.png": "2016-03-27T10:13:37Z", "resources/lib/watchdog/observers/__init__.py": "2016-01-19T05:48:22Z", "resources/lib/utils/selector.py": "2016-03-10T17:12:51Z", "resources/language/English/strings.po": "2016-04-16T05:36:00Z", "resources/lib/pubsub.py": "2016-04-04T07:13:25Z", "resources/lib/watchdog/observers/fsevents.py": "2016-01-17T07:10:59Z", "resources/lib/tasks/taskHttp.py": "2016-04-09T09:38:56Z", "resources/lib/events.py": "2016-04-02T09:09:38Z", "resources/lib/helpers/onQuit.bat": "2016-01-22T15:00:23Z", "resources/lib/subscriberfactory.py": "2016-02-26T13:45:16Z", "resources/lib/watchdog/AUTHORS": "2016-01-25T10:10:48Z", "resources/lib/utils/detectPath.py": "2016-02-27T06:41:25Z", "resources/lib/publishers/dummy.py": "2016-03-29T14:16:26Z", "resources/lib/kodilogging.py": "2016-04-04T07:13:26Z", "resources/lib/pathtools/__init__.py": "2016-01-17T07:10:43Z", "resources/lib/watchdog/observers/inotify_c.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/tricks/__init__.py": "2016-01-17T07:10:59Z", "script.py": "2016-04-14T06:35:41Z", "resources/lib/helpers/sudohelper.sh": "2016-01-28T07:46:47Z", "resources/settings.xml": "2016-04-16T05:35:59Z", "resources/lib/watchdog/utils/dirsnapshot.py": "2016-02-01T11:27:31Z", "resources/lib/watchdog/utils/bricks.py": "2016-01-17T07:10:59Z", "resources/lib/tasks/taskPython.py": "2016-03-06T11:30:01Z", "resources/lib/publishers/loop.py": "2016-03-29T14:16:26Z", "resources/lib/kodisettings/__init__.py": "2016-03-06T11:30:01Z", "timestamp.json": "2016-04-16T05:36:06Z", "testme.py": "2016-01-27T17:12:23Z", "resources/lib/watchdog/version.py": "2016-01-17T07:10:59Z", "resources/lib/utils/updateaddon.py": "2016-04-04T07:13:26Z", "resources/lib/watchdog/utils/unicode_paths.py": "2016-01-17T07:10:59Z", "resources/lib/publisherfactory.py": "2016-03-24T17:00:38Z", "resources/lib/schedule/__init__.py": "2016-02-26T13:45:25Z", "resources/lib/watchdog/__init__.py": "2016-01-17T07:10:59Z", "addon.xml": "2016-04-14T06:35:41Z", "resources/lib/watchdog/utils/platform.py": "2016-01-17T07:10:59Z", "resources/lib/tasks/taskBuiltin.py": "2016-03-06T11:30:01Z", "resources/lib/watchdog/utils/importlib2.py": "2016-01-17T07:10:59Z", "README.md": "2016-02-01T07:40:27Z", "resources/lib/pathtools/path.py": "2016-01-17T07:10:43Z", "resources/lib/tests/direct_test.py": "2016-04-09T09:38:56Z", "resources/lib/pathtools/LICENSE.txt": "2016-01-25T10:08:08Z", "resources/lib/watchdog/utils/echo.py": "2016-01-17T07:10:59Z", "resources/lib/pathtools/patterns.py": "2016-01-17T07:10:43Z", "resources/lib/publishers/monitor.py": "2016-04-04T07:13:26Z", "resources/lib/watchdog/observers/fsevents2.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/observers/kqueue.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/observers/read_directory_changes.py": "2016-01-17T07:10:59Z", "resources/lib/tests/tstScript.bat": "2016-02-27T07:24:43Z", "resources/lib/utils/__init__.py": "2016-01-24T07:36:06Z", "resources/lib/watchdog/utils/decorators.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/observers/inotify.py": "2016-01-17T07:10:59Z", "resources/lib/utils/poutil.py": "2016-04-14T06:35:41Z", "resources/lib/publishers/player.py": "2016-04-16T05:16:59Z", "LICENSE.txt": "2016-01-25T10:16:54Z", "resources/lib/watchdog/observers/winapi.py": "2016-01-17T07:10:59Z", "resources/lib/watchdog/events.py": "2016-01-17T07:10:59Z", "changelog.txt": "2016-03-27T10:13:57Z", "resources/lib/watchdog/utils/__init__.py": "2016-02-14T17:00:24Z", "resources/lib/tests/testPublishers.py": "2016-03-27T08:38:33Z", "resources/lib/taskABC.py": "2016-03-29T14:16:28Z", "resources/lib/watchdog/README.rst": "2016-01-25T10:10:48Z", "resources/lib/pathtools/version.py": "2016-01-17T07:10:43Z", "resources/lib/publishers/log.py": "2016-03-15T07:12:35Z", "resources/lib/utils/copyToDir.py": "2016-04-04T07:13:26Z", "resources/lib/watchdog/observers/inotify_buffer.py": "2016-01-17T07:10:59Z", "fanart.jpg": "2016-02-26T13:45:25Z", "resources/lib/utils/kodipathtools.py": "2016-03-27T10:13:57Z", "resources/lib/utils/debugger.py": "2016-04-02T08:14:50Z", "resources/lib/watchdog/utils/win32stat.py": "2016-01-17T07:10:59Z", "resources/lib/tasks/taskJson.py": "2016-03-11T06:45:04Z", "resources/lib/publishers/watchdog.py": "2016-03-29T14:16:26Z", "resources/lib/dialogtb.py": "2016-04-04T07:13:26Z", "resources/lib/settings.py": "2016-04-04T07:13:26Z", "restartaddon.py": "2016-02-13T15:07:18Z", "resources/lib/watchdog/observers/api.py": "2016-01-17T07:10:59Z", "resources/lib/tests/tstPythonGlobal.py": "2016-04-03T08:08:06Z", "resources/lib/tests/test.txt": "2016-04-16T05:36:04Z", "resources/default.py": "2016-04-12T18:18:32Z", "resources/lib/__init__.py": "2016-04-09T09:38:56Z", ".gitattributes": "2016-04-04T07:13:26Z", "resources/lib/watchdog/COPYING": "2016-01-25T10:10:48Z"}
\ No newline at end of file