From 6a5af14b405d317259c606a205ad4d9e65d654a2 Mon Sep 17 00:00:00 2001 From: KenV99 Date: Wed, 20 Apr 2016 20:10:08 -0400 Subject: [PATCH] [script.service.kodi.callbacks] 0.9.9 --- script.service.kodi.callbacks/LICENSE.txt | 682 +++++++++ script.service.kodi.callbacks/README.md | 9 + script.service.kodi.callbacks/addon.xml | 24 + script.service.kodi.callbacks/changelog.txt | 67 + script.service.kodi.callbacks/default.py | 171 +++ script.service.kodi.callbacks/fanart.jpg | Bin 0 -> 336560 bytes script.service.kodi.callbacks/icon.png | Bin 0 -> 18222 bytes .../resources/__init__.py | 18 + .../resources/language/English/strings.po | 1224 +++++++++++++++++ .../resources/lib/__init__.py | 88 ++ .../resources/lib/dialogtb.py | 83 ++ .../resources/lib/events.py | 286 ++++ .../resources/lib/kodilogging.py | 68 + .../resources/lib/kodisettings/__init__.py | 18 + .../lib/kodisettings/generate_xml.py | 317 +++++ .../resources/lib/kodisettings/struct.py | 1210 ++++++++++++++++ .../resources/lib/pathtools/AUTHORS | 2 + .../resources/lib/pathtools/LICENSE.txt | 21 + .../resources/lib/pathtools/README | 6 + .../resources/lib/pathtools/README.rst | 1 + .../resources/lib/pathtools/__init__.py | 21 + .../resources/lib/pathtools/path.py | 207 +++ .../resources/lib/pathtools/patterns.py | 265 ++++ .../resources/lib/pathtools/version.py | 31 + .../resources/lib/publisherfactory.py | 69 + .../resources/lib/publishers/__init__.py | 18 + .../resources/lib/publishers/dummy.py | 36 + .../resources/lib/publishers/log.py | 210 +++ .../resources/lib/publishers/loop.py | 176 +++ .../resources/lib/publishers/monitor.py | 106 ++ .../resources/lib/publishers/player.py | 364 +++++ .../resources/lib/publishers/schedule.py | 73 + .../resources/lib/publishers/watchdog.py | 93 ++ .../lib/publishers/watchdogStartup.py | 179 +++ .../resources/lib/pubsub.py | 316 +++++ .../resources/lib/schedule/__init__.py | 454 ++++++ .../resources/lib/settings.py | 245 ++++ .../resources/lib/subscriberfactory.py | 85 ++ .../resources/lib/taskABC.py | 114 ++ .../resources/lib/taskExample.py | 124 ++ .../resources/lib/tasks/__init__.py | 19 + .../resources/lib/tasks/taskBuiltin.py | 71 + .../resources/lib/tasks/taskHttp.py | 184 +++ .../resources/lib/tasks/taskJson.py | 76 + .../resources/lib/tasks/taskPython.py | 123 ++ .../resources/lib/tasks/taskScript.py | 174 +++ .../resources/lib/tests/__init__.py | 18 + .../resources/lib/tests/direct_test.py | 65 + .../resources/lib/tests/testPublishers.py | 602 ++++++++ .../resources/lib/tests/testTasks.py | 272 ++++ .../resources/lib/tests/tstPythonGlobal.py | 29 + .../resources/lib/tests/tstScript.bat | 1 + .../resources/lib/tests/tstScript.sh | 2 + .../resources/lib/utils/__init__.py | 18 + .../resources/lib/utils/copyToDir.py | 95 ++ .../resources/lib/utils/debugger.py | 41 + .../resources/lib/utils/detectPath.py | 64 + .../resources/lib/utils/kodipathtools.py | 182 +++ .../resources/lib/utils/poutil.py | 341 +++++ .../resources/lib/utils/selector.py | 107 ++ .../resources/lib/utils/updateaddon.py | 441 ++++++ .../resources/lib/watchdog/AUTHORS | 66 + .../resources/lib/watchdog/COPYING | 14 + .../resources/lib/watchdog/LICENSE.txt | 202 +++ .../resources/lib/watchdog/README.rst | 276 ++++ .../resources/lib/watchdog/__init__.py | 17 + .../resources/lib/watchdog/events.py | 615 +++++++++ .../lib/watchdog/observers/__init__.py | 98 ++ .../resources/lib/watchdog/observers/api.py | 369 +++++ .../lib/watchdog/observers/fsevents.py | 172 +++ .../lib/watchdog/observers/fsevents2.py | 240 ++++ .../lib/watchdog/observers/inotify.py | 186 +++ .../lib/watchdog/observers/inotify_buffer.py | 76 + .../lib/watchdog/observers/inotify_c.py | 564 ++++++++ .../lib/watchdog/observers/kqueue.py | 726 ++++++++++ .../lib/watchdog/observers/polling.py | 139 ++ .../observers/read_directory_changes.py | 135 ++ .../lib/watchdog/observers/winapi.py | 349 +++++ .../resources/lib/watchdog/tricks/__init__.py | 174 +++ .../resources/lib/watchdog/utils/__init__.py | 166 +++ .../resources/lib/watchdog/utils/bricks.py | 249 ++++ .../resources/lib/watchdog/utils/compat.py | 29 + .../lib/watchdog/utils/decorators.py | 198 +++ .../lib/watchdog/utils/delayed_queue.py | 88 ++ .../lib/watchdog/utils/dirsnapshot.py | 294 ++++ .../resources/lib/watchdog/utils/echo.py | 146 ++ .../lib/watchdog/utils/event_backport.py | 41 + .../lib/watchdog/utils/importlib2.py | 40 + .../resources/lib/watchdog/utils/platform.py | 57 + .../lib/watchdog/utils/unicode_paths.py | 64 + .../resources/lib/watchdog/utils/win32stat.py | 123 ++ .../resources/lib/watchdog/version.py | 28 + .../resources/lib/watchdog/watchmedo.py | 577 ++++++++ .../resources/settings.xml | 1214 ++++++++++++++++ .../skins/Default/720p/DialogTextBox.xml | 101 ++ script.service.kodi.callbacks/restartaddon.py | 28 + script.service.kodi.callbacks/script.py | 190 +++ script.service.kodi.callbacks/testme.py | 93 ++ script.service.kodi.callbacks/timestamp.json | 1 + 99 files changed, 18551 insertions(+) create mode 100644 script.service.kodi.callbacks/LICENSE.txt create mode 100644 script.service.kodi.callbacks/README.md create mode 100644 script.service.kodi.callbacks/addon.xml create mode 100644 script.service.kodi.callbacks/changelog.txt create mode 100644 script.service.kodi.callbacks/default.py create mode 100644 script.service.kodi.callbacks/fanart.jpg create mode 100644 script.service.kodi.callbacks/icon.png create mode 100644 script.service.kodi.callbacks/resources/__init__.py create mode 100644 script.service.kodi.callbacks/resources/language/English/strings.po create mode 100644 script.service.kodi.callbacks/resources/lib/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/dialogtb.py create mode 100644 script.service.kodi.callbacks/resources/lib/events.py create mode 100644 script.service.kodi.callbacks/resources/lib/kodilogging.py create mode 100644 script.service.kodi.callbacks/resources/lib/kodisettings/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/kodisettings/generate_xml.py create mode 100644 script.service.kodi.callbacks/resources/lib/kodisettings/struct.py create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/AUTHORS create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/LICENSE.txt create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/README create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/README.rst create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/path.py create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/patterns.py create mode 100644 script.service.kodi.callbacks/resources/lib/pathtools/version.py create mode 100644 script.service.kodi.callbacks/resources/lib/publisherfactory.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/dummy.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/log.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/loop.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/monitor.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/player.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/schedule.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/watchdog.py create mode 100644 script.service.kodi.callbacks/resources/lib/publishers/watchdogStartup.py create mode 100644 script.service.kodi.callbacks/resources/lib/pubsub.py create mode 100644 script.service.kodi.callbacks/resources/lib/schedule/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/settings.py create mode 100644 script.service.kodi.callbacks/resources/lib/subscriberfactory.py create mode 100644 script.service.kodi.callbacks/resources/lib/taskABC.py create mode 100644 script.service.kodi.callbacks/resources/lib/taskExample.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/taskBuiltin.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/taskHttp.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/taskJson.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/taskPython.py create mode 100644 script.service.kodi.callbacks/resources/lib/tasks/taskScript.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/direct_test.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/testPublishers.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/testTasks.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/tstPythonGlobal.py create mode 100644 script.service.kodi.callbacks/resources/lib/tests/tstScript.bat create mode 100644 script.service.kodi.callbacks/resources/lib/tests/tstScript.sh create mode 100644 script.service.kodi.callbacks/resources/lib/utils/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/copyToDir.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/debugger.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/detectPath.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/kodipathtools.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/poutil.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/selector.py create mode 100644 script.service.kodi.callbacks/resources/lib/utils/updateaddon.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/AUTHORS create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/COPYING create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/LICENSE.txt create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/README.rst create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/events.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/api.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/fsevents2.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_buffer.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/inotify_c.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/kqueue.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/polling.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/read_directory_changes.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/observers/winapi.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/tricks/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/__init__.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/bricks.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/compat.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/decorators.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/delayed_queue.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/dirsnapshot.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/echo.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/event_backport.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/importlib2.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/platform.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/unicode_paths.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/utils/win32stat.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/version.py create mode 100644 script.service.kodi.callbacks/resources/lib/watchdog/watchmedo.py create mode 100644 script.service.kodi.callbacks/resources/settings.xml create mode 100644 script.service.kodi.callbacks/resources/skins/Default/720p/DialogTextBox.xml create mode 100644 script.service.kodi.callbacks/restartaddon.py create mode 100644 script.service.kodi.callbacks/script.py create mode 100644 script.service.kodi.callbacks/testme.py create mode 100644 script.service.kodi.callbacks/timestamp.json 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..03da341890 --- /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..eb97a26dc9 --- /dev/null +++ b/script.service.kodi.callbacks/default.py @@ -0,0 +1,171 @@ +#!/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 = '1008' + +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 +import xbmcgui + +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): + dialog = xbmcgui.Dialog() + msg = _('If improperly implemented, running user tasks can damage your system.\nThe user assumes all risks and liability for running tasks.').split('\\n') + dialog.ok(_('Kodi Callbacks'), line1=msg[0], line2=msg[1]) + 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 0000000000000000000000000000000000000000..9652460b665b18f9e45afefca935a81fb2d196fc GIT binary patch literal 336560 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�=;O;+y5mRF^?Urfl`fEJ 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

8;(pR>uG1dsqdDN-tuwjmOQ5wJ0v_8y?Mv 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 Db*_mR literal 0 HcmV?d00001 diff --git a/script.service.kodi.callbacks/icon.png b/script.service.kodi.callbacks/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5de843e3a0a377fec2ec49373b54c9a9dca9fdbe GIT binary patch literal 18222 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 literal 0 HcmV?d00001 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..879b1248e2 --- /dev/null +++ b/script.service.kodi.callbacks/resources/language/English/strings.po @@ -0,0 +1,1224 @@ +# 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 32299 + +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 "" + +msgctxt "#32299" +msgid "If improperly implemented, running user tasks can damage your system.\nThe user assumes all risks and liability for running tasks." +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/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..e99a63af21 --- /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-20T16:19:39Z", "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/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:41:18Z", "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-20T16:20:57Z", "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/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/settings.xml": "2016-04-20T16:20:57Z", "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-20T16:21:58Z", "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:41:18Z", "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/__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