diff --git a/script.skinvariables/LICENSE.txt b/script.skinvariables/LICENSE.txt new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/script.skinvariables/LICENSE.txt @@ -0,0 +1,674 @@ + 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.skinvariables/README.md b/script.skinvariables/README.md new file mode 100644 index 0000000000..c7738a8852 --- /dev/null +++ b/script.skinvariables/README.md @@ -0,0 +1,12 @@ +# script.skinvariables +A helper script for Kodi skinners to construct multiple variables + +## Wiki +[Variable Builder](https://github.com/jurialmunkey/script.skinvariables/wiki/Skin-Variable-Builder) +[Viewtype Builder](https://github.com/jurialmunkey/script.skinvariables/wiki/Skin-Viewtype-Builder) + +## Kodi Versions + +- [Leia](https://github.com/jurialmunkey/script.skinvariables/tree/leia) +- [Matrix](https://github.com/jurialmunkey/script.skinvariables/tree/matrix) +- [Nexus](https://github.com/jurialmunkey/script.skinvariables/tree/nexus) diff --git a/script.skinvariables/addon.xml b/script.skinvariables/addon.xml new file mode 100644 index 0000000000..0dddd111a8 --- /dev/null +++ b/script.skinvariables/addon.xml @@ -0,0 +1,28 @@ + + + + + + + + + + video + + + Skin Variables helps skinners to construct variables and expressions for multiple containers and listitems using a template + Skin Variables aide les skinners à créer des variables et des expressions pour plusieurs conteneurs et listes à l'aide d'un modèle + Skin Variables helps skinners to construct variables and expressions for multiple containers and listitems using a template + Skin Variables aide les skinners à créer des variables et des expressions pour plusieurs conteneurs et listes à l'aide d'un modèle + For skinners + Pour les skinners + GPL-3.0-or-later + + icon.png + fanart.jpg + + + diff --git a/script.skinvariables/fanart.jpg b/script.skinvariables/fanart.jpg new file mode 100644 index 0000000000..728657a70f Binary files /dev/null and b/script.skinvariables/fanart.jpg differ diff --git a/script.skinvariables/icon.png b/script.skinvariables/icon.png new file mode 100644 index 0000000000..b5e863e695 Binary files /dev/null and b/script.skinvariables/icon.png differ diff --git a/script.skinvariables/plugin.py b/script.skinvariables/plugin.py new file mode 100644 index 0000000000..4266da9ea8 --- /dev/null +++ b/script.skinvariables/plugin.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html + +if __name__ == '__main__': + import sys + from resources.lib.plugin import Plugin + Plugin(int(sys.argv[1]), sys.argv[2][1:]).run() diff --git a/script.skinvariables/resources/__init__.py b/script.skinvariables/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/script.skinvariables/resources/language/resource.language.de_de/strings.po b/script.skinvariables/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..2763cabef6 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,522 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2024-01-21 16:12+0100\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Konstruiere Variablen..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Skin - Variablen" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Skin - Ansichtstypen" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Konstruiere Ansichtstypen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Wähle Ansichtstyp" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Erstelle Standardregeln für" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Erstelle Definitionen für Ansichts-IDs..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Erstelle sichtbare Ausdrucksformen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Erstelle XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Anzupassendes Plugin wählen" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Anzupassenden Inhalt wählen" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Alle {} Ansichten zurücksetzen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Plugin-Ansicht hinzufügen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Ansichtstypen anpassen" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "{} Ansichten zurücksetzen" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Alle {} Ansichten auf Standard zurücksetzen" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Element zum Ändern auswählen" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Element zum Löschen auswählen" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"[B]{}[/B]\n" +"erfolgreich hinzugefügt\n" +"zu\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Menü importieren" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Menü hinzugefügt" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Menü hinzufügen" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Keine Skin-Verknüpfungen zum Import gefunden." + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Als Menü hinzufügen" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Als Menü hinzugefügt" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"[B]{}[/B]\n" +"erfolgreich hinzugefügt\n" +"als\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Menü überschreiben" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "Menüs und Einstellungen werden überschrieben. Wirklich importieren?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"[B]{}[/B]\n" +"erfolgreich importiert\n" +"in\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Element auswählen" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Weiteren Pfad hinzufügen?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Einen weiteren Pfad zum Filter hinzufügen." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "Methode [CAPITALIZE]{}[/CAPITALIZE]" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Sortieren" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Angepasstes Infolabel / Namen für [LOWERCASE]{}[/LOWERCASE] eingeben" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Passenden eigenen Wert für [LOWERCASE]{}[/LOWERCASE] eingeben" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Weniger als <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Weniger als oder gleich <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Gleich ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Nicht gleich !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Größer als oder gleich >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Größer als >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Lösche Pfad" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]ACHTUNG[/B][/COLOR]: Diese Aktion kann nicht rückgängig gemacht werden." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Sichere Änderungen" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Ohne Speichern werden die Änderungen verworfen." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Generiere Globale" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Generiere Inhalt" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Formatiere Inhalt" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Erstelle XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Playliste hinzufügen" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Als abspielbare Verknüpfung oder durchsuchbares Verzeichnis hinzufügen?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Ordner hinzufügen" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Erstelle Verzeichnis" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Profilnamen eingeben" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Zahlencode hinzufügen" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "Bei aktiviertem Zahlencode müssen Nutzer den Code eingeben um das Profil zu öffnen." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "PIN-Code eingeben" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "PIN-Code erneut eingeben" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Falscher PIN-Code!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Eingegebener PIN ist falsch." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Standardprofil" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Noch nie eingeloggt" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Zugriff verweigert!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Nutzerprofil löschen" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Dies löscht das Skin-Profil für {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Nutzerprofil umbenennen" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "Kein Eintragspfad gefunden!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Kein Pfad gefunden" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Menü wählen" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Modus wählen" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Hier hinzufügen" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "{filename} mit {content} überschreiben?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "Deine {skin} Menüdateien" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "Die Menüdateien aus dem Ordner [B]{folder}[/B]" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Dateien überschreiben?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Keine Dateien gefunden!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Icon" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Element hinzufügen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Verknüpfungen neu erstellen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Verknüpfungsvorlage mit aktuellen Änderungen neu erstellen?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Verknüpfungen wiederherstellen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "Eine Verknüpfungswiederherstellung setzt den Skin auf Standardwerte zurück." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Liste hinzufügen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "[B][COLOR=red]ACHTUNG[/COLOR][/B]: Diese Liste wird {item_count} neue Einträge (überspringt {skip_count} existierende) hinzufügen. Aktion kann nicht rückgängig gemacht werden." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Keine Einträge zum hinzufügen!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Diese Liste enthält {item_count} Einträge. Das aktuelle Eintragslimit ist {item_limit}. Liste wird nicht hinzugefügt." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Lösche Liste" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]ACHTUNG[/COLOR][/B]: Löschen dieser Liste wird {item_count} Einträge entfernen. Aktion kann nicht rückgängig gemacht werden." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Keine Elemente zum Löschen!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Verknüpfung auswählen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Verknüpfungen aktualisieren" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Konfiguriere Untermenü" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Konfiguriere Widgets" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Editiere Filter" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Neu hinzufügen" diff --git a/script.skinvariables/resources/language/resource.language.en_gb/strings.po b/script.skinvariables/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..a910768b66 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,524 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\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" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "" + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32096" +msgid "Add new user" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32097" +msgid "Enable default user" +msgstr "" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32098" +msgid "Disable default user" +msgstr "" diff --git a/script.skinvariables/resources/language/resource.language.es_es/strings.po b/script.skinvariables/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..7cdf527c6a --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,521 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Construyendo variables" + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Variables de interfaz" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Tipos de vista interfaz" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Construyendo tipos de vista..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Elige tipo de vista" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Crear reglas predeterminadas para" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Crea las definiciones de vista de IDs..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Crea la visibilidad de expresiones..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Crea XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Elija el complemento para personalizar" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Elige contenido para personalizar" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Restablecer todas las {} vistas..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Agregar vista de complemento..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Personalizar tipos de vista" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Restablecer {} vistas" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Desea restablecer todas las {} vistas a sus valores predeterminados" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Elija el elemento a modificar" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Elija el elemento para eliminar" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Agregado exitosamente\n" +"[B]{}[/B]\n" +"a\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Impotar menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Añadido al menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Añadir al menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "No se encontraron atajos de interfaz para importar" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Agregar como menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Agregado como menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"Agregado satisfactoriamente\n" +"[B]{}[/B]\n" +"como\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Sobreescribir menú" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "La importación sobrescribirá sus menús y configuración actuales. ¿Está seguro?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Importado satisfactoriamente\n" +"[B]{}[/B]\n" +"a\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Elija elemento" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Agregar otra ruta?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Agregar una ruta adicional al filtro?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "[CAPITALIZE]{}[/CAPITALIZE] metodo" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Clasificar" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Personalice [LOWERCASE]{}[/LOWERCASE] la información o nombre apropiado" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Personalice [LOWERCASE]{}[/LOWERCASE] valor para igualar" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Menor que <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Menor que o igual <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Igual ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "No igual !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Mayor que o igual >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Mayor que >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Eliminar ruta" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]WARNING[/B][/COLOR]: Esta acción no se puede deshacer." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Guardar cambios" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Los cambios se descartarán si no los guardas." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Generando globales" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Generando contenido" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Formatear contenido" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Creando XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Añadir lista de reproducción" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Añadir una lista de reproducción como atajo reproducible o directorio navegable" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Añadir carpeta" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Construir directorio" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Introduce el nombre del perfil" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Añadir pin-codigo de bloqueo" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "Agregar un bloqueo con código PIN requerirá que los usuarios ingresen el código antes de acceder al perfil." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Introducir pin-codigo" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Reintroducir pin-codigo" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "¡Código PIN incorrecto!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Código PIN incorrecto ingresado." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Perfil por defecto" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Nunca inicie sesión" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Acceso denegado" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Eliminar perfil de usuario" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Esto eliminará el perfil de máscara de {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Renombre perfil de usuario" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "No se encontró ninguna ruta de elemento!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "No se encontró la ruta" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Elige menú" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Elige modo" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Añadir aquí" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Sobreescribir {filename} con {content}" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "sus {skin} archivos de menus" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "los archivos de menú de [B]{folder}[/B] la carpeta" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Sobreescribir archivos?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "No se encontraron archivos!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Icono" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Añadir elemento" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Reconstruir atajos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Reconstruir la plantilla de accesos directos para incluir cambios recientes?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Restaurar accesos directos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "La restauración de atajos restablece todos los atajos a los valores predeterminados de la apariencia." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Añadir lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone" +msgstr "[B][COLOR=red]WARNING[/COLOR][/B]: Agregar ésta lista agregará {item_count} elementos nuevos (omitiendo {skip_count} elementos existentes agregados anteriormente). Esta acción no se puede deshacer." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "¡No hay elementos nuevos para agregar!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Esta lista contiene {item_count} elementos. El límite máximo de artículos actual es {item_limit}. Esta lista no se agregará." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Borrar lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]ADVERTENCIA[/COLOR][/B]: Al eliminar esta lista, se eliminarán {item_count} elementos existentes. Esta acción no se puede deshacer." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "No hay elementos para eliminar!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Elige un atajo" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Actualizar atajos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Configurar submenú" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Configurar widgets" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Editar filtros" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Añadir nuevo" diff --git a/script.skinvariables/resources/language/resource.language.fi_fi/strings.po b/script.skinvariables/resources/language/resource.language.fi_fi/strings.po new file mode 100644 index 0000000000..2748ba9a4f --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.fi_fi/strings.po @@ -0,0 +1,522 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2024-04-21 13:18+0300\n" +"Last-Translator: Oskari Lavinto \n" +"Language-Team: Finnish\n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Rakennetaan muuttujia..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Skin Variables" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Ulkoasun näkymätyypit" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Rakennetaan näkymätyyppejä..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Valitse näkymätyyppi" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Rakennetaan oletussääntöjä kohteelle" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Rakennetaan määrityksiä näkymien tunnisteille..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Rakennetaan näkyvyyden ilmaisuja..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Rakennetaan XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Valitse mukautettava lisäosa" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Valitse mukautettava sisältö" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Palauta kaikki {} näkymää..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Lisää lisäosanäkymä..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Mukauta näkymätyyppejä" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Palauta {} näkymää" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Haluatko palauttaa kaikkien {} näkymän oletukset" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Valitse muokattava kohde" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Valitse poistettava kohde" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Lisäys onnistui:\n" +"[B]{}[/B]\n" +"kohteeseen\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Tuontivalikko" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Lisätty valikkoon" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Lisää valikkoon" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Tuotavia skinshortcut-valintoja ei löytynyt." + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Lisää valikkona" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Lisätty valikkona" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"Lisäys onnistui:\n" +"[B]{}[/B]\n" +"tyypillä\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Korvaa valikko" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "Tuonti korvaa nykyiset valikot ja määritykset. Haluatko varmasti jatkaa?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Tuonti onnistui:\n" +"[B]{}[/B]\n" +"kohteeseen\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Valitse kohde" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Lisätäänkö sijainti?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Lisää suodattimeen uusi sijainti." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "[CAPITALIZE]{}[/CAPITALIZE] tapa" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Järjestä" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Syötä mukautettu [LOWERCASE]{}[/LOWERCASE] -infolabel- tai -property-nimi" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Syötä oma [LOWERCASE]{}[/LOWERCASE] -tavoitearvo" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Pienempi kuin <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Pienempi tai sama kuin <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Sama ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Ei sama !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Suurempi tai sama kuin >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Suurempi kuin >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Poista sijainti" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]VAROITUS[/B][/COLOR]: Toiminto on peruuttamaton." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Tallenna muutokset" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Jos et tallenna niitä, muutokset hylätään." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Luodaan globaaleja" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Luodaan sisältöä" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Muotoillaan sisältöä" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Luodaan XML:ää" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Lisää toistolista" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Lisätäänkö toistolista toistettavana pikavalintana vaiko selattavana hakemistona?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Lisää kansio" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Rakennetaan hakemistoa" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Syötä profiilin nimi" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Lisää PIN-lukitus" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "PIN-lukitus vaatii käyttäjiä syöttämään koodin ennen profiilin avausta." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Syötä PIN-koodi" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Syötä PIN-koodi uudelleen" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Väärä PIN-koodi!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Syötetty PIN-koodi on virheellinen." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Oletusprofiili" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Ei koskaan kirjautunut" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Pääsy estetty!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Poista käyttäjäprofiili" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Tämä poistaa käyttäjän {} ulkoasuprofiilin." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Nimeä käyttäjäprofiili uudelleen" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "KOhteen sijaintia ei löytynhyt!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Sijaintia ei löytynyt" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Valitse valikko" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Valitse tila" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Lisää tähän" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Korvataanko {filename} {content}?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "ulkoasusi {skin} valikkotiedostoilla" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "kansion [B]{folder}[/B] valikkotiedostoilla" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Korvataanko tiedostot?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Tiedostoja ei löytynyt!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Kuvake" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Lisää kohde" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Rakenna pikavalinnat uudelleen" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Rakennetaanko pikavalintamalli uudelleen viimeisimpien muutosten sisällyttämiseksi?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Palauta pikavalinnat" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "Pikavalintojen palautus palauttaa kaikki pikavalinnat ulkoasun oletuksiin." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Lisää lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "[B][COLOR=red]VAROITUS[/COLOR][/B]: Tämän listan lisääminen lisää {item_count} uutta kohdetta ({skip_count} nykyistä aiemmin lisättyä kohdetta ohitetaan). Ttoiminto on peruuttamaton." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Uusi lisättäviä kohteita ei ole!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Tämä lista sisältää {item_count} kohdetta. Kohteiden enimmäismäärä on tällä hetkellä {item_limit}. Listaa ei lisätä." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Poista lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]VAROITUS[/COLOR][/B]: Tämän listan poistaminen poistaa {item_count} nykyistä kohdetta. Toiminto on peruuttamaton." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Poistettavia kohteita ei ole!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Valitse pikavalinta" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Päivitä pikavalinnat" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Määritä alivalikko" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Määritä widgetit" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Muokkaa suodattimia" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Lisää uusi" diff --git a/script.skinvariables/resources/language/resource.language.fr_fr/strings.po b/script.skinvariables/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..3d9c6e4f55 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,498 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2021-09-02 06:41-0400\n" +"Language-Team: alKODIque\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr_FR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Last-Translator: Ludovik35\n" +"X-Generator: Poedit 3.0\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Construction de variables..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Skin Variables" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Types de vues" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Construction des types de vues..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Choisissez le type de vue" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Création de règles par défaut pour" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Création de définitions pour les ID de vue..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Création d'expressions de visibilité..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Création de XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Choisissez le plugin à personnaliser" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Choisissez le contenu à personnaliser" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Réinitialiser toutes les {} vues..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Ajouter une vue pour le plug-in..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Personnaliser les types de vues" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Réinitialiser {} vues" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Souhaitez-vous réinitialiser toutes les {} vues par défaut" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Choisissez l'élément à modifier" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Choisissez l'élément à supprimer" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32018" +msgid "Successfully added\n[B]{}[/B]\nto\n[B]{}[/B]" +msgstr "[B]{}[/B]\najouté avec succès à\n[B]{}[/B]" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Importer menu" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Menu ajouté" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Ajouter au menu" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Pas de raccourci trouvé dans l'import" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Ajouter comme menu" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Ajouté comme menu" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32025" +msgid "Successfully added\n[B]{}[/B]\nas\n[B]{}[/B]" +msgstr "[B]{}[/B]\najouté avec succès comme\n[B]{}[/B]" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Ecraser le menu" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "L'import va écraser votre menu actuel. Etes-vous sûre ?" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32028" +msgid "Successfully imported\n[B]{}[/B]\nto\n[B]{}[/B]" +msgstr "[B]{}[/B]\nimporté avec succès à\n[B]{}[/B]" + +#: /resources/lib/skinshortcuts_menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Choisir un élément" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Ajouter un autre dossier ?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Ajouter un dossier supplémentaire au filtre." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "Méthode [CAPITALIZE]{}[/CAPITALIZE]" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Trier" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Saisir une étiquette ou de propriété personnalisée [LOWERCASE]{}[/LOWERCASE]." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Saisir la valeur personnalisée [LOWERCASE]{}[/LOWERCASE] à faire correspondre" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Inférieur à <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Inférieur ou égal à <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Egal à ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Différent de !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Supérieur ou égal à >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Supérieur à >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Supprimer dossier" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]ATTENTION[/B][/COLOR] : Cette action ne peut être annulée." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Enregistrer les changements" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Les modifications ne seront pas prises en compte si vous n'enregistrez pas." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Construction des valeurs globales" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Construction du contenu" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Mise en forme du contenu" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Création XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Ajouter à la liste de lecture" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Ajouter la liste de lecture comme raccourci jouable ou comme dossier consultable ?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Ajouter un dossier" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Construction du dossier" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Entrer le nom du profil" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Ajouter un verrouillage par code pin" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "L'ajout d'un verrouillage par code pin obligera les utilisateurs à saisir le code avant d'accéder au profil." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Entrer le code pin" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Ré-entrer le code pin" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Code pin erroné !" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Le code pin saisi est incorrect." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Profil par défaut" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Ne s'est jamais connecté" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Accès refusé !" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Supprimer profil utilisateur" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Cette opération supprime le profil de l'habillage de {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Renommer profil utilisateur" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "Aucun élément trouvé dans le dossier !" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Aucun dossier trouvé" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Choisir un menu" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Choisir un mode" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Ajouter ici" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Écraser {filename} avec {content} ?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "Vos fichiers menu de {skin}" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "les fichiers de menu du dossier [B]{dossier}[/B]." + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Écraser les fichiers ?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Aucun fichier trouvé !" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Icône" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Ajouter élément" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Reconstruire les raccourcis" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Reconstruire le modèle de raccourcis pour inclure les changements récents ?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Restaurer les raccourcis" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "La restauration des raccourcis rétablit les valeurs par défaut de tous les raccourcis." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Ajouter liste" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "[B][COLOR=red]ATTENTION[/COLOR][/B] : L'ajout de cette liste ajoutera {item_count} nouveaux éléments (en ignorant {skip_count} éléments existants précédemment ajoutés). Cette action ne peut être annulée." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Pas de nouveaux éléments à ajouter !" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Cette liste contient {item_count} éléments. La limite maximale actuelle est de {limite_article}. Cette liste ne sera pas ajoutée." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Supprimer liste" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]ATTENTION[/COLOR][/B] : La suppression de cette liste entraînera la suppression de {item_count} articles existants. Cette action ne peut être annulée." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Aucun élément à supprimer !" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Choisir un raccourci" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Rafraîchir les raccourcis" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Configurer sous-menu" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Configurer widgets" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Modifier les filtres" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Ajouter un nouveau" diff --git a/script.skinvariables/resources/language/resource.language.nl_nl/strings.po b/script.skinvariables/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..8465b1777c --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,97 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Variabelen maken..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Skin variabelen" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Skin weergavetypes" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Weergavetypen maken..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Kies weergavetype" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Standaardregels maken voor" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Definities maken voor weergave-ID's..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Zichtbaarheidsuitdrukkingen maken..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "XML bouwen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Kies plugin om aan te passen" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Kies inhoud om aan te passen" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Alle {} weergaven resetten..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Plugin weergave toevoegen..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Weergavetypen aanpassen" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Reset {} weergaven" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Wilt u alle {} weergaven terugzetten naar standaard" diff --git a/script.skinvariables/resources/language/resource.language.pt_br/strings.po b/script.skinvariables/resources/language/resource.language.pt_br/strings.po new file mode 100644 index 0000000000..101bdec484 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.pt_br/strings.po @@ -0,0 +1,536 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2024-04-16 55:MI+ZONE\n" +"Last-Translator: Havokdan \n" +"Language-Team: Português do Brasil\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt-br\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Construindo Variáveis" + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Variáveis da Skin" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Tipos de Exibição da Skin" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Construindo Tipos de Exibição" + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Escolher o tipo de exibição" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Construindo regras padrão para" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Criando definições para IDs de exibições..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Construindo expressões de visibilidade..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Construindo XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Escolher o plugin para personalizar " + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Escolha o conteúdo para personalizar" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Redefinir todas as {} exibições..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Adicionar exibição do plugin..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Personalizar tipos de exibição" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Redefiner {} Exibições" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Você deseja redefinir todas as exibições {} para o padrão" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Escolha o item a ser modificado" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Escolha o item a ser excluído" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Adicionado com sucesso\n" +"[B]{}[/B]\n" +"para\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Importar menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Adicionado ao menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Adicionar ao menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Não foram encontrados atalhos de skin para importar." + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Adicionar como menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Adicionado como menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"Adicionado com sucesso\n" +"[B]{}[/B]\n" +"como\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Sobrescrever menu" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "A importação substituirá seus menus e configurações atuais. Tem certeza?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Importado com sucesso\n" +"[B]{}[/B]\n" +"para\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Escolha o item" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Adicionar outro caminho?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Adicionar um caminho adicional ao filtro." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "Método [CAPITALIZE]{}[/CAPITALIZE]" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Ordenar" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Insira o rótulo de info personalizado [LOWERCASE]{}[/LOWERCASE] ou o nome da propriedade" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Insira o valor [LOWERCASE]{}[/LOWERCASE] personalizado para corresponder" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Menor que <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Menor ou igual <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Igual ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Não é igual !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Maior ou igual >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Maior que >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Excluir caminho" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]AVISO[/B][/COLOR]: Esta ação não pode ser desfeita." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Salvar alterações" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "As alterações serão descartadas se você não salvar." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Gerando globais" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Gerando conteúdo" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Formatando conteúdo" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Criando XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Adicionar à lista de reprodução" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Adicionar lista de reprodução como atalho reproduzível ou diretório navegável?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Adicionar pasta" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Construindo diretório" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Digite um nome para o perfil" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Adicionar bloqueio com código PIN" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "Adicionar um bloqueio com código PIN exigirá que os usuários insiram o código antes de acessar o perfil." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Digite o código PIN" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Digite novamente o código PIN" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Código PIN errado!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Código PIN incorreto inserido." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Perfil padrão" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Nunca entrou" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Acesso negado!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Excluir perfil de usuário" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Isso excluirá o perfil de skin de {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Renomear perfil de usuário" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "Nenhum caminho de item encontrado!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Nenhum caminho encontrado" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Escolha o menu" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Escolha o modo" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Adicionar aqui" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Substituir {filename} por {content}?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "seus arquivos de menu {skin}" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "os arquivos de menu da pasta [B]{folder}[/B]" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Sobrescrever arquivos?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Nenhum arquivoe ncontrado!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Ícone" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Adicionar item" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Reconstruir atalhos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Reconstruir modelo de atalhos para incluir alterações recentes?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Restaurar atalhos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "A restauração de atalhos redefine todos os atalhos para os padrões de skin." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Adicionar à lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "[B][COLOR=red]AVISO[/COLOR][/B]: adicionar esta lista adicionará {item_count} novos itens (ignorando {skip_count} itens existentes adicionados anteriormente). Essa ação não pode ser desfeita." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Não há novos itens para adicionar!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Esta lista contém {item_count} itens. O limite máximo de itens atual é {item_limit}. Esta lista não será adicionada." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Excluir lista" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]AVISO[/COLOR][/B]: excluir esta lista removerá {item_count} itens existentes. Essa ação não pode ser desfeita." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Nenhum item para excluir!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Escolha o atalho" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Atualizar atalhos" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Configurar submenu" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Configurar widgets" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Editar filtros" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Adicionar novo" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32096" +msgid "Add new user" +msgstr "Adicionar novo usuário" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32097" +msgid "Enable default user" +msgstr "Habilitar usuário padrão" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32098" +msgid "Disable default user" +msgstr "Desabilitar usuário padrão" diff --git a/script.skinvariables/resources/language/resource.language.ru_ru/strings.po b/script.skinvariables/resources/language/resource.language.ru_ru/strings.po new file mode 100644 index 0000000000..8ef040ccf7 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.ru_ru/strings.po @@ -0,0 +1,544 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Конструирование переменных..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "Переменные обложки" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Виды обложки" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Конструирование видов..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Выбор вида" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Построение правил для" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Построение определений для ID видов..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Создание выражений видимости..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Построение XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Выберите плагин для настройки" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Выберите контент для настройки" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Сброс всех видов {} ..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Добавить вид плагина..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Настройка видов" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Сброс {} видов" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Хотите ли вы сбросить все виды {} к значениям по умолчанию" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Выберите элемент для изменения" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Выберите элемент для удаления" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Успешно добавлено\n" +"[B]{}[/B]\n" +"в\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Импорт меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Добавлено в меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Добавить в меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Не найдено переменных обложки для импорта." + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Добавить как меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Добавлено как меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"Успешно добавлено\n" +"[B]{}[/B]\n" +"как\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Перезапись меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "При импорте текущие меню и настройки будут перезаписаны. Вы уверены?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Успешно импортировано\n" +"[B]{}[/B]\n" +"в\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Выберите элемент" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Добавить другой путь?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Добавьте дополнительный путь к фильтру." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "[CAPITALIZE]{}[/CAPITALIZE] метод" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Сортировка" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "" +"Введите сбственную [LOWERCASE]{}[/LOWERCASE] инфо-метку или имя параметра" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Введите собственное значение [LOWERCASE]{}[/LOWERCASE] для подбора" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Меньше чем <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Меньше или равно <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Ровно ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Не равно !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Больше или равно >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Больше >" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Удалить путь" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]ВНИМАНИЕ[/B][/COLOR]: Это действие нельзя отменить." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Сохранить изменения" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Изменения будут отменены, если вы не сохраните их." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Генерация глобальных переменных" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Генерация контента" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Форматирование контента" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Создание XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Добавить плейлист" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "" +"Добавить плейлист в качестве ярлыка для воспроизведения или папки для " +"просмотра?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Добавить папку" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Построение папки" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Введите имя профиля" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Добавить блокировку с помощью пин-кода" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "" +"Adding a pin-code lock will require users to enter the code before accessing " +"the profile." +msgstr "" +"Добавление блокировки с пин-кодом потребует от пользователей ввода кода " +"перед доступом к профилю." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Введите пин-код" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Повторно введите пин-код" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Неправильный пин-код!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Введен неправильный пин-код." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Профиль по умолчанию" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Никогда не входил в систему" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Доступ запрещен!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Удалить профиль пользователя" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Это приведет к удалению профиля обложки для {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Переименовать профиль пользователя" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "Путь к элементу не найден!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Путь не найден" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Выбор меню" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Выбор режима" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Добавить сюда" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Перезаписать {filename} {content}?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "ваши {skin} файлы меню" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "файлами меню из папки [B]{folder}[/B]" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Перезаписать файлы?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Файлы не найдены!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Иконка" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Добавить элемент" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Перестроить ярлыки" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Перестроить шаблон ярлыков для включения последних изменений?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Восстановление ярлыков" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "" +"Восстановление ярлыков возвращает все ярлыки к значениям скина по умолчанию." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Добавить список" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "" +"[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} " +"new items (skipping {skip_count} existing items previously added). This " +"action cannot be undone." +msgstr "" +"[B][COLOR=red]ВНИМАНИЕ[/COLOR][/B]: Добавление этого списка добавит " +"{item_count} новых элементов (пропуская {skip_count} существующих элементов, " +"добавленных ранее). Это действие не может быть отменено." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Нет новых элементов для добавления!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "" +"This list contains {item_count} items. The current max item limit is " +"{item_limit}. This list will not be added." +msgstr "" +"Этот список содержит {item_count} элементов. Текущий максимальный лимит " +"элементов {item_limit}. Этот список не будет добавлен." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Удалить список" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "" +"[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove " +"{item_count} existing items. This action cannot be undone." +msgstr "" +"[B][COLOR=red]ВНИМАНИЕ[/COLOR][/B]: Удаление этого списка удалит " +"{item_count} существующих элементов. Это действие не может быть отменено." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Нет элементов для удаления!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Выбрать ярлык" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Обновить ярлыки" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Настроить подменю" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Настроить виджеты" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Редактировать фильтры" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Добавить новый" diff --git a/script.skinvariables/resources/language/resource.language.uk_ua/strings.po b/script.skinvariables/resources/language/resource.language.uk_ua/strings.po new file mode 100644 index 0000000000..33cff964a7 --- /dev/null +++ b/script.skinvariables/resources/language/resource.language.uk_ua/strings.po @@ -0,0 +1,521 @@ +# XBMC Media Center language file +# Addon Name: Skin Variables +# Addon id: script.skinvariables +# Addon Provider: jurialmunkey +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk_ua\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: /resources/lib/script.py +msgctxt "#32000" +msgid "Constructing Variables..." +msgstr "Створюємо перемінні..." + +#: /resources/lib/script.py +msgctxt "#32001" +msgid "Skin Variables" +msgstr "" + +#: /resources/lib/viewtypes.py +msgctxt "#32002" +msgid "Skin Viewtypes" +msgstr "Види обкладинки" + +#: /resources/lib/viewtypes.py +msgctxt "#32003" +msgid "Constructing Viewtypes..." +msgstr "Створюємо види..." + +#: /resources/lib/viewtypes.py +msgctxt "#32004" +msgid "Choose viewtype" +msgstr "Обрати вид" + +#: /resources/lib/viewtypes.py +msgctxt "#32005" +msgid "Building default rules for" +msgstr "Будуємо правила для" + +#: /resources/lib/viewtypes.py +msgctxt "#32006" +msgid "Building definitions for view IDs..." +msgstr "Будуємо визначення для ID видів..." + +#: /resources/lib/viewtypes.py +msgctxt "#32007" +msgid "Building visibility expressions..." +msgstr "Будуємо вирази видимості..." + +#: /resources/lib/viewtypes.py +msgctxt "#32008" +msgid "Building XML..." +msgstr "Будуємо XML..." + +#: /resources/lib/viewtypes.py +msgctxt "#32009" +msgid "Choose plugin to customise" +msgstr "Обрати плагін для редагування" + +#: /resources/lib/viewtypes.py +msgctxt "#32010" +msgid "Choose content to customise" +msgstr "Обрати контент для редагування" + +#: /resources/lib/viewtypes.py +msgctxt "#32011" +msgid "Reset all {} views..." +msgstr "Скинути всі види {}..." + +#: /resources/lib/viewtypes.py +msgctxt "#32012" +msgid "Add plugin view..." +msgstr "Додати вид плагіну..." + +#: /resources/lib/viewtypes.py +msgctxt "#32013" +msgid "Customise viewtypes" +msgstr "Редагувати види" + +#: /resources/lib/viewtypes.py +msgctxt "#32014" +msgid "Reset {} Views" +msgstr "Скинути види {}" + +#: /resources/lib/viewtypes.py +msgctxt "#32015" +msgid "Do you wish to reset all {} views to default" +msgstr "Скинути всі види {} до стандартних?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32016" +msgid "Choose item to modify" +msgstr "Обрати елемент для зміни" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32017" +msgid "Choose item to delete" +msgstr "Обрати елемент для видалення" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32018" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Успішно додано\n" +"[B]{}[/B]\n" +"до\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32019" +msgid "Import menu" +msgstr "Імпортувати меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32020" +msgid "Added to menu" +msgstr "Додано до меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32021" +msgid "Add to menu" +msgstr "Додати до меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32022" +msgid "No skinshortcuts found to import." +msgstr "Не знайдено перемінних обкладинки для імпорту." + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32023" +msgid "Add as menu" +msgstr "Додати як меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32024" +msgid "Added as menu" +msgstr "Додано як меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32025" +msgid "" +"Successfully added\n" +"[B]{}[/B]\n" +"as\n" +"[B]{}[/B]" +msgstr "" +"Успішно додано\n" +"[B]{}[/B]\n" +"як\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32026" +msgid "Overwrite menu" +msgstr "Перезаписати меню" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32027" +msgid "Importing will overwrite your current menus and setup. Are you sure?" +msgstr "Імпорт перезапише поточні меню та налаштування. Ви впевнені?" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32028" +msgid "" +"Successfully imported\n" +"[B]{}[/B]\n" +"to\n" +"[B]{}[/B]" +msgstr "" +"Успішно імпортовано\n" +"[B]{}[/B]\n" +"до\n" +"[B]{}[/B]" + +#: /resources/lib/skinshortcuts.menu.py +msgctxt "#32029" +msgid "Choose item" +msgstr "Обрати елемент" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32030" +msgid "Add another path?" +msgstr "Додати інший шлях?" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32031" +msgid "Add an additional path to the filter." +msgstr "Додати додатковий шлях до фільтру." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32032" +msgid "[CAPITALIZE]{}[/CAPITALIZE] method" +msgstr "[CAPITALIZE]{}[/CAPITALIZE] метод" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32033" +msgid "Sort" +msgstr "Сортувати" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32034" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] infolabel or property name" +msgstr "Введіть власну [LOWERCASE]{}[/LOWERCASE] інфо-мітку чи ім'я параметру" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32035" +msgid "Enter custom [LOWERCASE]{}[/LOWERCASE] value to match" +msgstr "Введіть власне значення [LOWERCASE]{}[/LOWERCASE] для підбору" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32036" +msgid "Less than <" +msgstr "Менше чим <" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32037" +msgid "Less than or equal <=" +msgstr "Менше чи дорівнює <=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32038" +msgid "Equal ==" +msgstr "Дорівнює ==" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32039" +msgid "Not equal !=" +msgstr "Не дорівнює !=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32040" +msgid "Greater than or equal >=" +msgstr "Більше чи рівне >=" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32041" +msgid "Greater than >" +msgstr "Більше чим" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32042" +msgid "Delete path" +msgstr "Видалити шлях" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32043" +msgid "[COLOR=red][B]WARNING[/B][/COLOR]: This action cannot be undone." +msgstr "[COLOR=red][B]УВАГА[/B][/COLOR]: Цю дію неможливо відмінити." + +#: /resources/lib/lists/filterdir.py +msgctxt "#32044" +msgid "Save changes" +msgstr "Зберегти зміни" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32045" +msgid "Changes will be discarded if you do not save." +msgstr "Зміни буде втрачено без збереження." + +#: /resources/lib/shortcuts/template.py +msgctxt "#32046" +msgid "Generating globals" +msgstr "Генеруємо глобальні перемінні" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32047" +msgid "Generating content" +msgstr "Генеруємо контент" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32048" +msgid "Formatting content" +msgstr "Форматуємо контент" + +#: /resources/lib/shortcuts/template.py +msgctxt "#32049" +msgid "Creating XML" +msgstr "Створюємо XML" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32050" +msgid "Add playlist" +msgstr "Додати плейлист" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32051" +msgid "Add playlist as playable shortcut or a browseable directory?" +msgstr "Додати плейлист як ярлик чи теку для огляду?" + +#: /resources/lib/shortcuts/browser.py +msgctxt "#32052" +msgid "Add folder" +msgstr "Додати теку" + +#: /resources/lib/shortcuts/jsonrpc.py +msgctxt "#32053" +msgid "Building directory" +msgstr "Будуємо теку" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32054" +msgid "Enter profile name" +msgstr "Введіть ім'я профілю" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32055" +msgid "Add pin-code lock" +msgstr "Додати PIN-код блок" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32056" +msgid "Adding a pin-code lock will require users to enter the code before accessing the profile." +msgstr "PIN-код блок зобов'яже користувачів вводити код щоб увійти до профілю." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32057" +msgid "Enter pin-code" +msgstr "Введіть PIN" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32058" +msgid "Re-enter pin-code" +msgstr "Введіть PIN повторно" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32059" +msgid "Wrong pin code!" +msgstr "Хибний PIN!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32060" +msgid "Incorrect pin code entered." +msgstr "Введено хибний PIN." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32061" +msgid "Default profile" +msgstr "Стандартних профіль" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32062" +msgid "Never logged in" +msgstr "Ніколи не входив" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32063" +msgid "Access denied!" +msgstr "Доступ відхилено!" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32064" +msgid "Delete user profile" +msgstr "Видалити профіль користувача" + +#: /resources/lib/lists/skinusers.py +msgctxt "#32065" +msgid "This will delete the skin profile for {}." +msgstr "Видалить профіль обкладинки для {}." + +#: /resources/lib/lists/skinusers.py +msgctxt "#32066" +msgid "Rename user profile" +msgstr "Перейменувати профіль користувача" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32067" +msgid "No item path found!" +msgstr "Не знайдено шлях елементу!" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32068" +msgid "No path found" +msgstr "Шлях не знайдено" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32069" +msgid "Choose menu" +msgstr "Обрати меню" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32070" +msgid "Choose mode" +msgstr "Обрати режим" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32071" +msgid "Add here" +msgstr "Додати сюди" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32072" +msgid "Overwrite {filename} with {content}?" +msgstr "Перезаписати {filename} з {content}?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32073" +msgid "your {skin} menu files" +msgstr "меню файлів {skin} обкладинки" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32074" +msgid "the menu files from the [B]{folder}[/B] folder" +msgstr "файли меню з теки [B]{folder}[/B]" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32075" +msgid "Overwrite files?" +msgstr "Перезаписати файли?" + +#: /resources/lib/shortcuts/method.py +msgctxt "#32076" +msgid "No files found!" +msgstr "Не знайдено файлів!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32077" +msgid "Icon" +msgstr "Іконка" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32078" +msgid "Add item" +msgstr "Додати елемент" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32079" +msgid "Rebuild shortcuts" +msgstr "Перебудувати ярлики" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32080" +msgid "Rebuild shortcuts template to include recent changes?" +msgstr "Перебудувати шаблон ярликів для внесення змін?" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32081" +msgid "Restore shortcuts" +msgstr "Відновити ярлики" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32082" +msgid "Restoring shortcuts resets all shortcuts to skin defaults." +msgstr "Відновлення ярликів скидає всі ярлики до стандартних." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32083" +msgid "Add list" +msgstr "Додати список" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32084" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Adding this list will add {item_count} new items (skipping {skip_count} existing items previously added). This action cannot be undone." +msgstr "[B][COLOR=red]УВАГА[/COLOR][/B]: Додавання цього списку внесе {item_count} нових елементів (не враховуючи {skip_count} вже існуючих елементів). Цю дію неможливо відмінити." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32085" +msgid "No new items to add!" +msgstr "Немає нових елементів для додавання!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32086" +msgid "This list contains {item_count} items. The current max item limit is {item_limit}. This list will not be added." +msgstr "Цей список містить {item_count} елементів. Максимальна кількість елементів: {item_limit}. Цей список не буде додано." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32087" +msgid "Delete list" +msgstr "Видалити список" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32088" +msgid "[B][COLOR=red]WARNING[/COLOR][/B]: Deleting this list will remove {item_count} existing items. This action cannot be undone." +msgstr "[B][COLOR=red]УВАГА[/COLOR][/B]: Видалення цього списку видалить {item_count} існуючих елементів. Цю дію неможливо відмінити." + +#: /resources/lib/shortcuts/node.py +msgctxt "#32089" +msgid "No items to delete!" +msgstr "Немає елементів для видалення!" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32090" +msgid "Choose shortcut" +msgstr "Оберіть ярлик" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32091" +msgid "Refresh shortcuts" +msgstr "Оновити ярлики" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32092" +msgid "Configure submenu" +msgstr "Налаштувати підменю" + +#: /resources/lib/shortcuts/node.py +msgctxt "#32093" +msgid "Configure widgets" +msgstr "Налаштувати віджети" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32094" +msgid "Edit filters" +msgstr "Змінити фільтри" + +#: /resources/lib/lists/filterdir.py +msgctxt "#32095" +msgid "Add new" +msgstr "Додати нові" diff --git a/script.skinvariables/resources/lib/__init__.py b/script.skinvariables/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/script.skinvariables/resources/lib/filters.py b/script.skinvariables/resources/lib/filters.py new file mode 100644 index 0000000000..455a78e382 --- /dev/null +++ b/script.skinvariables/resources/lib/filters.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import operator +from jurialmunkey.parser import split_items, boolean + + +FILTER_KEYNAMES = ( + 'filter_key', 'filter_value', 'filter_operator', 'filter_empty', + 'exclude_key', 'exclude_value', 'exclude_operator', ) + + +def get_filters(**kwargs): + all_filters = {} + + for k, v in kwargs.items(): + key, num = k, '0' + if '__' in k: + key, num = k.split('__', 1) + if key not in FILTER_KEYNAMES: + continue + dic = all_filters.setdefault(num, {}) + dic[key] = v + + return all_filters + + +def is_excluded( + item, + filter_key=None, filter_value=None, filter_operator=None, filter_empty=None, + exclude_key=None, exclude_value=None, exclude_operator=None +): + """ Checks if item should be excluded based on filter/exclude values + """ + def is_filtered(d, k, v, exclude=False, operator_type=None): + comp = getattr(operator, operator_type or 'contains') + cond = False if exclude else True # Flip values if we want to exclude instead of include + if k and v and k in d and comp(str(d[k]).lower(), str(v).lower()): + cond = exclude + return cond + + if not item: + return + + il, ip = item.get('infolabels', {}), item.get('infoproperties', {}) + + if filter_key and filter_value: + _exclude = True + for fv in split_items(filter_value): + _exclude = False if boolean(filter_empty) else True + if filter_key in il: + _exclude = False + if is_filtered(il, filter_key, fv, operator_type=filter_operator): + _exclude = False if boolean(filter_empty) and il.get(filter_key) in [None, ''] else True + continue + if filter_key in ip: + _exclude = False + if is_filtered(ip, filter_key, fv, operator_type=filter_operator): + _exclude = False if boolean(filter_empty) and ip.get(filter_key) in [None, ''] else True + continue + if not _exclude: + break + if _exclude: + return True + + if exclude_key and exclude_value: + for ev in split_items(exclude_value): + if exclude_key in il: + if is_filtered(il, exclude_key, ev, True, operator_type=exclude_operator): + return True + if exclude_key in ip: + if is_filtered(ip, exclude_key, ev, True, operator_type=exclude_operator): + return True diff --git a/script.skinvariables/resources/lib/kodiutils.py b/script.skinvariables/resources/lib/kodiutils.py new file mode 100644 index 0000000000..dc1d92afbd --- /dev/null +++ b/script.skinvariables/resources/lib/kodiutils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import xbmcgui +import jurialmunkey.logger as jurialmunkey_logger +import jurialmunkey.plugin as jurialmunkey_plugin +import jurialmunkey.dialog as jurialmunkey_dialog +from contextlib import contextmanager + + +KODIPLUGIN = jurialmunkey_plugin.KodiPlugin('script.skinvariables') +ADDON = KODIPLUGIN._addon +get_localized = KODIPLUGIN.get_localized + + +LOGGER = jurialmunkey_logger.Logger( + log_name='script.skinvariables - ', + notification_head=f'SkinVariables {get_localized(257)}', + notification_text=get_localized(2104), + debug_logging=False) +kodi_log = LOGGER.kodi_log +BusyDialog = jurialmunkey_dialog.BusyDialog +busy_decorator = jurialmunkey_dialog.busy_decorator + + +@contextmanager +def isactive_winprop(name, value='True', windowid=10000): + xbmcgui.Window(windowid).setProperty(name, value) + try: + yield + finally: + xbmcgui.Window(windowid).clearProperty(name) + + +class ProgressDialog(jurialmunkey_dialog.ProgressDialog): + @staticmethod + def kodi_log(msg, level=0): + kodi_log(msg, level) diff --git a/script.skinvariables/resources/lib/lists/filterdir.py b/script.skinvariables/resources/lib/lists/filterdir.py new file mode 100644 index 0000000000..e3464b7a07 --- /dev/null +++ b/script.skinvariables/resources/lib/lists/filterdir.py @@ -0,0 +1,808 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from xbmcgui import ListItem, Dialog +from infotagger.listitem import ListItemInfoTag +from jurialmunkey.litems import Container +from jurialmunkey.window import set_to_windowprop, WindowProperty +from resources.lib.kodiutils import kodi_log, get_localized +from resources.lib.filters import get_filters, is_excluded +import jurialmunkey.thread as jurialmunkey_thread + + +class ParallelThread(jurialmunkey_thread.ParallelThread): + thread_max = 50 + + @staticmethod + def kodi_log(msg, level=0): + kodi_log(msg, level) + + +DIRECTORY_PROPERTIES_BASIC = ["title", "art", "file", "fanart"] + +DIRECTORY_PROPERTIES_VIDEO = [ + "genre", "year", "rating", "playcount", "director", "trailer", "tagline", "plot", "plotoutline", "originaltitle", "lastplayed", "writer", + "studio", "mpaa", "country", "premiered", "runtime", "set", "streamdetails", "top250", "votes", "firstaired", "season", "episode", "showtitle", + "tvshowid", "setid", "sorttitle", "thumbnail", "uniqueid", "dateadded", "customproperties"] + +DIRECTORY_PROPERTIES_MUSIC = [ + "artist", "albumartist", "genre", "year", "rating", "album", "track", "duration", "lastplayed", "studio", "mpaa", + "disc", "description", "theme", "mood", "style", "albumlabel", "sorttitle", "uniqueid", "dateadded", "customproperties", + "totaldiscs", "disctitle", "releasedate", "originaldate", "bpm", "bitrate", "samplerate", "channels"] + +SORTBY_METHODS = [ + "none", "title", "genre", "year", "rating", "playcount", "director", "trailer", "tagline", "plot", "originaltitle", "lastplayed", "writer", + "studio", "mpaa", "country", "premiered", "top250", "votes", "tvshowtitle", "custom"] + +STANDARD_OPERATORS = ( + ('contains', 21400), + ('lt', 32036), + ('le', 32037), + ('eq', 32038), + ('ne', 32039), + ('ge', 32040), + ('gt', 32041)) + + +def update_global_property_versions(): + """ Add additional properties from newer versions of JSON RPC """ + + from jurialmunkey.jsnrpc import get_jsonrpc + + response = get_jsonrpc("JSONRPC.Version") + version = ( + response['result']['version']['major'], + response['result']['version']['minor'], + response['result']['version']['patch'], + ) + + if version >= (13, 3, 0): + DIRECTORY_PROPERTIES_MUSIC.append('songvideourl') # Added in 13.3.0 of JSON RPC + + +INFOLABEL_MAP = { + "title": "title", + "artist": "artist", + "albumartist": "albumartist", + "genre": "genre", + "year": "year", + "rating": "rating", + "album": "album", + "track": "tracknumber", + "duration": "duration", + "playcount": "playcount", + "director": "director", + "trailer": "trailer", + "tagline": "tagline", + "plot": "plot", + "plotoutline": "plotoutline", + "originaltitle": "originaltitle", + "lastplayed": "lastplayed", + "writer": "writer", + "studio": "studio", + "mpaa": "mpaa", + "country": "country", + "premiered": "premiered", + "set": "set", + "top250": "top250", + "votes": "votes", + "firstaired": "aired", + "season": "season", + "episode": "episode", + "showtitle": "tvshowtitle", + "sorttitle": "sorttitle", + "episodeguide": "episodeguide", + "dateadded": "date", + "id": "dbid", + "songvideourl": "songvideourl", +} + +INFOPROPERTY_MAP = { + "disctitle": "disctitle", + "releasedate": "releasedate", + "originaldate": "originaldate", + "bpm": "bpm", + "bitrate": "bitrate", + "samplerate": "samplerate", + "channels": "channels", + "totaldiscs": "totaldiscs", + "disc": "disc", + "description": "description", + "theme": "theme", + "mood": "mood", + "style": "style", + "albumlabel": "albumlabel", + "tvshowid": "tvshow.dbid", + "setid": "set.dbid", + "songvideourl": "songvideourl", +} + + +class MetaItemJSONRPC(): + def __init__(self, meta, dbtype='video'): + self.meta = meta or {} + self.dbtype = dbtype + + @property + def label(self): + if self.meta.get('title'): + return self.meta['title'] + if self.meta.get('label'): + return self.meta['label'] + return '' + + @property + def path(self): + if self.meta.get('file'): + return self.meta['file'] + return '' + + @property + def mediatype(self): + mediatype = self.meta.get('type') or '' + if mediatype in ['unknown', '']: + return self.dbtype + return mediatype + + @property + def infolabels(self): + return {INFOLABEL_MAP[k]: v for k, v in self.meta.items() if v and k in INFOLABEL_MAP and v != -1} + + @property + def infoproperties(self): + infoproperties = {INFOPROPERTY_MAP[k]: str(v) for k, v in self.meta.items() if v and k in INFOPROPERTY_MAP and v != -1} + infoproperties.update({k: str(v) for k, v in (self.meta.get('customproperties') or {}).items()}) + return infoproperties + + @property + def uniqueids(self): + return self.meta.get('uniqueid') or {} + + @property + def streamdetails(self): + return self.meta.get('streamdetails') or {} + + @property + def artwork(self): + artwork = self.meta.get('art') or {} + remap = ( + ('thumb', 'thumb'), + ('fanart', 'fanart')) + for a, k in remap: + if self.meta.get(k) and not artwork.get(a): + artwork[a] = self.meta[k] + + return artwork + + @property + def filetype(self): + return self.meta.get('filetype') + + +class ListItemJSONRPC(): + def __init__(self, meta, library='video', dbtype='video'): + self.meta = MetaItemJSONRPC(meta, dbtype) + self.is_folder = True + self.library = library or 'video' + self.infolabels = self.meta.infolabels + self.infoproperties = self.meta.infoproperties + self.uniqueids = self.meta.uniqueids + self.streamdetails = self.meta.streamdetails + self.artwork = self.meta.artwork + self.filetype = self.meta.filetype + self.mediatype = self.meta.mediatype + self.path = self.meta.path + self.label = self.meta.label + self.label2 = '' + + @property + def mediatype(self): + return self._mediatype + + @mediatype.setter + def mediatype(self, value: str): + self._mediatype = value + self.infolabels['mediatype'] = value + + @property + def infolabels(self): + return self._infolabels + + @infolabels.setter + def infolabels(self, value): + self._infolabels = value + self.fix_music_infolabels() + + def fix_music_infolabels(self): + # Fix some incompatible type returns from JSON RPC to info_tag in music library + if self.library != 'music': + return + for a in ('artist', 'albumartist', 'album'): + if not isinstance(self.infolabels.get(a), list): + continue + self.infolabels[a] = ' / '.join(self.infolabels[a]) + + @property + def artwork(self): + return self._artwork + + @artwork.setter + def artwork(self, value): + self._artwork = value + + def _map_artwork(key: str, names: tuple): + if self._artwork.get(key): + return self._artwork[key] + for a in names: + if self._artwork.get(a): + return self._artwork[a] + return '' + + if self.library == 'music': + parents = ('album', 'albumartist', 'artist') + for k in ('thumb', 'fanart', 'clearlogo'): + self._artwork[k] = _map_artwork(k, (f'{parent}.{k}' for parent in parents)) + + @property + def path(self): + return self._path + + @path.setter + def path(self, value): + self._path = value + self.is_folder = True + + if self.filetype == 'file': + self.is_folder = False + self.infoproperties['isPlayable'] = 'true' + return + + if '://' in self._path: + return + + if self.mediatype == 'tvshow' and self.infolabels.get('dbid'): + self._path = f'videodb://tvshows/titles/{self.infolabels["dbid"]}/' + return + + if self.mediatype == 'season' and self.infolabels.get('tvshow.dbid'): + self._path = f'videodb://tvshows/titles/{self.infoproperties["tvshow.dbid"]}/{self.infolabels["season"]}/' + return + + @property + def listitem(self): + self._listitem = ListItem(label=self.label, label2=self.label2, path=self.path, offscreen=True) + self._listitem.setLabel2(self.label2) + self._listitem.setArt(self.artwork) + + self._info_tag = ListItemInfoTag(self._listitem, self.library) + self._info_tag.set_info(self.infolabels) + if self.library == 'video': + self._info_tag.set_unique_ids(self.uniqueids) + self._info_tag.set_stream_details(self.streamdetails) + + self._listitem.setProperties(self.infoproperties) + return self._listitem + + +class ListGetFilterFiles(Container): + def get_directory(self, filepath=None, **kwargs): + from resources.lib.shortcuts.futils import get_files_in_folder + + basepath = 'plugin://script.skinvariables/' + filepath = filepath or 'special://profile/addon_data/script.skinvariables/nodes/dynamic/' + + def _make_item(i): + editpath = f'{basepath}?info=set_filter_dir&filepath=special://profile/addon_data/script.skinvariables/nodes/dynamic/{i}' + itempath = f'{basepath}?info=get_params_file&path=special://profile/addon_data/script.skinvariables/nodes/dynamic/{i}' + li = ListItem(label=f'{i}', path=itempath) + li.addContextMenuItems([(get_localized(32094), f'RunPlugin({editpath})')]) + return (itempath, li, True) + + def _add_new_item(): + path = f'{basepath}?info=set_filter_dir' + return (path, ListItem(label=f'{get_localized(32095)}...', path=path), True) + + files = get_files_in_folder(filepath, r'.*\.json') + items = [_make_item(i) for i in files if i] + [_add_new_item()] + + plugin_category = '' + container_content = '' + self.add_items(items, container_content=container_content, plugin_category=plugin_category) + + +class MetaFilterDir(): + def __init__(self, library='video', filepath=None): + self.library = library + self.filepath = filepath + + @property + def meta(self): + try: + return self._meta + except AttributeError: + self._meta = self.get_files_meta() + return self._meta + + def get_blank_meta(self): + return { + 'info': 'get_filter_dir', + 'library': self.library, + 'paths': [], + 'names': [] + } + + def get_files_meta(self): + if not self.filepath: + return self.get_blank_meta() + from resources.lib.shortcuts.futils import read_meta_from_file + return read_meta_from_file(self.filepath) or self.get_blank_meta() + + @staticmethod + def get_new_path(): + from resources.lib.shortcuts.browser import GetDirectoryBrowser + with WindowProperty(('IsSkinShortcut', 'True')): + directory_browser = GetDirectoryBrowser(use_rawpath=True) + item = directory_browser.get_directory(path='library://video/') # TODO: Add some choice of library + name = directory_browser.heading_str + try: + path, target = item['path'], item['target'] + except (TypeError, KeyError): + return (None, None) + if not target: # TODO: Add some validation we have correct library + pass + return (path, name) + + @staticmethod + def get_new_method(heading, customheading, methods=SORTBY_METHODS): + x = Dialog().select(heading, methods) + if x == -1: + return None + v = methods[x] + if v == 'custom': + return Dialog().input(heading=customheading) + if v == 'none': + return '' + return v + + def get_new_suffix(self, prefix): + import random + existing_filter_suffix = [k.replace(f'{prefix}_key__', '') for k in self.meta.keys() if k.startswith(f'{prefix}_key__')] # Suffix prefixed by double underscore + + def get_suffix(): + suffix = f'{random.randrange(16**8):08x}' + if suffix not in existing_filter_suffix: + return f'_{suffix}' # Suffix prefixed by double underscore but one will be added when joining so only add one now + return get_suffix() + + return get_suffix() + + def toggle_randomise(self): + from jurialmunkey.parser import boolean + if boolean(self.meta.get('randomise', False)): + del self.meta['randomise'] + return + self.meta['randomise'] = 'true' + + def toggle_fallback(self): + from jurialmunkey.parser import boolean + if boolean(self.meta.get('fallback', False)): + del self.meta['fallback'] + return + self.meta['fallback'] = 'true' + + def del_path(self, value): + x = next(x for x, i in enumerate(self.meta['paths']) if i == value) + del self.meta['paths'][x] + del self.meta['names'][x] + + def rename_path(self, x): + name = Dialog().input(heading=get_localized(551), defaultt=self.meta['names'][x]) + if not name: + return + self.meta['names'][x] = name + + def add_new_path(self): + path, name = self.get_new_path() + if path is None: + return self.meta['paths'] + name = Dialog().input(heading=get_localized(551), defaultt=name) + self.meta['paths'].append(path) + self.meta['names'].append(name) + if Dialog().yesno(get_localized(32030), get_localized(32031)): + return self.add_new_path() + return self.meta['paths'] + + def add_new_sort_how(self): + self.meta['sort_how'] = 'desc' if Dialog().yesno( + get_localized(580), # Sort direction + '', + yeslabel=get_localized(585), # Descending + nolabel=get_localized(584) # Ascending + ) else 'asc' + + def add_new_sort_by(self): + sort_by = self.get_new_method( + get_localized(32032).format(get_localized(32033)), + get_localized(32034).format(get_localized(32033)) + ) + if sort_by is None: + return + self.meta['sort_by'] = sort_by + + def add_new_sort(self): + self.add_new_sort_by() + if not self.meta['sort_by']: + return + self.add_new_sort_how() + + def del_filter(self, prefix='filter', suffix='', keys=('key', 'value', 'operator')): + key_names = ['_'.join(filter(None, [prefix, k, suffix])) for k in keys] + for k in key_names: + try: + del self.meta[k] + except KeyError: + pass + + def add_new_filter_operator(self, prefix='filter', suffix=''): + choices = [(k, get_localized(v)) for k, v in STANDARD_OPERATORS] + x = Dialog().select('[CAPITALIZE]{}[/CAPITALIZE] operator'.format(prefix), [i for _, i in choices]) + if x == -1: + return + filter_operator = choices[x][0] + k = '_'.join(filter(None, [prefix, 'operator', suffix])) + self.meta[k] = filter_operator + return filter_operator + + def add_new_filter_key(self, prefix='filter', suffix=''): + filter_key = self.get_new_method( + get_localized(32032).format(prefix), + get_localized(32034).format(prefix) + ) + if filter_key is None: + return + if filter_key == '': + self.del_filter(prefix, suffix) + return + k = '_'.join(filter(None, [prefix, 'key', suffix])) + self.meta[k] = filter_key + return filter_key + + def add_new_filter_value(self, prefix='filter', suffix=''): + k = '_'.join(filter(None, [prefix, 'key', suffix])) + if not self.meta.get(k): + self.del_filter(prefix, suffix) + return + filter_value = Dialog().input(heading=get_localized(32035).format(prefix)) + if not filter_value: + self.del_filter(prefix, suffix) + return + k = '_'.join(filter(None, [prefix, 'value', suffix])) + self.meta[k] = filter_value + return filter_value + + def add_new_filter(self, prefix='filter', suffix=''): + if not self.add_new_filter_key(prefix, suffix): + return + self.add_new_filter_operator(prefix, suffix) + self.add_new_filter_value(prefix, suffix) + + def write_meta(self, filename=None): + from resources.lib.shortcuts.futils import FILEUTILS, validify_filename + filename = filename or Dialog().input(heading=get_localized(551)) + filename = validify_filename(filename) + if not filename: # TODO: Ask user if they are sure they dont want to make the file. + return + filename = f'{filename}.json' + FILEUTILS.dumps_to_file(self.meta, folder='dynamic', filename=filename, indent=4) # TODO: Make sure we dont overwrite? + return filename + + def delete_meta(self): + if not self.filepath: + return + import xbmcvfs + xbmcvfs.delete(self.filepath) + + def save_meta(self): + if not self.filepath: + return + import xbmcvfs + from json import dump + with xbmcvfs.File(self.filepath, 'w') as file: + dump(self.meta, file, indent=4) + + +class ListSetFilterDir(Container): + def get_directory(self, library='video', filename=None, filepath=None, **kwargs): + meta_filter_dir = MetaFilterDir(library=library, filepath=filepath) + + def get_new(): + meta_filter_dir.add_new_path() + meta_filter_dir.add_new_sort() + meta_filter_dir.add_new_filter('filter') + meta_filter_dir.add_new_filter('exclude') + meta_filter_dir.write_meta(filename) + ListGetFilterFiles(self.handle, '').get_directory() + + def get_path_name_pair(x, i): + names = meta_filter_dir.meta.setdefault('names', []) + if x >= len(names): + names.append('') + return (f'path = {i}', f'name = {names[x]}') + + def do_edit(): + options = [a for j in (get_path_name_pair(x, i) for x, i in enumerate(meta_filter_dir.meta['paths'])) for a in j] + options += [f'{k} = {v}' for k, v in meta_filter_dir.meta.items() if k not in ('paths', 'info', 'library', 'names')] + options += ['randomise = false'] if 'randomise' not in meta_filter_dir.meta.keys() else [] + options += ['fallback = false'] if 'fallback' not in meta_filter_dir.meta.keys() else [] + options += ['add sort'] if 'sort_by' not in meta_filter_dir.meta.keys() else [] + options += ['add filter', 'add exclude', 'add path', 'rename', 'delete', 'save'] + + x = Dialog().select(get_localized(21435), options) + if x == -1: + meta_filter_dir.save_meta() if Dialog().yesno(get_localized(32044), get_localized(32045)) == 1 else None + return + + choice_k, choice_s, choice_v = options[x].partition(' = ') + + if choice_k == 'save': + meta_filter_dir.save_meta() + return + + if choice_k == 'rename': + filename = meta_filter_dir.write_meta() + if filename: + import xbmc + meta_filter_dir.delete_meta() # Delete the old file + xbmc.executebuiltin('Container.Refresh') # Refresh container to see changes + return + return do_edit() # If user didn't enter a valid filename we just go back to menu + + if choice_k == 'delete': + if Dialog().yesno(get_localized(117), get_localized(32043)) == 1: + import xbmc + meta_filter_dir.delete_meta() + xbmc.executebuiltin('Container.Refresh') + return + return do_edit() + + if choice_k == 'sort_by': + meta_filter_dir.add_new_sort_by() + return do_edit() + + if choice_k == 'sort_how': + meta_filter_dir.add_new_sort_how() + return do_edit() + + if choice_k == 'path': + meta_filter_dir.del_path(value=choice_v) if Dialog().yesno(get_localized(32042), '\n'.join([choice_v, get_localized(32043)])) == 1 else None + return do_edit() + + if choice_k == 'name': + meta_filter_dir.rename_path(x=((x - 1) // 2)) + return do_edit() + + if choice_k == 'randomise': + meta_filter_dir.toggle_randomise() + return do_edit() + + if choice_k == 'fallback': + meta_filter_dir.toggle_fallback() + return do_edit() + + if choice_k == 'add path': + meta_filter_dir.add_new_path() + return do_edit() + + if choice_k == 'add sort': + meta_filter_dir.add_new_sort() + return do_edit() + + if choice_k == 'add filter': + suffix = meta_filter_dir.get_new_suffix('filter') + meta_filter_dir.add_new_filter('filter', suffix) + return do_edit() + + if choice_k == 'add exclude': + suffix = meta_filter_dir.get_new_suffix('exclude') + meta_filter_dir.add_new_filter('exclude', suffix) + return do_edit() + + if '_key' in choice_k: + prefix, sep, suffix = choice_k.partition('_key') + suffix = suffix[1:] if suffix else suffix # Remove additional underscore on suffix + meta_filter_dir.add_new_filter_key(prefix, suffix) + return do_edit() + + if '_value' in choice_k: + prefix, sep, suffix = choice_k.partition('_value') + suffix = suffix[1:] if suffix else suffix # Remove additional underscore on suffix + meta_filter_dir.add_new_filter_value(prefix, suffix) + return do_edit() + + if '_operator' in choice_k: + prefix, sep, suffix = choice_k.partition('_operator') + suffix = suffix[1:] if suffix else suffix # Remove additional underscore on suffix + meta_filter_dir.add_new_filter_operator(prefix, suffix) + return do_edit() + + return do_edit() + + get_new() if not filepath else do_edit() + + +class ListGetFilterDir(Container): + def get_directory(self, paths=None, library=None, no_label_dupes=False, dbtype=None, sort_by=None, sort_how=None, randomise=False, fallback=False, names=None, **kwargs): + if not paths: + return + + from jurialmunkey.jsnrpc import get_directory + from jurialmunkey.parser import boolean + + update_global_property_versions() # Add in any properties added in later JSON-RPC versions + + mediatypes = {} + added_items = [] + all_filters = get_filters(**kwargs) + directory_properties = DIRECTORY_PROPERTIES_BASIC + directory_properties += { + 'video': DIRECTORY_PROPERTIES_VIDEO, + 'music': DIRECTORY_PROPERTIES_MUSIC}.get(library) or [] + + def _make_item(i, path_name=None): + if not i: + return + + listitem_jsonrpc = ListItemJSONRPC(i, library=library, dbtype=dbtype) + listitem_jsonrpc.infolabels['title'] = listitem_jsonrpc.label + listitem_jsonrpc.infoproperties['widget'] = path_name or listitem_jsonrpc.infoproperties.get('widget') or '' + + for _, filters in all_filters.items(): + if is_excluded({'infolabels': listitem_jsonrpc.infolabels, 'infoproperties': listitem_jsonrpc.infoproperties}, **filters): + return + + if listitem_jsonrpc.mediatype: + mediatypes[listitem_jsonrpc.mediatype] = mediatypes.get(listitem_jsonrpc.mediatype, 0) + 1 + + return listitem_jsonrpc + + def _is_not_dupe(i): + if not no_label_dupes: + return i + label = i.infolabels['title'] + if label in added_items: + return + added_items.append(label) + return i + + def _get_sorting(i): + v = i.infolabels.get(sort_by) or i.infoproperties.get(sort_by) or '' + try: + v = float(v) + x = 2 # We want high numbers (e.g. rating/year) before empty values when sorting in descending order (reversed) + except ValueError: + v = str(v) + x = 1 + except TypeError: + v = '' + x = 0 # We want empty values to come last when sorting in descending order (reversed) + return (x, v) # Sorted will sort by first value in tuple, then second order afterwards + + def _get_indexed_path(x=0): + seed_paths = [paths.pop(x)] + try: + seed_names = [names.pop(x)] + except (IndexError, TypeError): + seed_names = None + return (seed_paths, seed_names) + + def _get_random_path(): + import random + x = random.choice(range(len(paths))) + return _get_indexed_path(x) + + def _get_paths_names_tuple(): + if not paths or len(paths) < 1: + return (None, None) + if boolean(randomise): + return _get_random_path() + if boolean(fallback): + return _get_indexed_path(0) + return (paths, names) + + def _get_items_from_paths(): + items = [] + seed_paths, seed_names = _get_paths_names_tuple() + + for x, path in enumerate(seed_paths): + try: + path_name = seed_names[x] + except (IndexError, TypeError): + path_name = '' + directory = get_directory(path, directory_properties) + with ParallelThread(directory, _make_item, path_name) as pt: + item_queue = pt.queue + items += [i for i in item_queue if i and (not no_label_dupes or _is_not_dupe(i))] + + if not items and len(paths) > 0: + if boolean(randomise) or boolean(fallback): + return _get_items_from_paths() + + return items + + items = _get_items_from_paths() + + items = sorted(items, key=_get_sorting, reverse=sort_how == 'desc') if sort_by else items + items = [(i.path, i.listitem, i.is_folder, ) for i in items if i] + + plugin_category = '' + container_content = f'{max(mediatypes, key=lambda key: mediatypes[key])}s' if mediatypes else '' + self.add_items(items, container_content=container_content, plugin_category=plugin_category) + + +class ListGetContainerLabels(Container): + def get_directory( + self, containers, infolabel, numitems=None, thumb=None, label2=None, separator=' / ', + filter_value=None, filter_operator=None, exclude_value=None, exclude_operator=None, + window_prop=None, window_id=None, contextmenu=None, + **kwargs): + import xbmc + from resources.lib.method import get_paramstring_tuplepairs + + filters = { + 'filter_key': 'title', + 'filter_value': filter_value, + 'filter_operator': filter_operator, + 'exclude_key': 'title', + 'exclude_value': exclude_value, + 'exclude_operator': exclude_operator, + } + + added_items = [] + contextmenu = get_paramstring_tuplepairs(contextmenu) + + def _make_item(title, image, label): + if (title, image, label, ) in added_items: + return + + if is_excluded({'infolabels': {'title': title}}, **filters): + return + + listitem = ListItem(label=title, label2=label or '', path='', offscreen=True) + listitem.setArt({'icon': image or '', 'thumb': image or ''}) + listitem.addContextMenuItems([ + (k.format(label=title, thumb=image, label2=label), v.format(label=title, thumb=image, label2=label)) + for k, v in contextmenu]) + + item = ('', listitem, True, ) + + added_items.append((title, image, label, )) + return item + + items = [] + for container in containers.split(): + numitems = int(xbmc.getInfoLabel(f'Container({container}).NumItems') or 0) + if not numitems: + continue + for x in range(numitems): + image = xbmc.getInfoLabel(f'Container({container}).ListItemAbsolute({x}).{thumb}') if thumb else '' + label = xbmc.getInfoLabel(f'Container({container}).ListItemAbsolute({x}).{label2}') if label2 else '' + for il in infolabel.split(): + titles = xbmc.getInfoLabel(f'Container({container}).ListItemAbsolute({x}).{il}') + if not titles: + continue + for title in titles.split(separator): + item = _make_item(title, image, label) + if not item: + continue + items.append(item) + + self.add_items(items) + + if not window_prop or not added_items: + return + + for x, i in enumerate(added_items): + set_to_windowprop(i, x, window_prop, window_id) + + xbmc.executebuiltin(f'SetProperty({window_prop},{" / ".join([i[0] for i in added_items])}{f",{window_id}" if window_id else ""})') diff --git a/script.skinvariables/resources/lib/lists/koditools.py b/script.skinvariables/resources/lib/lists/koditools.py new file mode 100644 index 0000000000..1a7c3b0f4f --- /dev/null +++ b/script.skinvariables/resources/lib/lists/koditools.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from jurialmunkey.window import set_to_windowprop +from jurialmunkey.litems import Container + + +class ListGetNumberSum(Container): + def get_directory(self, expression, window_prop=None, window_id=None, **kwargs): + + values = [0] + values += [int(i) for i in expression.split() if i] + + label = f'{sum(values)}' + items = [self.get_list_item(label)] + set_to_windowprop(label, 0, window_prop, window_id) + + self.add_items(items) + + +class ListRunExecuteBuiltin(Container): + def get_directory(self, paths, **kwargs): + from resources.lib.method import run_executebuiltin + + for path in paths: + run_executebuiltin(path, use_rules=True, **kwargs) + + items = [self.get_list_item('None')] # Add a blank item to keep container alive + + self.add_items(items) + + +class ListGetJSONRPC(Container): + def get_directory(self, info, method, window_prop=None, window_id=None, **kwargs): + from jurialmunkey.jsnrpc import get_jsonrpc + result = get_jsonrpc(method, kwargs) or {} + result = result.get("result") + if not result: + return + + items = [self.get_list_item(method)] + + li = items[0][1] + for k, v in result.items(): + li.setProperty(str(k), str(v)) + set_to_windowprop(v, k, window_prop, window_id) + + self.add_items(items) + + return result + + +class ListGetSplitString(Container): + def get_directory(self, values=None, infolabel=None, separator='|', window_prop=None, window_id=None, **kwargs): + from xbmc import getInfoLabel as get_infolabel + values = get_infolabel(infolabel) if infolabel else values + + if not values: + return + + x = 0 + items = [] + for i in values.split(separator): + if not i: + continue + label = f'{i}' + items.append(self.get_list_item(label)) + set_to_windowprop(label, x, window_prop, window_id) + x += 1 + + self.add_items(items) + + +class ListGetEncodedString(Container): + def get_directory(self, paths=None, window_prop=None, window_id=None, **kwargs): + from urllib.parse import quote_plus + + if not paths: + return + + items = [] + for x, i in enumerate(paths): + label = quote_plus(i) + items.append(self.get_list_item(label)) + set_to_windowprop(label, x, window_prop, window_id) + + self.add_items(items) + + +class ListGetFileExists(Container): + def get_directory(self, paths, window_prop=None, window_id=None, **kwargs): + import xbmcvfs + + if not paths: + return + + items = [] + for x, i in enumerate(paths): + label = i + path = i if xbmcvfs.exists(i) else '' + items.append(self.get_list_item(label)) + set_to_windowprop(path, x, window_prop, window_id) + + self.add_items(items) + + +class ListGetSelectedItem(Container): + def get_directory( + self, container, infolabels='', artwork='', separator='/', listitem='ListItem(0)', + window_prop=None, window_id=None, **kwargs + ): + import xbmc + + if not container: + return + + _fstr = f'Container({container}).{listitem}.{{}}' + _label = xbmc.getInfoLabel(_fstr.format('Label')) + + _infoproperties = {} + for i in infolabels.split(separator): + _infoproperties[i] = xbmc.getInfoLabel(_fstr.format(i)) + + _artwork = {} + for i in artwork.split(separator): + _artwork[i] = xbmc.getInfoLabel(_fstr.format(f'Art({i})')) + + item = self.get_list_item(_label) + item[1].setProperties(_infoproperties) + item[1].setArt(_artwork) + + self.add_items([item]) + + if not window_prop: + return + + window_id = f',{window_id}' if window_id else '' + + for k, v in _infoproperties.items(): + window_prop_name = f'{window_prop}.{k}' + xbmc.executebuiltin(f'SetProperty({window_prop_name},{v}{window_id})') + + for k, v in _artwork.items(): + window_prop_name = f'{window_prop}.{k}' + xbmc.executebuiltin(f'SetProperty({window_prop_name},{v}{window_id})') diff --git a/script.skinvariables/resources/lib/lists/playerstreams.py b/script.skinvariables/resources/lib/lists/playerstreams.py new file mode 100644 index 0000000000..7dec2ea49f --- /dev/null +++ b/script.skinvariables/resources/lib/lists/playerstreams.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from xbmcgui import ListItem +from jurialmunkey.jsnrpc import get_jsonrpc +from jurialmunkey.litems import Container + + +PLAYERSTREAMS = { + 'audio': {'key': 'audiostreams', 'cur': 'currentaudiostream'}, + 'subtitle': {'key': 'subtitles', 'cur': 'currentsubtitle'} +} + + +class ListGetPlayerStreams(Container): + def get_directory(self, stream_type=None, **kwargs): + + def _get_items(stream_type): + def make_item(i): + label = i.get("language", "UND") + label2 = i.get("name", "") + path = f'plugin://script.skinvariables/?info=set_player_streams&stream_index={i.get("index")}&stream_type={stream_type}' + infoproperties = {f'{k}': f'{v}' for k, v in i.items() if v} + if cur_strm == i.get('index'): + infoproperties['iscurrent'] = 'true' + infoproperties['isfolder'] = 'false' + listitem = ListItem(label=label, label2=label2, path=path, offscreen=True) + listitem.setProperties(infoproperties) + return listitem + + ps_def = PLAYERSTREAMS.get(stream_type) + if not ps_def: + return [] + + method = "Player.GetProperties" + params = {"playerid": 1, "properties": [ps_def['key'], ps_def['cur']]} + response = get_jsonrpc(method, params) or {} + response = response.get('result', {}) + all_strm = response.get(ps_def['key']) or [] + if not all_strm: + return [] + + cur_strm = response.get(ps_def['cur'], {}).get('index', 0) + return [make_item(i) for i in all_strm if i] + + if not stream_type: + return + + items = [ + (li.getPath(), li, li.getProperty('isfolder').lower() == 'true', ) + for li in _get_items(stream_type) if li] + + self.add_items(items) + + +class ListSetPlayerStreams(Container): + def get_directory(self, stream_type=None, stream_index=None, **kwargs): + if not stream_type or stream_index is None: + return + if stream_type == 'audio': + from resources.lib.method import set_player_audiostream + set_player_audiostream(stream_index) + return + if stream_type == 'subtitle': + from resources.lib.method import set_player_subtitle + set_player_subtitle(stream_index) + return diff --git a/script.skinvariables/resources/lib/lists/rpcdetails.py b/script.skinvariables/resources/lib/lists/rpcdetails.py new file mode 100644 index 0000000000..ff22ad9ef5 --- /dev/null +++ b/script.skinvariables/resources/lib/lists/rpcdetails.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from xbmcgui import ListItem +from jurialmunkey.jsnrpc import get_jsonrpc +from jurialmunkey.litems import Container + + +JSON_RPC_LOOKUPS = { + 'addonid': { + 'method': "Addons.GetAddonDetails", + 'properties': [ + "name", "version", "summary", "description", "path", "author", "thumbnail", "disclaimer", "fanart", + "dependencies", "broken", "extrainfo", "rating", "enabled", "installed", "deprecated"], + 'key': "addon", + }, + 'setid': { + 'method': "VideoLibrary.GetMovieSetDetails", + 'properties': ["title", "plot", "playcount", "fanart", "thumbnail", "art"], + 'key': "setdetails", + }, + 'movieid': { + 'method': "VideoLibrary.GetMovieDetails", + 'properties': ["title", "plot", "genre", "director", "writer", "studio", "cast", "country", "fanart", "thumbnail", "tag", "art", "ratings"], + 'key': "moviedetails", + }, + 'tvshowid': { + 'method': "VideoLibrary.GetTVShowDetails", + 'properties': ["title", "plot", "genre", "studio", "cast", "fanart", "thumbnail", "tag", "art", "ratings", "runtime"], + 'key': "tvshowdetails", + }, + 'seasonid': { + 'method': "VideoLibrary.GetSeasonDetails", + 'properties': ["title", "plot", "fanart", "thumbnail", "tvshowid", "art"], + 'key': "seasondetails", + }, + 'episodeid': { + 'method': "VideoLibrary.GetEpisodeDetails", + 'properties': ["title", "plot", "writer", "director", "cast", "fanart", "thumbnail", "tvshowid", "art", "seasonid", "ratings"], + 'key': "episodedetails", + }, +} + + +class ListGetItemDetails(Container): + jrpc_method = "" + jrpc_properties = [] + jrpc_id = "" + jrpc_idtype = int + jrpc_key = "" + jrpc_sublookups = [] + + @staticmethod + def make_item(i, sub_lookups=None): + try: + label = i.get('label') or '' + except AttributeError: + return # NoneType + + label2 = '' + path = f'plugin://script.skinvariables/' + sub_lookups = sub_lookups or [] + + artwork = i.pop('art', {}) + artwork.setdefault('fanart', i.pop('fanart', '')) + artwork.setdefault('thumb', i.pop('thumbnail', '')) + + def _iter_dict(d, prefix='', sub_lookups=False): + ip = {} + for k, v in d.items(): + if isinstance(v, dict): + ip.update(_iter_dict(v, prefix=f'{prefix}{k}.', sub_lookups=sub_lookups)) + continue + if isinstance(v, list): + ip[f'{prefix}{k}.count'] = f'{len(v)}' + for x, j in enumerate(v): + if isinstance(j, dict): + ip.update(_iter_dict(j, prefix=f'{prefix}{k}.{x}.', sub_lookups=sub_lookups)) + continue + ip[f'{prefix}{k}.{x}'] = f'{j}' + continue + ip[f'{prefix}{k}'] = f'{v}' + + if not sub_lookups or k not in sub_lookups or k not in JSON_RPC_LOOKUPS: + continue + + try: + lookup = JSON_RPC_LOOKUPS[k] + method = lookup['method'] + params = {k: int(v), "properties": lookup['properties']} + response = get_jsonrpc(method, params) + item = response['result'][lookup['key']] or {} + ip.update(_iter_dict(item, prefix=f'{prefix}item.', sub_lookups=False)) + except (KeyError, AttributeError): + pass + + return ip + + infoproperties = {} + infoproperties.update(_iter_dict(i, sub_lookups=sub_lookups)) + infoproperties['isfolder'] = 'false' + + # kodi_log(f'ip {infoproperties}', 1) + + listitem = ListItem(label=label, label2=label2, path=path, offscreen=True) + listitem.setProperties(infoproperties) + listitem.setArt(artwork) + + return listitem + + def get_items(self, dbid, **kwargs): + def _get_items(): + method = self.jrpc_method + params = { + self.jrpc_id: self.jrpc_idtype(dbid), + "properties": self.jrpc_properties + } + response = get_jsonrpc(method, params) or {} + item = response.get('result', {}).get(self.jrpc_key) + + return [self.make_item(item, self.jrpc_sublookups)] + + items = [ + (li.getPath(), li, li.getProperty('isfolder').lower() == 'true', ) + for li in _get_items() if li] if dbid else [] + + return items + + def get_directory(self, dbid, **kwargs): + items = self.get_items(dbid, **kwargs) + self.add_items(items) + + +class ListGetAddonDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['addonid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['addonid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['addonid']['key'] + jrpc_id = "addonid" + jrpc_idtype = str + + def get_directory(self, dbid, convert_path=False, **kwargs): + if convert_path: + if not dbid.startswith('plugin://'): + return + import re + result = re.search('plugin://(.*)/', dbid) + return result.group(1) if result else None + + items = self.get_items(dbid, **kwargs) + self.add_items(items) + + +class ListGetMovieSetDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['setid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['setid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['setid']['key'] + jrpc_id = "setid" + jrpc_sublookups = ["movieid"] + + +class ListGetMovieDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['movieid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['movieid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['movieid']['key'] + jrpc_id = "movieid" + + +class ListGetTVShowDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['tvshowid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['tvshowid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['tvshowid']['key'] + jrpc_id = "tvshowid" + + +class ListGetSeasonDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['seasonid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['seasonid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['seasonid']['key'] + jrpc_id = "seasonid" + + +class ListGetEpisodeDetails(ListGetItemDetails): + jrpc_method = JSON_RPC_LOOKUPS['episodeid']['method'] + jrpc_properties = JSON_RPC_LOOKUPS['episodeid']['properties'] + jrpc_key = JSON_RPC_LOOKUPS['episodeid']['key'] + jrpc_id = "episodeid" diff --git a/script.skinvariables/resources/lib/lists/skinusers.py b/script.skinvariables/resources/lib/lists/skinusers.py new file mode 100644 index 0000000000..91a890b991 --- /dev/null +++ b/script.skinvariables/resources/lib/lists/skinusers.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from xbmcgui import ListItem, Dialog, INPUT_NUMERIC +from jurialmunkey.litems import Container +from resources.lib.kodiutils import get_localized +import jurialmunkey.futils as jmfutils + + +BASEPLUGIN = 'plugin://script.skinvariables/' +BASEFOLDER = 'special://profile/addon_data/script.skinvariables/logins/' +USERS_FILE = 'skinusers.json' + + +class FileUtils(jmfutils.FileUtils): + addondata = BASEFOLDER # Override module addon_data with plugin addon_data + + +class ListAddSkinUser(Container): + def get_directory(self, skinid, **kwargs): + import re + import random + from jurialmunkey.futils import load_filecontent + from resources.lib.shortcuts.futils import reload_shortcut_dir + from json import loads + filepath = f'{BASEFOLDER}/{skinid}/{USERS_FILE}' + file = load_filecontent(filepath) + meta = loads(file) if file else [] + + name = Dialog().input(get_localized(32054)) + if not name: + return + + slug = re.sub('[^0-9a-zA-Z]+', '', name) + if not slug: + slug = f'{random.randrange(16**8):08x}' # Assign a random 32bit hex value if no valid slug name + slug = f'user-{slug}' # Avoid Kodi trying to localize slugs which are only numbers by adding alpha prefix + + icon = '' + + def _get_code(): + if not Dialog().yesno(get_localized(32055), get_localized(32056)): + return + code = Dialog().input(get_localized(32057), type=INPUT_NUMERIC) + if not code: + return + if not Dialog().input(get_localized(32058), type=INPUT_NUMERIC) == code: + return _get_code() + return str(code) + + code = _get_code() + + item = { + 'name': name, + 'slug': slug, + 'icon': icon, + 'code': code + } + + meta.append(item) + FileUtils().dumps_to_file(meta, folder=skinid, filename=USERS_FILE, indent=4) + reload_shortcut_dir() + + +class ListGetSkinUser(Container): + def get_directory(self, skinid, folder, slug=None, allow_new=False, func=None, **kwargs): + import xbmc + from jurialmunkey.parser import boolean + from jurialmunkey.futils import load_filecontent, write_skinfile + from resources.lib.shortcuts.futils import reload_shortcut_dir + from json import loads + + filepath = f'{BASEFOLDER}/{skinid}/{USERS_FILE}' + file = load_filecontent(filepath) + meta = loads(file) if file else [] + + def _login_user(): + if slug == 'default': + user = _get_default_user() + else: + user = next(i for i in meta if slug == i.get('slug')) + + if user.get('code') and str(user.get('code')) != str(Dialog().input(get_localized(32057), type=INPUT_NUMERIC)): + Dialog().ok(get_localized(32063), get_localized(32060)) + return + + xbmc.executebuiltin('SetProperty(SkinVariables.SkinUser.LoggingIn,True,Home)') + + filename = 'script-skinvariables-skinusers.xml' + content = load_filecontent(f'special://skin/shortcuts/skinvariables-skinusers.xmltemplate') + content = content.format(slug=slug if slug != 'default' else '', **kwargs) + write_skinfile(folders=[folder], filename=filename, content=content) + + import datetime + last = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + executebuiltin = xbmc.getInfoLabel('Skin.String(SkinVariables.SkinUser.ExecuteBuiltIn)') + xbmc.executebuiltin(f'Skin.SetString(SkinVariables.SkinUser.Name,{user.get("name")})') + xbmc.executebuiltin(f'Skin.SetString(SkinVariables.SkinUser.Icon,{user.get("icon", "")})') + xbmc.executebuiltin(f'Skin.SetString(SkinVariables.SkinUser,{slug})' if slug != 'default' else 'Skin.Reset(SkinVariables.SkinUser)') + xbmc.executebuiltin(f'Skin.SetString(SkinVariables.SkinUser.{slug}.LastLogin,{last})') + xbmc.executebuiltin('SetProperty(SkinVariables.SkinUserLogin,True,Home)') + xbmc.executebuiltin(executebuiltin or 'ReloadSkin()') + + def _get_default_user(): + return {'name': get_localized(32061), 'slug': 'default'} + + def _make_item(i): + name = i.get('name') or '' + slug = i.get('slug') or '' + + if not name: + return + + icon = i.get('icon') or '' + code = i.get('code') or '' + menu = boolean(i.get('menu', True)) + path = f'{BASEPLUGIN}?info=get_skin_user&skinid={skinid}&slug={slug}' + path = f'{path}&folder={folder}' if folder else path + last = xbmc.getInfoLabel(f'Skin.String(SkinVariables.SkinUser.{slug}.LastLogin)') or get_localized(32062) + + li = ListItem(label=name, label2=last, path=path) + li.setProperty('last', last) + li.setProperty('slug', slug) + li.setProperty('code', code) if code else None + li.setArt({'thumb': icon, 'icon': icon}) if icon else None + + def _get_contentmenuitems(): + if not menu: + return [] + if slug == 'default': + return [_get_contextmenu_item_toggle_default_user()] + return [ + ('Rename', f'RunPlugin({path}&func=rename)'), + ('Delete', f'RunPlugin({path}&func=delete)')] + + li.addContextMenuItems(_get_contentmenuitems()) + + return (path, li, False) + + def _get_contextmenu_item_toggle_default_user(): + path = f'{BASEPLUGIN}?info=get_skin_user&skinid={skinid}&slug=default' + path = f'{path}&folder={folder}' if folder else path + path = f'RunPlugin({path}&func=toggle)' + if xbmc.getCondVisibility('Skin.HasSetting(SkinVariables.SkinUsers.DisableDefaultUser)'): + return (get_localized(32097), path) + return (get_localized(32098), path) + + def _join_item(): + if not boolean(allow_new): + return [] + name = f'{get_localized(32096)}...' + path = f'{BASEPLUGIN}?info=add_skin_user&skinid={skinid}' + path = f'{path}&folder={folder}' if folder else path + li = ListItem(label=name, path=path) + li.addContextMenuItems([_get_contextmenu_item_toggle_default_user()]) + return [(path, li, False)] + + def _open_directory(): + items = [] + if xbmc.getCondVisibility('!Skin.HasSetting(SkinVariables.SkinUsers.DisableDefaultUser)'): + items += [_make_item(_get_default_user())] + items += [j for j in (_make_item(i) for i in meta) if j] + _join_item() + plugin_category = '' + container_content = '' + self.add_items(items, container_content=container_content, plugin_category=plugin_category) + + def _toggle_default_user(): + xbmc.executebuiltin('Skin.ToggleSetting(SkinVariables.SkinUsers.DisableDefaultUser)') + reload_shortcut_dir() + + def _delete_user(): + x, user = next((x, i) for x, i in enumerate(meta) if slug == i.get('slug')) + + if user.get('code') and str(user.get('code')) != str(Dialog().input(get_localized(32057), type=INPUT_NUMERIC)): + Dialog().ok(get_localized(32063), get_localized(32060)) + return + if not Dialog().yesno(get_localized(32064), f'{get_localized(32065).format(user["name"])}\n{get_localized(32043)}'): + return + + del meta[x] + FileUtils().dumps_to_file(meta, folder=skinid, filename=USERS_FILE, indent=4) + reload_shortcut_dir() + + def _rename_user(): + x, user = next((x, i) for x, i in enumerate(meta) if slug == i.get('slug')) + + if user.get('code') and str(user.get('code')) != str(Dialog().input(get_localized(32057), type=INPUT_NUMERIC)): + Dialog().ok(get_localized(32063), get_localized(32060)) + return + user['name'] = Dialog().input(get_localized(32066), defaultt=user.get('name', '')) + if not user['name']: + return + meta[x] = user + FileUtils().dumps_to_file(meta, folder=skinid, filename=USERS_FILE, indent=4) + reload_shortcut_dir() + + if not slug: + _open_directory() + return + + route = { + 'toggle': _toggle_default_user, + 'delete': _delete_user, + 'rename': _rename_user + } + route.get(func, _login_user)() diff --git a/script.skinvariables/resources/lib/method.py b/script.skinvariables/resources/lib/method.py new file mode 100644 index 0000000000..af3ef60653 --- /dev/null +++ b/script.skinvariables/resources/lib/method.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import jurialmunkey.futils +import jurialmunkey.parser +ADDONDATA = 'special://profile/addon_data/script.skinvariables/' + + +class FileUtils(jurialmunkey.futils.FileUtils): + addondata = ADDONDATA # Override module addon_data with plugin addon_data + + +boolean = jurialmunkey.parser.boolean +parse_localize = jurialmunkey.parser.parse_localize + + +def set_animation_list(animations): + import xbmcgui + win_id = xbmcgui.getCurrentWindowId() + window = xbmcgui.Window(win_id) + for control_id, event, effect in animations: + control = window.getControl(int(control_id)) + control.setAnimations([(event, effect,)]) + + +def set_animation(set_animation, **kwargs): + set_animation_list([ + (control_id, event, effect,) + for i in set_animation.split('||') + for control_id, event, effect in i.split('|') + ]) + + +def run_executebuiltin_list(builtins): + import xbmc + for builtin in builtins: + if builtin.startswith('sleep='): + xbmc.Monitor().waitForAbort(float(builtin[6:])) + continue + if builtin.startswith('route='): + from resources.lib.script import Script + Script(paramstring=builtin[6:]).run() + continue + if builtin.startswith('animation='): + animation = builtin[10:] + control_id, event, effect = animation.split('|') + set_animation_list([(control_id, event, effect, )]) + continue + xbmc.executebuiltin(builtin) + + +def run_executebuiltin(run_executebuiltin=None, use_rules=False, **kwargs): + if not run_executebuiltin: + return + if not boolean(use_rules): + return run_executebuiltin_list(run_executebuiltin.split('||')) + + from json import loads + from jurialmunkey.futils import load_filecontent + from resources.lib.operations import RuleOperations + + try: + meta = loads(str(load_filecontent(run_executebuiltin))) + except Exception: + raise Exception(f'Unable to load {run_executebuiltin} !') + + rule_operations = RuleOperations(meta, **kwargs) + actions_list = rule_operations.get_actions_list(rule_operations.meta['actions']) + return run_executebuiltin_list(actions_list) + + +def get_paramstring_tuplepairs(paramstring): + if not paramstring: + return [] + return [tuple(i.split(';')) for i in paramstring.split(';;')] + + +def executebuiltin(executebuiltin='', index=None, values=None, **kwargs): + if index == -1 or index is False: + return + + if isinstance(index, int): + executebuiltin = kwargs.get(f'executebuiltin_{index}') or executebuiltin + value = values[index] if values else index + else: + value = index + + if not executebuiltin: + return + + run_executebuiltin_list([builtin.format(x=index, v=value) for builtin in executebuiltin.split('||')]) + + +def run_progressdialog(run_progressdialog, background=False, heading='', message='', polling='0.1', message_info='', progress_info='', timeout='200', max_value='100', **kwargs): + import xbmc + import xbmcgui + + func = xbmcgui.DialogProgressBG if boolean(background) else xbmcgui.DialogProgress + dialog = func() + + polling = float(polling) + timeout = int(timeout) + max_value = int(max_value) + + monitor = xbmc.Monitor() + dialog.create(heading, message) + + x = 0 + while x < max_value and timeout > 0 and not monitor.abortRequested(): + x += 1 + timeout -= 1 + if progress_info: + x = int(xbmc.getInfoLabel(progress_info) or 0) + if message_info: + message = str(xbmc.getInfoLabel(message_info) or '') + progress = int((x / max_value) * 100) + dialog.update(progress, message=message) + monitor.waitForAbort(polling) + dialog.close() + del dialog + del monitor + + +def run_dialog(run_dialog, separator=' / ', **kwargs): + import xbmcgui + + def _split_items(items): + return items.split(separator) + + def _get_path_or_str(string): + if not boolean(kwargs.get('load_file')): + return str(string) + from jurialmunkey.futils import load_filecontent + return str(load_filecontent(string)) + + def _get_preselected_items(string): + if not string: + return -1 + try: + return int(string) + except TypeError: + return -1 + except ValueError: + pass + items = _split_items(kwargs.get('list') or '') + if not items: + return -1 + if len(items) == 0: + return -1 + if string not in items: + return -1 + return items.index(string) + + dialog = xbmcgui.Dialog() + + dialog_standard_routes = { + 'ok': { + 'func': dialog.ok, + 'params': ( + ('heading', str, ''), ('message', _get_path_or_str, ''), ) + }, + 'yesno': { + 'func': dialog.yesno, + 'params': ( + ('heading', str, ''), ('message', _get_path_or_str, ''), ('nolabel', str, 'No'), ('yeslabel', str, 'Yes'), + ('defaultbutton', int, xbmcgui.DLG_YESNO_YES_BTN), ('autoclose', int, 0), ) + }, + 'yesnocustom': { + 'func': dialog.yesnocustom, + 'params': ( + ('heading', str, ''), ('message', _get_path_or_str, ''), ('nolabel', str, 'No'), ('yeslabel', str, 'Yes'), ('customlabel', str, 'Custom'), + ('defaultbutton', int, xbmcgui.DLG_YESNO_YES_BTN), ('autoclose', int, 0), ) + }, + 'textviewer': { + 'func': dialog.textviewer, + 'params': ( + ('heading', str, ''), ('text', _get_path_or_str, ''), + ('usemono', boolean, True), ) + }, + 'notification': { + 'func': dialog.notification, + 'params': ( + ('heading', str, ''), ('message', str, ''), ('icon', str, ''), + ('time', int, 5000), ('sound', boolean, True), ) + }, + 'numeric': { + 'func': dialog.numeric, + 'params': ( + ('heading', str, ''), ('defaultt', str, ''), + ('type', int, 0), ('bHiddenInput', boolean, False), ) + }, + 'input': { + 'func': dialog.input, + 'params': ( + ('heading', str, ''), ('defaultt', str, ''), + ('type', int, xbmcgui.INPUT_ALPHANUM), ('option', int, 0), ('autoclose', int, 0), ) + }, + 'browse': { + 'func': dialog.browse, + 'params': ( + ('heading', str, ''), ('shares', str, ''), ('mask', str, ''), ('defaultt', str, ''), + ('type', int, 0), ('useThumbs', boolean, True), ('treatAsFolder', boolean, True), ('enableMultiple', boolean, True), ) + }, + 'colorpicker': { + 'func': dialog.colorpicker, + 'params': ( + ('heading', str, ''), ('selectedcolor', str, ''), ('colorfile', str, ''), ) + }, + 'contextmenu': { + 'func': dialog.contextmenu, + 'params': ( + ('list', _split_items, ''), ) + }, + 'select': { + 'func': dialog.select, + 'params': ( + ('heading', str, ''), + ('list', _split_items, ''), + ('autoclose', int, 0), ('preselect', _get_preselected_items, -1), ('useDetails', boolean, False), ) + }, + 'multiselect': { + 'func': dialog.select, + 'params': ( + ('heading', str, ''), + ('list', _split_items, ''), + ('autoclose', int, 0), ('preselect', _get_preselected_items, -1), ('useDetails', boolean, False), ) + }, + } + + route = dialog_standard_routes[run_dialog] + params = {k: func(kwargs.get(k) or fallback) for k, func, fallback in route['params']} + executebuiltin(index=route['func'](**params), values=params.get('list'), **kwargs) + + +def set_player_subtitle(set_player_subtitle, reload_property='UID', **kwargs): + import time + import xbmc + from jurialmunkey.jsnrpc import get_jsonrpc + from jurialmunkey.parser import try_int + method = "Player.SetSubtitle" + params = {"playerid": 1, "subtitle": try_int(set_player_subtitle), "enable": True} + get_jsonrpc(method, params) + xbmc.executebuiltin(f'SetProperty({reload_property},{time.time()})') + + +def set_player_audiostream(set_player_audiostream, reload_property='UID', **kwargs): + import time + import xbmc + from jurialmunkey.jsnrpc import get_jsonrpc + from jurialmunkey.parser import try_int + method = "Player.SetAudioStream" + params = {"playerid": 1, "stream": try_int(set_player_audiostream)} + get_jsonrpc(method, params) + xbmc.executebuiltin(f'SetProperty({reload_property},{time.time()})') + + +def set_editcontrol(set_editcontrol, text=None, window_id=None, setfocus=None, setfocus_wait='00:00', **kwargs): + import xbmc + from jurialmunkey.jsnrpc import get_jsonrpc + xbmc.executebuiltin(f'SetFocus({set_editcontrol})') + get_jsonrpc("Input.SendText", {"text": text or '', "done": True}) + xbmc.executebuiltin(f'AlarmClock(Refocus,SetFocus({setfocus}),{setfocus_wait},silent)') if setfocus else None + + +def add_skinstring_history(add_skinstring_history, value, separator='|', use_window_prop=False, window_id='', toggle=False, **kwargs): + import xbmc + + def _get_info_str() -> str: + if not use_window_prop: + return 'Skin.String({})' + if window_id: + return f'Window({window_id}).Property({{}})' + return 'Window.Property({})' + + values = xbmc.getInfoLabel(_get_info_str().format(add_skinstring_history)) or '' + values = values.split(separator) + if not values: + return + try: + values.remove(value) + remove = True + except ValueError: + remove = False + if not toggle or not remove: + values.insert(0, value) + + def _get_exec_str() -> str: + if not use_window_prop: + return 'Skin.SetString({},{})' + if window_id: + return f'SetProperty({{}},{{}},{window_id})' + return 'SetProperty({},{})' + + xbmc.executebuiltin(_get_exec_str().format(add_skinstring_history, separator.join(filter(None, values)))) + + +def set_dbid_tag(set_dbid_tag, dbtype, dbid, **kwargs): + from jurialmunkey.jsnrpc import set_tags + set_tags(int(dbid), dbtype, [set_dbid_tag]) + + +def get_jsonrpc(get_jsonrpc, textviewer=False, filewrite=True, **kwargs): + from jurialmunkey.jsnrpc import get_jsonrpc as _get_jsonrpc + result = _get_jsonrpc(get_jsonrpc, kwargs) + + if textviewer: + from xbmcgui import Dialog + Dialog().textviewer(f'GET {get_jsonrpc}', f'PARAMS\n{kwargs}\n\nRESULT\n{result}') + + if filewrite: + filename = '_'.join([f'{k}-{v}' for k, v in kwargs.items()]) + filename = jurialmunkey.futils.validify_filename(f'{get_jsonrpc}_{filename}.json') + FileUtils().dumps_to_file({'method': get_jsonrpc, 'params': kwargs, 'result': result}, 'log_request', filename) diff --git a/script.skinvariables/resources/lib/operations.py b/script.skinvariables/resources/lib/operations.py new file mode 100644 index 0000000000..a65b288701 --- /dev/null +++ b/script.skinvariables/resources/lib/operations.py @@ -0,0 +1,178 @@ +import re +import xbmc + + +def check_condition(condition): + if not condition: + return True # No condition set so we treat as True + if '||' in condition: + return check_or_conditions(condition.split('||')) + if '==' in condition: + a, b = condition.split('==') + return True if a == b else False + if '!=' in condition: + a, b = condition.split('!=') + return True if a != b else False + if '>>' in condition: + a, b = condition.split('>>') + return True if a in b else False + if '<<' in condition: + a, b = condition.split('<<') + return True if b in a else False + if '!>' in condition: + a, b = condition.split('!>') + return True if a not in b else False + if '!<' in condition: + a, b = condition.split('!<') + return True if b not in a else False + if xbmc.getCondVisibility(condition): + return True + return False + + +def check_or_conditions(conditions): + for condition in conditions: + if condition and check_condition(condition): + return True + return False + + +def check_and_conditions(conditions): + for condition in conditions: + if condition and not check_condition(condition): + return False + return True + + +class FormatDict(dict): + def __missing__(self, key): + return '' + + +class RuleOperations(): + def __init__(self, meta, **params): + self.meta = meta + self.params = FormatDict(params) + self.run_operations() + + def run_operations(self): + for i in self.operations: + for k, v in i.items(): + self.routes[k](v) + + @property + def operations(self): + return [{i: self.meta[i]} for i in self.routes if i in self.meta] + self.meta.get('operations', []) + + @property + def routes(self): + try: + return self._routes + except AttributeError: + self._routes = { + 'capitalize': self.set_capitalize, + 'infolabels': self.set_infolabels, + 'regex': self.set_regex, + 'values': self.set_values, + 'sums': self.set_sums, + 'decode': self.set_decode, + 'encode': self.set_encode, + 'escape': self.set_escape, + 'lower': self.set_lower, + 'upper': self.set_upper, + } + return self._routes + + def set_infolabels(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + v = v.format_map(self.params) + self.params[k] = xbmc.getInfoLabel(v) + + def set_regex(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = re.sub(v['regex'].format_map(self.params), v['value'].format_map(self.params), v['input'].format_map(self.params)) + + def set_values(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = self.get_actions_list(v)[0] + + def set_sums(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = sum([int(i.format_map(self.params)) for i in v]) + + def set_decode(self, d): + from urllib.parse import unquote_plus + for k, v in d.items(): + k = k.format_map(self.params) + v = v.format_map(self.params) + self.params[k] = unquote_plus(v) + + def set_encode(self, d): + from urllib.parse import quote_plus + for k, v in d.items(): + k = k.format_map(self.params) + v = v.format_map(self.params) + self.params[k] = quote_plus(v) + + def set_escape(self, d): + from xml.sax.saxutils import escape + for k, v in d.items(): + k = k.format_map(self.params) + v = v.format_map(self.params) + self.params[k] = escape(v) + + def set_lower(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = v.format_map(self.params).lower() + + def set_upper(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = v.format_map(self.params).upper() + + def set_capitalize(self, d): + for k, v in d.items(): + k = k.format_map(self.params) + self.params[k] = v.format_map(self.params).capitalize() + + def check_rules(self, rules): + for rule in rules: + rule = rule.format_map(self.params) + if not check_condition(rule): # If one rule of many is false then rule is false overall so exit early + return False + return True # If all rules are successful then rule is true + + def get_actions_list(self, rule_actions): + actions_list = [] + + if not isinstance(rule_actions, list): + rule_actions = [rule_actions] + + for action in rule_actions: + + # Parts are prefixed with percent % so needs to be replaced + if isinstance(action, str) and action.startswith('%'): + action = action.format_map(self.params) + action = self.meta['parts'][action[1:]] + + # Standard actions are strings - add formatted action to list and continue + if isinstance(action, str): + actions_list.append(action.format_map(self.params)) + continue + + # Sublists of actions are lists - recursively add sublists and continue + if isinstance(action, list): + actions_list += self.get_actions_list(action) + continue + + # Rules are dictionaries - successful rules add their actions and stop iterating (like a skin variable) + if self.check_rules(action['rules']): + actions_list += self.get_actions_list(action['value']) + break + + return actions_list diff --git a/script.skinvariables/resources/lib/plugin.py b/script.skinvariables/resources/lib/plugin.py new file mode 100644 index 0000000000..1c5fee1789 --- /dev/null +++ b/script.skinvariables/resources/lib/plugin.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html + +class Plugin(): + routes = { + 'get_player_streams': { + 'module_name': 'resources.lib.lists.playerstreams', + 'import_attr': 'ListGetPlayerStreams'}, + 'set_player_streams': { + 'module_name': 'resources.lib.lists.playerstreams', + 'import_attr': 'ListSetPlayerStreams'}, + 'get_dbitem_movieset_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetMovieSetDetails'}, + 'get_dbitem_movie_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetMovieDetails'}, + 'get_dbitem_tvshow_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetTVShowDetails'}, + 'get_dbitem_season_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetSeasonDetails'}, + 'get_dbitem_episode_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetEpisodeDetails'}, + 'get_dbitem_addon_details': { + 'module_name': 'resources.lib.lists.rpcdetails', + 'import_attr': 'ListGetAddonDetails'}, + 'get_number_sum': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetNumberSum'}, + 'get_split_string': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetSplitString'}, + 'get_jsonrpc': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetJSONRPC'}, + 'get_encoded_string': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetEncodedString'}, + 'get_file_exists': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetFileExists'}, + 'get_selected_item': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListGetSelectedItem'}, + 'run_executebuiltin': { + 'module_name': 'resources.lib.lists.koditools', + 'import_attr': 'ListRunExecuteBuiltin'}, + 'get_filter_files': { + 'module_name': 'resources.lib.lists.filterdir', + 'import_attr': 'ListGetFilterFiles'}, + 'get_filter_dir': { + 'module_name': 'resources.lib.lists.filterdir', + 'import_attr': 'ListGetFilterDir'}, + 'set_filter_dir': { + 'module_name': 'resources.lib.lists.filterdir', + 'import_attr': 'ListSetFilterDir'}, + 'get_container_labels': { + 'module_name': 'resources.lib.lists.filterdir', + 'import_attr': 'ListGetContainerLabels'}, + 'get_shortcuts_node': { + 'module_name': 'resources.lib.shortcuts.node', + 'import_attr': 'ListGetShortcutsNode'}, + 'get_skin_user': { + 'module_name': 'resources.lib.lists.skinusers', + 'import_attr': 'ListGetSkinUser'}, + 'add_skin_user': { + 'module_name': 'resources.lib.lists.skinusers', + 'import_attr': 'ListAddSkinUser'}, + } + + def __init__(self, handle, paramstring): + # plugin:// params configuration + self.handle = handle # plugin:// handle + self.parse_paramstring(paramstring) + + def parse_paramstring(self, paramstring): + from jurialmunkey.parser import parse_paramstring + self.paramstring, *secondary_params = paramstring.split('&&') # plugin://plugin.video.themoviedb.helper?paramstring + self.params = parse_paramstring(self.paramstring) # paramstring dictionary + if not secondary_params: + return + from urllib.parse import unquote_plus + self.params['paths'] = [unquote_plus(i) for i in secondary_params] + + def get_container(self, info): + from jurialmunkey.modimp import importmodule + return importmodule(**self.routes[info]) + + def get_directory(self): + container = self.get_container(self.params.get('info', 'get_filter_files'))(self.handle, self.paramstring, **self.params) + return container.get_directory(**self.params) + + def run(self): + if self.params.get('info') == 'get_params_file': + from resources.lib.shortcuts.futils import read_meta_from_file + path = self.params.get('path') or self.params.get('paths', [None])[0] or '' + self.params = read_meta_from_file(path) if path else {} + self.get_directory() diff --git a/script.skinvariables/resources/lib/script.py b/script.skinvariables/resources/lib/script.py new file mode 100644 index 0000000000..dcb4675604 --- /dev/null +++ b/script.skinvariables/resources/lib/script.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from jurialmunkey.modimp import importmodule + + +class Script(object): + def __init__(self, *args, paramstring=None): + def map_args(arg): + if '=' in arg: + key, value = arg.split('=', 1) + value = value.strip('\'').strip('"') if value else None + return (key, value) + return (arg, True) + + self.params = {} + + if paramstring: + args = [i for i in args] + paramstring.split('&') + + for arg in args: + k, v = map_args(arg) + self.params[k] = v + + routing_table = { + 'set_animation': + lambda **kwargs: importmodule('resources.lib.method', 'set_animation')(**kwargs), + 'run_executebuiltin': + lambda **kwargs: importmodule('resources.lib.method', 'run_executebuiltin')(**kwargs), + 'run_dialog': + lambda **kwargs: importmodule('resources.lib.method', 'run_dialog')(**kwargs), + 'run_progressdialog': + lambda **kwargs: importmodule('resources.lib.method', 'run_progressdialog')(**kwargs), + 'set_player_subtitle': + lambda **kwargs: importmodule('resources.lib.method', 'set_player_subtitle')(**kwargs), + 'set_player_audiostream': + lambda **kwargs: importmodule('resources.lib.method', 'set_player_audiostream')(**kwargs), + 'set_editcontrol': + lambda **kwargs: importmodule('resources.lib.method', 'set_editcontrol')(**kwargs), + 'set_dbid_tag': + lambda **kwargs: importmodule('resources.lib.method', 'set_dbid_tag')(**kwargs), + 'get_jsonrpc': + lambda **kwargs: importmodule('resources.lib.method', 'get_jsonrpc')(**kwargs), + 'add_skinstring_history': + lambda **kwargs: importmodule('resources.lib.method', 'add_skinstring_history')(**kwargs), + 'set_shortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.method', 'set_shortcut')(**kwargs), + 'copy_menufile': + lambda **kwargs: importmodule('resources.lib.shortcuts.method', 'copy_menufile')(**kwargs), + 'copy_menufolder': + lambda **kwargs: importmodule('resources.lib.shortcuts.method', 'copy_menufolder')(**kwargs), + 'set_listitem_to_menunode': + lambda **kwargs: importmodule('resources.lib.shortcuts.method', 'set_listitem_to_menunode')(**kwargs), + 'add_skinshortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.skinshortcuts', 'get_skinshortcuts_menu')(route='add_skinshortcut', **kwargs), + 'del_skinshortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.skinshortcuts', 'get_skinshortcuts_menu')(route='del_skinshortcut', **kwargs), + 'mod_skinshortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.skinshortcuts', 'get_skinshortcuts_menu')(route='mod_skinshortcut', **kwargs), + 'imp_skinshortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.skinshortcuts', 'get_skinshortcuts_menu')(route='imp_skinshortcut', **kwargs), + 'mov_skinshortcut': + lambda **kwargs: importmodule('resources.lib.shortcuts.skinshortcuts', 'get_skinshortcuts_menu')(route='mov_skinshortcut', **kwargs), + } + + def run(self): + if not self.params: + return + routes_available, params_given = set(self.routing_table.keys()), set(self.params.keys()) + try: + route_taken = set.intersection(routes_available, params_given).pop() + except KeyError: + return self.router() + return self.routing_table[route_taken](**self.params) + + def router(self): + if self.params.get('action') == 'buildviews': + from resources.lib.viewtypes import ViewTypes + return ViewTypes().update_xml(skinfolder=self.params.get('folder'), **self.params) + + if self.params.get('action') == 'buildtemplate': + from resources.lib.shortcuts.template import ShortcutsTemplate + return ShortcutsTemplate(template=self.params.get('template')).update_xml(**self.params) + + from resources.lib.skinvariables import SkinVariables + return SkinVariables(template=self.params.get('template'), skinfolder=self.params.get('folder')).update_xml(**self.params) diff --git a/script.skinvariables/resources/lib/shortcuts/browser.py b/script.skinvariables/resources/lib/shortcuts/browser.py new file mode 100644 index 0000000000..3b13dc846f --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/browser.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +from xbmcgui import ListItem, Dialog +from resources.lib.kodiutils import get_localized + + +SHORTCUT_CONFIG = 'skinvariables-shortcut-config.json' +SHORTCUT_FOLDER = 'special://skin/shortcuts/' +PLAYLIST_EXT = ('.xsp', '.m3u', '.m3u8', '.strm', '.wpl') +NO_FOLDER_ITEM = ('grouping://', 'plugin://script.skinvariables/?info=set_filter_dir') + + +def _ask_is_playable(path): + return Dialog().yesno( + get_localized(32050), # Add playlist + f'{path}\n{get_localized(32051)}', # Add playlist as playable shortcut or browesable directory + yeslabel=get_localized(208), # Play + nolabel=get_localized(1024) # Browse + ) + + +class GetDirectoryBrowser(): + def __init__(self, use_details=True, item_prefix=None, use_rawpath=False, allow_links=True, folder_name=None): + self.history = [] + self.filepath = f'{SHORTCUT_FOLDER}{SHORTCUT_CONFIG}' + self.item_prefix = item_prefix or '' + self.use_details = use_details + self.use_rawpath = use_rawpath + self.allow_links = allow_links + self.folder_name = folder_name or f'{get_localized(32052)}...' + self.heading_str = '' + + @property + def definitions(self): + try: + return self._definitions + except AttributeError: + from resources.lib.shortcuts.futils import read_meta_from_file + self._definitions = read_meta_from_file(self.filepath) + return self._definitions + + @staticmethod + def get_formatted_path(path, node=None, link=True): + if not path: + return ('', '') + if node and not link and path.endswith(PLAYLIST_EXT) and _ask_is_playable(path): + return (f'PlayMedia({path})', '') + if (not node) is not (not link): # XOR: Links without nodes return raw path; Folders with nodes return raw path (+ node) + return (path, node) + if path.startswith('script://'): + path = path.replace('script://', '') + return (f'RunScript({path})', '') + if path.startswith('androidapp://'): + path = path.replace('androidapp://', '') + return (f'StartAndroidActivity({path})', '') + return (f'PlayMedia({path})', '') + + def get_formatted_item(self, name, path, icon, node=None, link=True): + if node == 'link': + link = True + node = '' + path, target = self.get_formatted_path(path, node, link) if not self.use_rawpath else (path, node) + item = {"label": name or '', "path": path or '', "icon": icon or '', "target": target or ''} + # from resources.lib.shortcuts.futils import dumps_log_to_file + # dumps_log_to_file({'name': name, 'path': path, 'icon': icon, 'node': node, 'item': item}, filename=f'{name}.json') + return item + + def get_new_item(self, item, allow_browsing=True): + from jurialmunkey.parser import boolean + # Update to new item values + icon = item[1].getArt('thumb') or '' + node = item[1].getProperty('nodetype') or None + name = item[1].getProperty('nodename') or item[1].getLabel() or '' + link = not boolean(item[1].getProperty('isfolder') or False) + path = item[0] or '' + + # If the item is a folder then we open it otherwise return formatted item + if allow_browsing and item[2]: + return self.get_directory(path, icon, name, item, True) + return self.get_formatted_item(name, path, icon, node, link) + + def get_items(self, directory, path, icon, name, item, add_item=False): + directory_items = [i for i in directory.items if self.allow_links or i[2]] # All items if allow links otherwise filter for folders only + + if add_item and path and not path.startswith(NO_FOLDER_ITEM): + li = ListItem(label=self.folder_name, label2=path, path=path, offscreen=True) + li.setArt({'icon': icon, 'thumb': icon}) + li.setProperty('isfolder', 'True') + li.setProperty('nodename', name) + li.setProperty('nodetype', item[1].getProperty('nodetype') or '') + directory_items.insert(0, (path, li, False, )) + + self.heading_str = name or path + items = [i[1] for i in directory_items if i] + x = Dialog().select(heading=self.heading_str, list=items, useDetails=self.use_details) + if x != -1: + item = directory_items[x] + self.history.append((directory, path, icon, name, item, True, )) if item[2] else None # Add old values to history before updating + return self.get_new_item(item) + try: + return self.get_items(*self.history.pop()) + except IndexError: + return [] + + def get_directory(self, path='grouping://shortcuts/', icon='', name='Shortcuts', item=None, add_item=False): + if not path: + return + + from resources.lib.shortcuts.grouping import GetDirectoryGrouping + DirectoryClass = GetDirectoryGrouping + + if not path.startswith('grouping://'): + from resources.lib.shortcuts.jsonrpc import GetDirectoryJSONRPC + DirectoryClass = GetDirectoryJSONRPC + + directory = DirectoryClass(path, definitions=self.definitions, target=item[1].getProperty('nodetype') if item else None) + if not directory.items: + return + + if not item: + li = ListItem(label=name, label2=path, path=path, offscreen=True) + li.setArt({'icon': icon, 'thumb': icon}) + li.setProperty('nodename', name) + item = (path, li, True, ) + + return self.get_items(directory, path, icon, name, item, add_item) diff --git a/script.skinvariables/resources/lib/shortcuts/common.py b/script.skinvariables/resources/lib/shortcuts/common.py new file mode 100644 index 0000000000..2c60e133e1 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/common.py @@ -0,0 +1,53 @@ +import re + + +IMAGE_REGEX = r'image://(.*)/' +ARTWORK_PREFERENCE = ['poster', 'thumb', 'icon', 'landscape', 'fanart'] + + +class GetDirectoryCommon(): + def __init__(self, path, library='video', dbtype='video', definitions=None, target=None): + self.path = path + self.library = library + self.dbtype = dbtype + self.target = target + self.definitions = definitions or {} + + @property + def directory(self): + try: + return self._directory + except AttributeError: + self._directory = self.get_directory() + return self._directory + + @property + def items(self): + try: + return self._items + except AttributeError: + self._items = self.get_items() + return self._items + + def get_artwork_fallback(self, listitem): + artwork = listitem.artwork + artwork_types = ARTWORK_PREFERENCE + for a in artwork_types: + if not artwork.get(a): + continue + artwork['thumb'] = artwork[a] + break + thumb = '' + try: + thumb = artwork.get('thumb') or '' + if thumb.startswith('image://Default'): + regex = re.search(IMAGE_REGEX, thumb) + thumb = regex.group(1) if regex else thumb + thumb = self.definitions.setdefault('icons', {}).get(thumb) or thumb + thumb = f'special://skin/media/{thumb}' if thumb.startswith('Default') else thumb + thumb = thumb or 'special://skin/media/DefaultFolder.png' + except KeyError: + thumb = 'special://skin/media/DefaultFolder.png' + thumb = self.definitions.setdefault('icons', {}).get(thumb.replace('special://skin/media/', '')) or thumb + artwork['thumb'] = thumb + return artwork diff --git a/script.skinvariables/resources/lib/shortcuts/futils.py b/script.skinvariables/resources/lib/shortcuts/futils.py new file mode 100644 index 0000000000..499fd9f9b3 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/futils.py @@ -0,0 +1,55 @@ +import jurialmunkey.futils as jmfutils + + +BASE_PROPERTY = 'SkinVariables.ShortcutsNode' +ADDON_DATA = 'special://profile/addon_data/script.skinvariables/nodes/' +RELOAD_PROPERTY = f'{BASE_PROPERTY}.Reload' +FILE_PREFIX = 'skinvariables-shortcut-' + + +validify_filename = jmfutils.validify_filename + + +class FileUtils(jmfutils.FileUtils): + addondata = ADDON_DATA # Override module addon_data with plugin addon_data + + +FILEUTILS = FileUtils() + + +def get_files_in_folder(folder, regex): + import re + import xbmcvfs + return [x for x in xbmcvfs.listdir(folder)[1] if re.match(regex, x)] + + +def dumps_log_to_file(meta, folder='logging', filename='logging.json', indent=4): + FILEUTILS.dumps_to_file(meta, folder=folder, filename=filename, indent=indent) + + +def reload_shortcut_dir(): + import xbmc + import time + xbmc.executebuiltin(f'SetProperty({RELOAD_PROPERTY},{time.time()},Home)') + + +def write_meta_to_file(meta, folder, filename, indent=4, fileprop=None, reload=True): + FILEUTILS.dumps_to_file(meta, folder=folder, filename=filename, indent=indent) + write_meta_to_prop(meta, fileprop) if fileprop else None + reload_shortcut_dir() if reload else None + + +def write_meta_to_prop(meta, fileprop): + from xbmcgui import Window + Window(10000).setProperty(f'{BASE_PROPERTY}.{fileprop}', jmfutils.json_dumps(meta) if meta else '') + + +def read_meta_from_file(filepath): + meta = jmfutils.load_filecontent(filepath) + return jmfutils.json_loads(meta) if meta else None + + +def read_meta_from_prop(fileprop): + from xbmcgui import Window + meta = Window(10000).getProperty(f'{BASE_PROPERTY}.{fileprop}') + return jmfutils.json_loads(meta) if meta else None diff --git a/script.skinvariables/resources/lib/shortcuts/grouping.py b/script.skinvariables/resources/lib/shortcuts/grouping.py new file mode 100644 index 0000000000..a9e02879fa --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/grouping.py @@ -0,0 +1,55 @@ +from xbmc import getCondVisibility +from resources.lib.shortcuts.common import GetDirectoryCommon +from resources.lib.kodiutils import get_localized + + +class GetDirectoryGrouping(GetDirectoryCommon): + def get_directory(self): + if not self.path: + return [] + try: + self._directory = self.definitions[self.path] + except KeyError: + return [] + return self._directory + + def get_items(self): + from xbmcgui import ListItem + from jurialmunkey.parser import boolean + from resources.lib.kodiutils import ProgressDialog + + def _make_item(i): + if i.get('rule') and not getCondVisibility(i['rule']): + return + listitem = ListItem(label=i['name'], label2=i['path'], path=i['path'], offscreen=True) + listitem.setArt({'icon': i['icon'], 'thumb': i['icon']}) + listitem_isfolder = True + listitem_nodetype = i.get('node') or self.target or '' + if boolean(i['link']): + listitem_nodetype = 'link' + listitem_isfolder = False + listitem.setProperty('nodetype', listitem_nodetype) + item = (i['path'], listitem, listitem_isfolder, ) + return item + + with ProgressDialog('Skin Variables', f'{get_localized(32053)}...\n{self.path}', total=1, logging=2, background=False): + if not self.directory: + return [] + items = [] + for i in self.directory: + if not i: + continue + if isinstance(i, dict): + j = _make_item(i) + items.append(j) if j else None + continue + if '://' not in i: + continue + DirectoryClass = GetDirectoryGrouping + if not i.startswith('grouping://'): + from resources.lib.shortcuts.jsonrpc import GetDirectoryJSONRPC + DirectoryClass = GetDirectoryJSONRPC + directory = DirectoryClass(i, definitions=self.definitions, target=self.target) + new_items = directory.get_items() or [] + items += new_items + return items diff --git a/script.skinvariables/resources/lib/shortcuts/jsonrpc.py b/script.skinvariables/resources/lib/shortcuts/jsonrpc.py new file mode 100644 index 0000000000..cdad177ec2 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/jsonrpc.py @@ -0,0 +1,63 @@ +from resources.lib.shortcuts.common import GetDirectoryCommon +from resources.lib.kodiutils import get_localized + + +DIRECTORY_PROPERTIES_BASIC = ["title", "art", "file", "fanart"] +DIRECTORY_SOURCES = { + "sources://video/": "video", + "sources://music/": "music", + "sources://pictures/": "pictures", + "sources://programs/": "programs", + "sources://files/": "files", + "sources://games/": "game", +} + + +class GetDirectoryJSONRPC(GetDirectoryCommon): + def get_directory_path(self): + from jurialmunkey.jsnrpc import get_directory + return get_directory(self.path, DIRECTORY_PROPERTIES_BASIC) + + def get_directory_source(self): + from contextlib import suppress + from jurialmunkey.jsnrpc import get_jsonrpc + response = get_jsonrpc("Files.GetSources", {"media": DIRECTORY_SOURCES[self.path]}) + with suppress(KeyError): + result = response['result']['sources'] + return result or [{}] + + def get_directory(self): + if not self.path: + return [] + + if self.path in DIRECTORY_SOURCES: + func = self.get_directory_source + else: + func = self.get_directory_path + + self._directory = func() + + return self._directory + + + + def get_items(self): + + from resources.lib.lists.filterdir import ListItemJSONRPC + + def _make_item(i): + if not i: + return + listitem_jsonrpc = ListItemJSONRPC(i, library=self.library, dbtype=self.dbtype) + listitem_jsonrpc.infolabels['title'] = listitem_jsonrpc.label + listitem_jsonrpc.infoproperties['nodetype'] = self.target or '' + listitem_jsonrpc.artwork = self.get_artwork_fallback(listitem_jsonrpc) + listitem_jsonrpc.label2 = listitem_jsonrpc.path + item = (listitem_jsonrpc.path, listitem_jsonrpc.listitem, listitem_jsonrpc.is_folder, ) + return item + + from resources.lib.kodiutils import ProgressDialog + with ProgressDialog('Skin Variables', f'{get_localized(32053)}...\n{self.path}', total=1, logging=2, background=False): + if not self.directory: + return [] + return [j for j in (_make_item(i) for i in self.directory) if j] diff --git a/script.skinvariables/resources/lib/shortcuts/method.py b/script.skinvariables/resources/lib/shortcuts/method.py new file mode 100644 index 0000000000..148198fd1f --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/method.py @@ -0,0 +1,168 @@ +import xbmc +import xbmcgui +from resources.lib.kodiutils import get_localized + + +LISTITEM_VALUE_PAIRS = (('label', 'Label'), ('icon', 'Icon'), ('path', 'FolderPath')) +DEFAULT_MODES = ('submenu', 'widgets') + + +def get_target_from_window(): + if xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'): + return 'videos' + if xbmc.getCondVisibility('Window.IsVisible(MyMusicNav.xml)'): + return 'music' + if xbmc.getCondVisibility('Window.IsVisible(MyPics.xml)'): + return 'pictures' + if xbmc.getCondVisibility('Window.IsVisible(MyPrograms.xml)'): + return 'programs' + if xbmc.getCondVisibility('Window.IsVisible(MyPVRGuide.xml)'): + return 'tvguide' + if xbmc.getCondVisibility('Window.IsVisible(MyPVRChannels.xml)'): + return 'tvchannels' + + +def get_item_from_listitem(item=None, value_pairs=None, listitem='Container.ListItem'): + item = item or {} + value_pairs = value_pairs or LISTITEM_VALUE_PAIRS + return {k: xbmc.getInfoLabel(f'{listitem}.{v}') or item.get(k) or '' for k, v in value_pairs} + + +class MenuNode(): + def __init__(self, skin, menufiles=None, levels=1): + self.skin = skin + self.menufiles = menufiles or [] + self.levels = int(levels) + + def select_menu(self): + if not self.menufiles: + return + x = xbmcgui.Dialog().select(get_localized(32069), self.menufiles) + if x == -1: + return + return self.menufiles[x] + + def get_menu(self): + self._menu = self.select_menu() + return self._menu + + @property + def menu(self): + try: + return self._menu + except AttributeError: + return self.get_menu() + + def select_node(self, mode, guid, level=0): + from resources.lib.shortcuts.node import ListGetShortcutsNode + lgsn = ListGetShortcutsNode(-1, '') + lgsn.get_directory(menu=self.menu, skin=self.skin, item=None, mode=mode, guid=guid, func='node') + if lgsn.menunode is None: + return + choices = [f'{get_localized(32071)}...'] + if level < self.levels: # Only add the options to traverse submenu/widgets if we're not deeper than our max level + from jurialmunkey.parser import parse_localize + choices = [parse_localize(i.get('label') or '') for i in lgsn.menunode] + choices + x = xbmcgui.Dialog().select(get_localized(32069), choices) + if x == -1: + return + if choices[x] == f'{get_localized(32071)}...': + return lgsn + y = xbmcgui.Dialog().select(get_localized(32070), DEFAULT_MODES) + if y == -1: + return self.select_node(mode, guid, level) # Go back to previous level + return self.select_node(DEFAULT_MODES[y], lgsn.menunode[x].get('guid'), level=level + 1) # Go up to next level + + def set_item_to_node(self, item): + lgsn = self.select_node('submenu', None) + if not lgsn: + return + lgsn.menunode.append(item) + lgsn.write_meta_to_file() + lgsn.do_refresh() + + +def set_listitem_to_menunode(set_listitem_to_menunode, skin, label=None, icon=None, path=None, target=None, use_listitem=True): + if not set_listitem_to_menunode or not skin: + return + item = {'label': label, 'icon': icon, 'path': path, 'target': target} + + if use_listitem: + item = get_item_from_listitem(item) + item['target'] = get_target_from_window() or target or 'videos' + + if not item['path']: + xbmcgui.Dialog().ok(heading=get_localized(32068), message=get_localized(32067)) + return + + MenuNode(skin, menufiles=set_listitem_to_menunode.split('||')).set_item_to_node(item) + + +def set_shortcut(set_shortcut, use_rawpath=False): + import xbmc + from jurialmunkey.parser import boolean + from jurialmunkey.window import WindowProperty + from resources.lib.shortcuts.browser import GetDirectoryBrowser + + with WindowProperty(('IsSkinShortcut', 'True')): + item = GetDirectoryBrowser(use_rawpath=boolean(use_rawpath)).get_directory() + + if not item: + return + + item = {f'{set_shortcut}.{k}': v for k, v in item.items()} + + for k, v in item.items(): + if not isinstance(v, str): + continue + xbmc.executebuiltin(f'Skin.SetString({k},{v})') + + +def copy_menufolder(copy_menufolder, skin): + from resources.lib.shortcuts.futils import read_meta_from_file, get_files_in_folder + + files = get_files_in_folder(copy_menufolder, r'.*\.json') + if not files: + xbmcgui.Dialog().ok(get_localized(32076), f'copy_menufolder={copy_menufolder}\nskin={skin}') + return + + msg = get_localized(32072).format( + filename=get_localized(32073).format(skin=skin), + content=get_localized(32074).format(folder=copy_menufolder)) + msg = f'{msg}\n{get_localized(32043)}' + + x = xbmcgui.Dialog().yesno(get_localized(32075), msg) + + if not x or x == -1: + return + + from resources.lib.shortcuts.node import assign_guid + from resources.lib.shortcuts.futils import write_meta_to_file + + files = ((read_meta_from_file(f'{copy_menufolder}{f}'), f) for f in files if f) + for meta, file in files: + if not meta or not file: + continue + write_meta_to_file( + assign_guid(meta), + folder=skin, + filename=file, + fileprop=f'{skin}-{file}', + reload=True) + + +def copy_menufile(copy_menufile, filename, skin): + from resources.lib.shortcuts.futils import read_meta_from_file, write_meta_to_file, FILE_PREFIX + if not copy_menufile or not filename or not skin: + raise ValueError(f'copy_menufile details missing\ncopy_menufile={copy_menufile}\nfilename={filename}\nskin={skin}') + return + filename = f'{FILE_PREFIX}{filename}.json' + meta = read_meta_from_file(copy_menufile) + if meta is None: + raise ValueError(f'copy_menufile content missing\ncopy_menufile={copy_menufile}\nfilename={filename}\nskin={skin}') + return + x = xbmcgui.Dialog().yesno(get_localized(32075), f'{get_localized(32072).format(filename=filename, content=copy_menufile)}\n{get_localized(32043)}') + if not x or x == -1: + return + from resources.lib.shortcuts.node import assign_guid + write_meta_to_file(assign_guid(meta), folder=skin, filename=filename, fileprop=f'{skin}-{filename}', reload=True) diff --git a/script.skinvariables/resources/lib/shortcuts/node.py b/script.skinvariables/resources/lib/shortcuts/node.py new file mode 100644 index 0000000000..64bca44438 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/node.py @@ -0,0 +1,912 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import random +import resources.lib.shortcuts.futils as shortcutfutils +from xbmcgui import ListItem, Dialog, INPUT_NUMERIC +from jurialmunkey.litems import Container +from jurialmunkey.parser import boolean, parse_localize +from resources.lib.kodiutils import get_localized +from resources.lib.filters import get_filters, is_excluded + +FILE_PREFIX = shortcutfutils.FILE_PREFIX + +ICON_DIR = 'special://skin/extras/icons/' +SKIN_DIR = 'special://skin/shortcuts/' +CONTEXTMENU_CONFIGFILE = f'{SKIN_DIR}/skinvariables-shortcut-context.json' +ICON_FOLDER = f'{ICON_DIR}folder.png' +ICON_ADD = f'{ICON_DIR}circle-plus.png' +ICON_WIDGET = f'{ICON_DIR}shapes.png' +GROUPING_DEFAULT = 'grouping://shortcuts/' + + +CONTEXTMENU_BASIC = [ + [get_localized(32090), "do_choose", []], + [get_localized(15217), "do_action", []], + [get_localized(118), "do_edit", ["label"]], + [get_localized(32077), "do_icon", []], + [get_localized(13332), "do_move", ["-1"]], + [get_localized(13333), "do_move", ["1"]], + [get_localized(115), "do_copy", []], + [get_localized(117), "do_delete", []], + [get_localized(32091), "do_refresh", []], +] + + +CONTEXTMENU_MAINMENU = [ + [get_localized(32081), "do_refresh", ["True"]], + [get_localized(32092), "do_submenu", []], + [get_localized(32093), "do_widgets", []], +] + + +def get_default_item(): + return { + 'label': '', + 'icon': '', + 'path': '', + 'target': '', + 'submenu': [], + 'widgets': [] + } + + +class FormatDict(dict): + def __missing__(self, key): + if 'listitem_obj' in self.keys(): + return self['listitem_obj'].getProperty(key) + return '' + + +class ContextMenuDict(dict): + def __getitem__(self, key): + if key != 'basic': # Basic menu should be appended to all other types + return dict.__getitem__(self, 'basic') + dict.__getitem__(self, key) + return dict.__getitem__(self, key) + + def __missing__(self, key): + if key == 'basic': + return CONTEXTMENU_BASIC + if key == 'mainmenu': + return CONTEXTMENU_MAINMENU + return [] + + +def get_contextmenu_config(): + return ContextMenuDict(shortcutfutils.read_meta_from_file(CONTEXTMENU_CONFIGFILE) or {}) + + +def get_contextmenu(node, mode='submenu'): + contextmenu = get_contextmenu_config() + if not node: + return contextmenu['mainmenu'] + if mode == 'widgets': + return contextmenu['widgets'] + return contextmenu['basic'] + + +def get_submenunode(meta, mode='submenu'): + try: + return meta[mode] + except KeyError: + meta[mode] = [] + return meta[mode] + + +def get_submenuitem(meta, n): + try: + return meta[n] + except IndexError: + meta.append(get_default_item()) + return meta[-1] + + +def get_menuguid(meta, guid, mode='submenu', subkeys=('submenu', 'widgets')): + """ Lookup menu node using guid and return tuple of meta for item and current node """ + if not meta or not guid: + return + + def get_menuguid_item(item, node): + name = parse_localize(item.get('label', '')) + if item.get('guid') == guid: + return (get_submenunode(item, mode), node, name) + for k in subkeys: + subitem, subnode, subname = get_menuguid_iter(item.get(k) or []) + if not isinstance(subitem, list): + continue + subnode = subnode + node + return (subitem, subnode, subname) + return (None, None, None) + + def get_menuguid_iter(menu): + for x, i in enumerate(menu): + item, node, name = get_menuguid_item(i, [x]) + if not isinstance(item, list): + continue + return (item, node, name) + return (None, None, None) + + item, node, name = get_menuguid_iter(meta) + return (item, tuple(node), name) if node else (item, tuple(), name) + + +def get_menunode(meta, node, mode='submenu'): + """ Lookup menu node using node value and return tuple of meta for item and current node """ + if not meta or not node: # Return base of meta if no node because were in main menu + return (meta, node, '') + + for n in node[:-1]: # Walk submenus until last item + meta = get_submenuitem(meta, n) + meta = get_submenunode(meta) + + for n in node[-1:]: # Last item we get in the current mode + meta = get_submenuitem(meta, n) + name = parse_localize(meta.get('label', '')) + meta = get_submenunode(meta, mode) + + return (meta, node, name) + + +def get_nodename(node): + return '.'.join([f'{n}' for n in node]) + + +def get_menunode_item(menunode, x): + try: + return menunode[x] + except IndexError: + menunode.append(get_default_item()) + return menunode[x] + + +def assign_guid(meta): + id_list = [] + + def get_unique_guid(guid=None): + guid = guid or f'guid-{random.randrange(16**8):08x}' + return guid if guid not in id_list else get_unique_guid() + + def set_unique_guid(item): + item['guid'] = get_unique_guid(item.get('guid')) + return item['guid'] + + def walk_item_lists(meta): + for item in meta: + id_list.append(set_unique_guid(item)) + walk_item_lists(item['submenu']) if 'submenu' in item else None + walk_item_lists(item['widgets']) if 'widgets' in item else None + + walk_item_lists(meta) if meta else None + return meta + + +def cache_meta_from_file(filepath, fileprop, refresh=False): + meta = shortcutfutils.read_meta_from_prop(fileprop) if not refresh else None + if meta is None: + meta = shortcutfutils.read_meta_from_file(filepath) + meta = assign_guid(meta) + shortcutfutils.write_meta_to_prop(meta, fileprop) + return meta + + +def get_menunode_lookup(lookup, skin, menu, item=None, node=None, mode=None, guid=None, **kwargs): + node_obj = ListGetShortcutsNode(None, None) + node_obj.refresh = True # Refresh mem cache because we want to build from the file + node_obj.skin = skin + node_obj.menu = menu + node_obj.item = item + node_obj.node = node + node_obj.mode = mode + node_obj.guid = guid + node_obj.edit = False + + if not node_obj.menunode: + return '' + + key_filters = {k[7:]: v for k, v in kwargs.items() if k.startswith('filter_')} + + def _is_filtered(i): + for k, v in key_filters.items(): + if k not in i: + return + if i[k] != v: + return + return i + + item = None + + for x, i in enumerate(node_obj.menunode): + if not isinstance(i, dict): + i = {'value': i} + item = _is_filtered(i) + if item: + break + + if not item: + return '' + + return item.get(lookup) or '' + + +class GetDirectoryItems(): + def __init__(self, grouping=GROUPING_DEFAULT, use_rawpath=False, folder_name=None): + self.grouping = grouping + self.use_rawpath = use_rawpath + self.folder_name = folder_name + + @property + def directory_browser(self): + try: + return self._directory_browser + except AttributeError: + from resources.lib.shortcuts.browser import GetDirectoryBrowser + self._directory_browser = GetDirectoryBrowser(use_rawpath=True, allow_links=False, folder_name=self.folder_name) + return self._directory_browser + + @property + def directory_jsonrpc(self): + try: + return self._directory_jsonrpc + except AttributeError: + from resources.lib.shortcuts.jsonrpc import GetDirectoryJSONRPC + self._directory_jsonrpc = GetDirectoryJSONRPC(self.item_folder['path'], definitions=self.directory_browser.definitions, target=self.item_folder['target']) + return self._directory_jsonrpc + + @property + def item_folder(self): + try: + return self._item_folder + except AttributeError: + self._item_folder = self.get_item_folder() + return self._item_folder + + @property + def items(self): + try: + return self._items + except AttributeError: + self._items = self.get_items() + return self._items + + def get_item_folder(self): + from jurialmunkey.window import WindowProperty + with WindowProperty(('IsSkinShortcut', 'True')): + self._item_folder = self.directory_browser.get_directory(path=self.grouping) + return self._item_folder + + def get_items(self): + if not self.item_folder: + return + + if not self.directory_jsonrpc.items: + return + + if not boolean(self.use_rawpath): + self.directory_browser.use_rawpath = False + self.directory_browser.allow_links = True + + def _configure_item(i): + i[1].setProperty('isfolder', 'True' if i[2] else 'False') + return i + + return (self.directory_browser.get_new_item(_configure_item(i), allow_browsing=False) for i in self.directory_jsonrpc.items) + + +class NodeProperties(): + @property + def fileprop(self): + try: + return self._fileprop + except AttributeError: + if not self.skin or not self.filename: + return + self._fileprop = f'{self.skin}-{self.filename}' + return self._fileprop + + @property + def filepath(self): + try: + return self._filepath + except AttributeError: + if not self.skin or not self.filename: + return + self._filepath = f'{shortcutfutils.ADDON_DATA}{self.skin}/{self.filename}' + return self._filepath + + @property + def filename(self): + try: + return self._filename + except AttributeError: + if not self.menu: + return + self._filename = shortcutfutils.validify_filename(f'{FILE_PREFIX}{self.menu}.json') + return self._filename + + @property + def meta(self): + try: + return self._meta + except AttributeError: + if not self.filepath: + return + meta = self.get_meta(self.refresh) + if not meta: + return + self._meta = meta + return self._meta + + @property + def menunode(self): + try: + return self._menunode + except AttributeError: + self._menunode, self.node, self.name = get_menuguid(self.meta, self.guid, self.mode) or get_menunode(self.meta, self.node, self.mode) + return self._menunode + + @property + def nodename(self): + try: + return self._nodename + except AttributeError: + self._nodename = get_nodename(self.node) + return self._nodename + + @property + def node(self): + return self._node + + @node.setter + def node(self, value): + try: + self._node = tuple([int(i) for i in value.split('.') if i]) + except (TypeError, AttributeError): + self._node = tuple() + + @property + def mode(self): + try: + return self._mode or 'submenu' + except AttributeError: + self._mode = 'submenu' + return self._mode + + @mode.setter + def mode(self, value): + self._mode = value + + @property + def edit(self): + try: + return self._edit + except AttributeError: + self._edit = False + return self._edit + + @edit.setter + def edit(self, value): + self._edit = boolean(value) + + +class NodeSubmenuMethods(): + def do_submenu_item(self, mode='submenu'): + x = int(self.item) + from resources.lib.shortcuts.browser import GetDirectoryBrowser + from jurialmunkey.window import WindowProperty + with WindowProperty(('IsSkinShortcut', 'True')): + item = GetDirectoryBrowser().get_directory() + if not item: + return + self.get_menunode_item(x).setdefault(mode, []).append(item) + self.write_meta_to_file() + + def do_widgets_item(self): + self.do_submenu_item('widgets') + + def do_submenu(self, mode='submenu'): + + node = [str(i) for i in self.node] + [str(self.item)] + node = '.'.join(node) + + def get_choices_item(i): + item = i[1] + item.setLabel2(i[0]) + icon = item.getArt('icon') or item.getArt('thumb') or ICON_WIDGET + item.setArt({'icon': icon, 'thumb': icon}) + return item + + def get_add_item(): + item = ListItem(label=f'{get_localized(32078)}...') + item.setArt({'icon': ICON_ADD, 'thumb': ICON_ADD}) + return item + + # Generate new class object for node and get items to select + submenu_container = ListGetShortcutsNode(self.handle, self.paramstring, **self.params) + items = submenu_container.get_directory(menu=self.menu, skin=self.skin, node=node, mode=mode, func='list') + choices = [get_choices_item(i) for i in items] + [get_add_item()] + x = Dialog().select(get_localized(1034), list=choices, useDetails=True) + + # User cancelled so we leave + if x == -1: + return + + contextmenu = get_contextmenu(node, mode) + + y = 0 # TODO CHECK FOR CUSTOM LIST CONTEXT + if x != len(items): # Last item is ADD so we dont show contextmenu for it. We always do choose shortcut for ADD. + item = choices[x] + format_mapping = {'label': item.getLabel(), 'listitem_obj': item} + format_mapping = FormatDict(format_mapping) + y = Dialog().select(item.getLabel(), list=[cm_label.format_map(format_mapping) for cm_label, *_ in contextmenu]) + + # User cancelled so we go back to original dialog + if y == -1: + return self.do_submenu(mode) + + # Generate new class object for item and do the action + from copy import deepcopy + action = contextmenu[y][1] + params = deepcopy(self.params) + params['paths'] = contextmenu[y][2] + submenu_container = ListGetShortcutsNode(-1, '', **params) + submenu_container.get_directory(menu=self.menu, skin=self.skin, node=node, mode=mode, item=x, func=action) + + return self.do_submenu(mode) + + def do_widgets(self): + return self.do_submenu(mode='widgets') + + +class NodeMethods(): + def get_menunode_item(self, x): + return get_menunode_item(self.menunode, x) + + def do_refresh(self, restore=False, executebuiltin=None): + restore = boolean(restore) + if restore and not Dialog().yesno(get_localized(32081), f'{get_localized(32082)}\n{get_localized(32043)}'): + return + self._meta = self.get_meta(refresh=True, restore=restore) + self.do_rebuild(dialog=False, executebuiltin=executebuiltin) + + def do_rebuild(self, dialog=True, executebuiltin=None): + if dialog and not Dialog().yesno(get_localized(32079), get_localized(32080)): + return + self.write_meta_to_file(reload=False) + from resources.lib.script import Script + Script(paramstring=f'action=buildtemplate&force').run() + if not executebuiltin: + return + import xbmc + xbmc.Monitor().waitForAbort(0.4) + xbmc.executebuiltin(executebuiltin) + + def do_open(self): + x = int(self.item) + path = self.get_menunode_item(x).get('path') + target = self.get_menunode_item(x).get('target') + if not path: + return + import xbmc + import xbmcplugin + xbmcplugin.setResolvedUrl(self.handle, False, ListItem()) + xbmc.Monitor().waitForAbort(0.2) + xbmc.executebuiltin('ActivateWindow({target},{path},return)' if target else path) + + def do_icon(self, key='icon', value=None, heading=None, icon_dir=ICON_DIR): + """ + Set property[key] to value or prompt user to browse images in icon_dir if no value specified + """ + x = int(self.item) + + heading = heading or get_localized(32077) + + new_value = value or Dialog().browse(type=2, heading=heading, useThumbs=True, defaultt=icon_dir, shares="files") + if not new_value or new_value == -1 or new_value == icon_dir: + return + self.get_menunode_item(x)[key] = new_value + self.write_meta_to_file() + + def do_copy(self): + x = int(self.item) + from copy import deepcopy + self.menunode.append(deepcopy(self.get_menunode_item(x))) + self.write_meta_to_file() + + def do_delete(self, warning=True): + x = int(self.item) + n = Dialog().yesno(heading=get_localized(117), message=get_localized(32043)) if boolean(warning) else 1 + if not n or n == -1: + return + self.menunode.pop(x) + self.write_meta_to_file() + + def do_toggle(self, key='disabled'): + """ + Toggles property[key] between 'True' and empty + """ + x = int(self.item) + current = self.get_menunode_item(x).get(key) + self.get_menunode_item(x)[key] = 'True' if not current else '' + self.write_meta_to_file() + + def do_executebuiltin(self, *args): + if not args: + return + import xbmc + for i in args: + xbmc.executebuiltin(i) + + def do_edit(self, key='label', value=None, heading=None, use_prop_pairs=False): + """ + key, value = property to edit and value to set + heading = heading of select dialog when use_prop_pairs enabled + use_prop_pairs allows for selecting key/values using & separated list with = partition for key value pairs + -- e.g. foo=bar&fizz=buzz will show a list with foo|fizz as options that set bar|buzz respectively + -- 'edit' as value will prompt input via keyboard + -- 'null' as value will delete value for key + """ + x = int(self.item) + + heading = heading or get_localized(21435) + + def _get_items(): + items = [(k, v if s else k) for k, s, v in (i.partition('=') for i in value.split("&") if i)] + preselect = self.get_menunode_item(x).get(key) + preselect = next((x for x, i in enumerate(items) if i[1] == preselect), -1) + choice = Dialog().select(heading=heading, list=[i[0] for i in items], preselect=preselect) + if choice == -1: + return -1 + choice = items[choice][1] + if choice == 'edit': + return None + return choice + + def _get_input(): + return Dialog().input(heading=heading, defaultt=parse_localize(self.get_menunode_item(x).get(key) or '')) + + if boolean(use_prop_pairs): + value = _get_items() + if value is None: + value = _get_input() + if value == -1: + return + if not value: + return + + self.get_menunode_item(x)[key] = value if value != 'null' else '' + self.write_meta_to_file() + + def do_numeric(self, key='limit', value=None, heading=None): + """ + Set property[key] to a numeric value. + Prompts for user input if value not specified. + """ + x = int(self.item) + + heading = heading or get_localized(21435) + + if not value and value != 0: + value = Dialog().input(heading=heading, type=INPUT_NUMERIC, defaultt=parse_localize(self.get_menunode_item(x).get(key) or '')) + if value == -1: + return + + self.get_menunode_item(x)[key] = str(value or '') + self.write_meta_to_file() + + def do_action(self, prefix=None, grouping=GROUPING_DEFAULT, use_rawpath=False): + """ + Update path and target for item by giving user option to browse or edit + Specify prefix to set a specific property e.g. prefix=myshortcut updates myshortcut_path myshortcut_target + Specify grouping to open at grouping other than basedir default + """ + x = int(self.item) + menunode_item = self.get_menunode_item(x) + path = menunode_item.get('path') or '' + target = menunode_item.get('target') or '' + a = Dialog().yesnocustom( + heading=get_localized(15217), message=path, + yeslabel='Edit', nolabel='Browse', customlabel='Cancel') + if a == 2 or a == -1: + return + if a == 1: + path = Dialog().input(heading=get_localized(15217), defaultt=path) + else: + from resources.lib.shortcuts.browser import GetDirectoryBrowser + from jurialmunkey.window import WindowProperty + with WindowProperty(('IsSkinShortcut', 'True')): + item = GetDirectoryBrowser(use_rawpath=boolean(use_rawpath)).get_directory(path=grouping) + try: + path = item['path'] + target = item['target'] + except TypeError: + return + if not path: + return + prefix = f'{prefix}_' if prefix else '' + item = { + f'{prefix}path': path, + f'{prefix}target': target + } + menunode_item.update(item) + self.write_meta_to_file() + + def do_list_del(self, grouping=GROUPING_DEFAULT, use_rawpath=False, **kwargs): + directory_item_getter = GetDirectoryItems(grouping=grouping, use_rawpath=use_rawpath, folder_name='Delete list...') + items = directory_item_getter.items or [] + paths = [i['path'] for i in items if i and i.get('path')] + + def _is_included(i): + if not i: + return + if i.get('path') not in paths: + return + for k, v in kwargs.items(): + if k not in i: + return + if i[k] != v: + return + return i + + index = [x for x, i in enumerate(self.menunode) if _is_included(i)] + + if not index: + Dialog().ok(get_localized(32087), get_localized(32089)) + return + if not Dialog().yesno(get_localized(32087), get_localized(32088).format(item_count=len(index))): + return + for x in sorted(index, reverse=True): + del self.menunode[x] + self.write_meta_to_file() + + def do_list_add(self, grouping=GROUPING_DEFAULT, use_rawpath=False, item_limit=30, **kwargs): + """ + Choose a list to add multiple items automatically + Specific kwargs as additional properties to add to items + """ + x = int(self.item) + item_limit = int(item_limit) + + directory_item_getter = GetDirectoryItems(grouping=grouping, use_rawpath=use_rawpath, folder_name='Add list...') + items = directory_item_getter.items + if not items: + Dialog().ok(get_localized(32083), get_localized(32085)) + return + directory_jsonrpc_items = directory_item_getter.directory_jsonrpc.items + + def _update_item(i): + i.update(kwargs) + return i + + paths = [i['path'] for i in self.menunode if i and i.get('path')] + items = [_update_item(i) for i in items if i and i['path'] not in paths] + + if len(items) < 1: + Dialog().ok(get_localized(32083), get_localized(32085)) + return + + if len(items) > item_limit: + Dialog().ok(get_localized(32083), get_localized(32086).format(item_count=len(items), item_limit=item_limit)) + return + + if not Dialog().yesno(get_localized(32083), get_localized(32084).format( + item_count=len(items), + skip_count=len(directory_jsonrpc_items) - len(items))): + return + + for y, item in enumerate(items): + self.menunode.insert(x + y + 1, item) # Add enumerator to original position to insert in order + + self.write_meta_to_file() + + def do_choose(self, prefix=None, grouping=GROUPING_DEFAULT, create_new=False, use_rawpath=False, refocus=None, window_prop=None, window_id=None, **kwargs): + """ + Wrapper for do_action which also sets icon and label + Specify prefix to set a specific property e.g. prefix=myshortcut updates myshortcut_path myshortcut_target myshortcut_icon myshortcut_label + Specify create_new boolean to insert in place, otherwise updates item + Specify additional kwargs to add default properties to item + """ + x = int(self.item) + from resources.lib.shortcuts.browser import GetDirectoryBrowser + from jurialmunkey.window import WindowProperty + with WindowProperty(('IsSkinShortcut', 'True')): + item = GetDirectoryBrowser(use_rawpath=boolean(use_rawpath)).get_directory(path=grouping) + if not item: + return + item.update(kwargs) # Allow adding in additional forced properties + item = {f'{prefix}_{k}': v for k, v in item.items()} if prefix else item + if boolean(create_new): + x = x + 1 + self.menunode.insert(x, item) + else: + self.get_menunode_item(x).update(item) + self.write_meta_to_file() + + self.do_windowprop(window_prop, x, window_id) + self.do_refocus(refocus, x) + + def do_new(self, prefix=None, grouping=GROUPING_DEFAULT, use_rawpath=False, refocus=None, window_prop=None, window_id=None, **kwargs): + """ + Wrapper for do_choose that forces create_new=True + """ + self.do_choose( + prefix=prefix, grouping=grouping, create_new=True, use_rawpath=use_rawpath, + refocus=refocus, window_prop=window_prop, window_id=window_id, **kwargs) + + def do_move(self, move=0, refocus=None, window_prop=None, window_id=None, offset=None): + x = int(self.item) + y = int(move) + nodeitem = self.menunode.pop(x) + nodesize = len(self.menunode) + + def _get_offset_x(): + if offset is None: + return x + y + if y < 0: + return int(offset) + 1 + y + if y > 0: + return int(offset) - 1 + y + return int(offset) + + x = _get_offset_x() + x = x if x <= nodesize else 0 # Loop back to top + x = x if x >= 0 else nodesize + 1 # Loop back to bottom + + self.menunode.insert(x, nodeitem) + self.write_meta_to_file() + + x = self.menunode.index(nodeitem) + + self.do_windowprop(window_prop, x, window_id) + self.do_refocus(refocus, x) + + def do_windowprop(self, window_prop, x, window_id=None): + import xbmc + if not window_prop or x is None: + return + window_id = '' if not window_id else f',{window_id}' + xbmc.executebuiltin(f'SetProperty({window_prop},{self.get_url(x)}{window_id})') + + @staticmethod + def do_refocus(refocus, x, sleep=0.2): + import xbmc + if not refocus or x is None: + return + xbmc.Monitor().waitForAbort(sleep) # Wait a moment before refocusing to make sure has updated + xbmc.executebuiltin(f'SetFocus({refocus},{x},absolute)') + + +class ListGetShortcutsNode(Container, NodeProperties, NodeMethods, NodeSubmenuMethods): + refresh = False + update_listing = False + + def get_url(self, x, node_name=None): + url = f'plugin://script.skinvariables/?info=get_shortcuts_node&menu={self.menu}&skin={self.skin}&mode={self.mode}&item={x}' + url = url if not self.node else f'{url}&node={node_name or get_nodename(self.node)}' + url = url if not self.guid else f'{url}&guid={self.guid}' + return url + + def get_meta(self, refresh=False, restore=False): + if not self.filepath: + return + meta = cache_meta_from_file(self.filepath, fileprop=self.fileprop, refresh=refresh) if not restore else None + if meta is None: + meta = cache_meta_from_file(f'{SKIN_DIR}{self.filename}', fileprop=self.fileprop, refresh=refresh) # Get from skin + shortcutfutils.write_meta_to_file(meta, folder=self.skin, filename=self.filename, fileprop=self.fileprop) if meta is not None else None # Write to addon_data + return meta + + def write_meta_to_file(self, reload=True): + shortcutfutils.write_meta_to_file(assign_guid(self.meta), folder=self.skin, filename=self.filename, fileprop=self.fileprop, reload=reload) + + def get_directory_items(self, blank=False, filters=None): + + contextmenu = get_contextmenu_config() + + def _is_filtered(i): + if not filters: + return i + for _, f in filters.items(): + if is_excluded({'infolabels': i}, **f): + return + return i + + def _make_item(x, i): + if (not i or boolean(i.get('disabled'))) and not blank and not self.edit: + return + + url = self.get_url(x, node_name) + list_name = f'{node_name}.{x}' if self.node else f'{x}' + + i['item'] = f'{x}' + i['node'] = f'{node_name}' if self.node else '' + i['list'] = f'{list_name}' if list_name else '' + i['menu'] = f'{self.menu}' if self.menu else '' + i['skin'] = f'{self.skin}' if self.skin else '' + i['mode'] = f'{self.mode}' if self.mode else '' + i['name'] = f'{self.name}' if self.name else '' + + submenu = i.pop('submenu', []) + widgets = i.pop('widgets', []) + + if not blank and not _is_filtered(i): + return + + target = i.get('target', '') + name = parse_localize(i.pop('label', '')) + path = i.get('path', '') # if target else f'{url}&func=do_open' + icon = i.pop('icon', '') + + listitem = ListItem(label=name, label2='', path=path, offscreen=True) + listitem.setArt({'icon': icon, 'thumb': icon}) + listitem.setProperties({k: v for k, v in i.items() if k and v}) + listitem.setProperty('isPlayable', 'True') if not target else None + listitem.setProperty('target', target) + listitem.setProperty('url', url) + listitem.setProperty('label', name) + listitem.setProperty('hasSubmenu', 'True') if submenu else None + listitem.setProperty('hasWidgets', 'True') if widgets else None + listitem_isfolder = True if target else False + + contextitems = contextmenu['basic'] + if not self.node: # Main menu options + contextitems = contextmenu['mainmenu'] + format_mapping = {'label': name, 'path': path, 'icon': icon, 'name': name, 'target': target} + format_mapping.update(i) + format_mapping = FormatDict(format_mapping) + listitem_contextmenu = [ + (cm_label.format_map(format_mapping), f'RunPlugin({url}&func={cm_action}{"&&" if cm_params else ""}{"&&".join(cm_params)})'.format_map(format_mapping), ) + for cm_label, cm_action, cm_params in contextitems] + + listitem.addContextMenuItems(listitem_contextmenu) + item = (path, listitem, listitem_isfolder, ) + return item + + node_name = get_nodename(self.node) + + if blank: + return [_make_item(0, {'label': get_localized(32078), 'blank': 'True'})] + + return [j for j in (_make_item(x, i) for x, i in enumerate(self.menunode or [])) if j] + + def get_directory( + self, + menu=None, # The menu filename + skin=None, # The skin addon ID + item=None, # The item index of the current menu + node=None, # Tuple of point separated submenu indicies to get to current level + mode=None, # Get widgets or submenu items - defaults to submenu + func=None, # The method to run + guid=None, # Unique identifier for group + edit=None, # Edit mode if on all items are shown even if disabled + **kwargs + ): + + self.menu = menu + self.skin = skin + self.mode = mode + self.guid = guid + self.node = node + self.item = item + self.edit = edit + + if func == 'node': + return self.menunode + + if (self.item is None or not self.meta) and func in [None, 'list']: # If no item is specified then we show the whole directory + blank = True if not self.menunode and self.edit else False # If we're in edit mode and have no items show a blank one + items = self.get_directory_items(blank=blank, filters=get_filters(**kwargs)) + if func == 'list': + return items + if not items and self.edit: # If we didn't get any items from filter and we're in edit mode then get blank + items = self.get_directory_items(blank=True) + return self.add_items(items, update_listing=self.update_listing) + + item_func = getattr(self, func) + + if not self.meta and self.filepath: + self._meta = [get_default_item()] # Create a blank item in meta to write to if we're trying to do a function on it. + + path_partitions = [i.partition('::') if i else ('', '', '', ) for i in self.params.get('paths', [])] + path_args = [k for k, s, v in path_partitions if not s] + path_kwargs = {k: v for k, s, v in path_partitions if s} + item_func(*path_args, **path_kwargs) # If an item is specified we do its function diff --git a/script.skinvariables/resources/lib/shortcuts/skinshortcuts.py b/script.skinvariables/resources/lib/shortcuts/skinshortcuts.py new file mode 100644 index 0000000000..d8955f8b7d --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/skinshortcuts.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import re +import xbmc +import xbmcgui +import jurialmunkey.futils +import xml.etree.ElementTree as ET +from json import loads +from jurialmunkey.futils import get_files_in_folder, load_filecontent, write_file +from resources.lib.kodiutils import get_localized + +ADDONDATA = 'special://profile/addon_data/script.skinvariables/' +TAB = ' ' +DATA_FOLDER = 'special://profile/addon_data/script.skinshortcuts/' +SKIN_FOLDER = 'special://skin/shortcuts/' + + +class FileUtils(jurialmunkey.futils.FileUtils): + addondata = ADDONDATA # Override module addon_data with plugin addon_data + + +FILEUTILS = FileUtils() +delete_file = FILEUTILS.delete_file + + +get_infolabel = xbmc.getInfoLabel + + +class SkinShortcutsMethodsJSON(): + pass + + +class SkinShortcutsMethodsXML(): + def write_shortcut(self, name): + shortcuts_content = [] + for shortcut in self.meta[name]: + shortcut_content = '\n'.join([f'{TAB}{TAB}<{tag_name}>{tag_text}' for tag_name, tag_text in shortcut.items()]) + shortcut_content = f'{TAB}\n{shortcut_content}\n{TAB}' + shortcut_content = shortcut_content.replace('&', '&') + shortcuts_content.append(shortcut_content) + shortcuts_content = '\n'.join(shortcuts_content) + content = f'\n{shortcuts_content}\n' + filepath = f'{DATA_FOLDER}{self.skin}-{name}.DATA.xml' + write_file(filepath=filepath, content=content) + delete_file(folder=DATA_FOLDER, filename=f'{self.skin}.hash', join_addon_data=False) + + def mod_skinshortcut(self): + name = self.get_menu_name(self.params.get('name'), heading=get_localized(32016)) + if not name: + return + if name[-2:-1] == '-': + name = name[:-2] + '.' + name[-1:] + xbmc.executebuiltin(f'RunScript(script.skinshortcuts,type=manage&group={name})') + return name + + def del_skinshortcut(self): + name = self.get_menu_name(self.params.get('name'), heading=get_localized(32017)) + if not name: + return + try: + x = int(self.params.get('index')) - 1 + except (ValueError, TypeError): + files = [self.get_nice_name(i.get('label')) for i in self.meta[name]] + x = xbmcgui.Dialog().select(get_localized(117), files) + if x == -1: + return + self.meta[name].pop(x) + self.write_shortcut(name) + return name + + def add_skinshortcut(self): + action = '' + + def _get_infolabel(infolabel): + if self.params.get('use_listitem'): + return get_infolabel(infolabel) or '' + return '' + + if self.params.get('path') or self.params.get('use_listitem'): + window = self.params.get('window') or 'videos' + folder = self.params.get('path') or _get_infolabel('Container.ListItem.FolderPath') + action = f"ActivateWindow({window},{folder},return)" + + item = self.config_id({ + 'label': self.params.get('label') or _get_infolabel('Container.ListItem.Label'), + 'label2': self.params.get('label2') or _get_infolabel('Container.ListItem.Label2'), + 'icon': self.params.get('icon') or _get_infolabel('Container.ListItem.Icon'), + 'thumb': self.params.get('thumb') or '', + 'action': action + }) + + name, nice_name = self.choose_menu(get_localized(32021)) + if not name: + return + self.meta[name].append(item) + self.write_shortcut(name) + + xbmcgui.Dialog().ok(get_localized(32020), get_localized(32018).format(item.get('label') or '', nice_name)) + return name + + def imp_skinshortcut(self): + files = [i for i in get_files_in_folder(DATA_FOLDER, r'.*?-(.*)\.DATA\.xml')] + if not files: + xbmcgui.Dialog().ok(get_localized(32019), get_localized(32022)) + return + x = xbmcgui.Dialog().select(get_localized(32019), files) + if x == -1: + return + + name, nice_name = self.choose_menu(get_localized(32023)) + if not name: + return + self.meta[name] = self.load_skinshortcut(f'{DATA_FOLDER}{files[x]}') + self.write_shortcut(name) + + xbmcgui.Dialog().ok(get_localized(32024), get_localized(32025).format(files[x], nice_name)) + return name + + def mov_skinshortcut(self): + regex = r'(.*)\.DATA\.xml' + folder = self.params['folder'] + + if not xbmcgui.Dialog().yesno(get_localized(32026), get_localized(32027), yeslabel=get_localized(186), nolabel=get_localized(222)): + return + + for file in get_files_in_folder(folder, regex): + name = re.search(regex, file).group(1) + self.meta[name] = self.load_skinshortcut(f'{folder}{file}') + self.write_shortcut(name) + + xbmcgui.Dialog().ok(get_localized(32019), get_localized(32028).format(folder, self.skin)) + return name + + +class SkinShortcutsMenu(): + def __init__(self, skin, **kwargs): + self.skin = skin + self.params = kwargs + self.folders = [ + (SKIN_FOLDER, r'(.*)\.DATA\.xml'), + (DATA_FOLDER, fr'{self.skin}-(.*)\.DATA\.xml')] + self.meta = self.read_skinshortcuts(self.folders) + self.config = self.read_config() + + def read_config(self): + content = load_filecontent('special://skin/shortcuts/skinvariables-skinshortcuts.json') + if not content: + return {} + config = loads(content) or {} + levels = config.get('mainmenu', {}).get('levels') or [{}] + + mainmenu = self.meta.setdefault('mainmenu', []) + for i in mainmenu: + default_id = i.get('defaultID') + for level in levels: + affix = level.get('affix') or '' + level_default_id = f'{default_id}{affix}' + self.meta.setdefault(level_default_id, []) + config.setdefault(level_default_id, {k: v for k, v in level.items()}) + if i.get('label') and not i['label'].startswith('$SKIN'): + config[level_default_id]['name'] = i['label'] + + return config + + @staticmethod + def load_skinshortcut_file(filename): + xmlstring = load_filecontent(filename) + if not xmlstring: + return [] + return [{i.tag: i.text for i in shortcut} for shortcut in ET.fromstring(xmlstring)] + + def load_skinshortcut(self, filename, configure_ids=True): + meta = self.load_skinshortcut_file(filename) + if not configure_ids: + return meta + return self.configure_ids(meta) + + def read_skinshortcuts(self, folders): + meta = {} + for folder, regex in folders: + for file in get_files_in_folder(folder, regex): + name = re.search(regex, file).group(1) + meta[name] = self.load_skinshortcut(f'{folder}{file}') + return meta + + def configure_ids(self, meta): + return [self.config_id(item) for item in meta] + + @staticmethod + def config_id(item): + if item.get('defaultID'): + return item + label_id = item.get('labelID') or re.sub('[^0-9a-zA-Z]+', '', item.get('label') or '') + item['defaultID'] = item['labelID'] = label_id.lower() + return item + + def get_index(self, label): + if label not in self.config: + return '' + if 'index' not in self.config[label]: + return + return str(self.config[label]['index'] or '') + + def get_nice_name(self, label): + prefix, suffix, affix = '', '', '' + + if label in self.config: + affix = self.config[label].get('affix') or '' + suffix = self.config[label].get('suffix') or '' + prefix = self.config[label].get('prefix') or '' + label = self.config[label].get('name') or label + + if affix and label.endswith(affix): + label = label[:-len(affix)] + + monitor = xbmc.Monitor() + + while not monitor.abortRequested(): + result = re.search(r'.*\$LOCALIZE\[(.*?)\].*', label) + if not result: + break + try: + localized = xbmc.getLocalizedString(int(result.group(1))) or '' + except ValueError: + localized = '' + label = label.replace(result.group(0), localized) + + while not monitor.abortRequested(): + result = re.search(r'.*\$INFO\[(.*?)\].*', label) + if not result: + break + localized = get_infolabel(result.group(1)) or '' + label = label.replace(result.group(0), localized) + + try: + label = xbmc.getLocalizedString(int(label)) or label + except ValueError: + pass + + label = f'{prefix}{label}{suffix}' + + return label + + def choose_menu(self, header, names=None): + names = names if names else self.meta.keys() + regex = self.params.get('label_regex') + files = [(self.get_nice_name(i), i, self.get_index(i), ) for i in names if not regex or re.search(regex, self.get_nice_name(i))] + files = sorted(files, key=lambda a: f'{a[2] or ""}{a[0]}') + x = xbmcgui.Dialog().select(header, [i[0] for i in files]) + if x == -1: + return (None, '') + choice = [i for i in files][x] + return (choice[1], choice[0]) + + def get_menu_name(self, name=None, heading=get_localized(32029)): + if not name: + return + name = [i[4:] if i.startswith('num-') else i for i in name.split('||')] + menu = [k for k in self.meta.keys() if any(re.match(i, k) for i in name)] + if len(menu) == 1: + return menu[0] + if len(menu) > 1: + return self.choose_menu(heading, menu)[0] + return self.choose_menu(heading)[0] + + def run(self, action): + route = getattr(self, action) + + try: + success = route() + except KeyError: + success = False + + if not success: + return + + if self.params.get('executebuiltin'): + xbmc.executebuiltin(self.params['executebuiltin']) + + +class SkinShortcutsXML(SkinShortcutsMenu, SkinShortcutsMethodsXML): + pass + + +class SkinShortcutsJSON(SkinShortcutsMenu, SkinShortcutsMethodsJSON): + pass + + +def get_skinshortcuts_menu(route, mode='xml', **kwargs): + factory = { + 'xml': SkinShortcutsXML, + 'json': SkinShortcutsJSON + } + factory[mode](**kwargs).run(route) diff --git a/script.skinvariables/resources/lib/shortcuts/template.py b/script.skinvariables/resources/lib/shortcuts/template.py new file mode 100644 index 0000000000..02105b2462 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/template.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import re +import xbmc +import xbmcaddon +from json import loads +from jurialmunkey.logger import TimerFunc +from jurialmunkey.parser import parse_math, boolean, parse_paramstring +from jurialmunkey.futils import load_filecontent, write_skinfile, make_hash +from resources.lib.kodiutils import ProgressDialog, get_localized +from resources.lib.operations import RuleOperations, check_condition +from resources.lib.shortcuts.node import ListGetShortcutsNode, get_menunode_lookup +from resources.lib.shortcuts.xmltojson import xml_to_json +from xml.dom import minidom +from copy import deepcopy + + +ADDON = xbmcaddon.Addon() + +SKIN_BASEDIR = 'special://skin' +SHORTCUTS_FOLDER = 'shortcuts' + + +def escape_ampersands(myxml): + regex = re.compile(r"&(?!amp;|lt;|gt;)") + return regex.sub("&", myxml) + + +def pretty_xmlcontent(myxml): + myxml = minidom.parseString(myxml) + return '\n'.join([line for line in myxml.toprettyxml(indent=' ' * 4).split('\n') if line.strip()]) + + +class FormatDict(dict): + def __missing__(self, key): + if key.endswith('_escaped'): + return self[key[:-8]] + return '' + + +class TemplatePart(): + def __init__(self, parent, genxml, **kwargs): + self.skinid = parent.skinid + self.genxml = deepcopy(genxml) + self.params = FormatDict(kwargs) + self.stored = parent.stored if hasattr(parent, 'stored') else {} + + @property + def is_condition(self): + try: + return self._is_condition + except AttributeError: + self._is_condition = self.parse_condition(self.genxml.pop('condition', [])) + return self._is_condition + + def parse_condition(self, conditions): + conditions = conditions if isinstance(conditions, list) else [conditions] + return all([check_condition(self.get_formatted(condition)) for condition in conditions]) + + def parse_lookup(self, string): + """ $LOOKUP[lookup_key?menu=sidemenu&filter_guid=xyz] """ + LOOKUP_REGEX = r'\$LOOKUP\[(.*?)\]' + match = re.search(LOOKUP_REGEX, string) + if not match: + return string + lookup, paramstring = match.group(1).split('?', 1) + params = parse_paramstring(paramstring) + output = get_menunode_lookup(lookup, skin=self.skinid, **params) + string = string.replace(match.group(0), output) + return self.parse_lookup(string) + + def get_formatted(self, string, params=None): + string = string.format_map(params or self.params) + string = parse_math(string) + string = self.parse_lookup(string) + return string + + def get_conditional_value(self, items): + for i in items: + if isinstance(i, str): + return self.get_formatted(i) + if self.parse_condition(i.get('condition', 'true')): + return self.get_formatted(i['value'] or '') + return '' + + def update_params(self): + for k, v in self.genxml.items(): + if isinstance(v, dict): + self.params[k] = '\n'.join(self.get_contents(v, self.params)) + continue + if isinstance(v, list): + self.params[k] = self.get_conditional_value(v) + continue + self.params[k] = self.get_formatted(v) + return self.params + + def get_menunode(self): + for_each = self.genxml.pop("for_each") + menu = self.get_formatted(self.genxml.pop("menu", '')) + item = self.get_formatted(self.genxml.pop("item", '')) + mode = self.get_formatted(self.genxml.pop("mode", '')) + + contents = [] + + node_obj = ListGetShortcutsNode(None, None) + node_obj.refresh = True # Refresh mem cache because we want to build from the file + nodelist = node_obj.get_directory(menu=menu, skin=self.skinid, node=item, mode=mode, func='node') or [] + + for item_x, item_i in enumerate(nodelist): + item_i = {'value': item_i} if isinstance(item_i, str) else item_i # In case of actions list we only have strings so massage to dictionary + item_i.pop('submenu', []) + item_i.pop('widgets', []) + for action_x, action_i in enumerate(for_each): + item_d = deepcopy(self.params) # Inherit parent values + item_d.update({f'parent_{k}': v for k, v in item_d.items()}) # Update with item values + item_d.update({f'item_{k}': v for k, v in item_i.items()}) # Update with item values + item_d['item_x'] = item_x # Add item index + item_d['item_action_x'] = action_x # Add item index + item_d['item_length_x'] = len(nodelist) # Add length of nodelist that current item is in + item_d['item_menu'] = menu # Add item menu + item_d['item_node'] = item # Add item menu + item_d['item_mode'] = mode # Add item menu + contents += self.get_contents(action_i, item_d) + return contents + + def get_itemlist(self): + contents = [] + itemlist = self.genxml.pop("list") + for_each = self.genxml.pop("for_each") + for item, defs in itemlist: + item_d = deepcopy(self.params) # Inherit parent values + item_d.update(defs) # Add in any specific values for item + item_d.update({f'parent_{k}': v for k, v in item_d.items()}) # Update with item values + item_d['item'] = item # Add item menu + for action in for_each: + contents += self.get_contents(action, item_d) + return contents + + def get_template(self): # _make_template + filelist = self.genxml.pop("template") + filelist = filelist if isinstance(filelist, list) else [filelist] + contents = [] + fmt_dict = self.update_params() + for template in filelist: + file = load_filecontent(f'{SKIN_BASEDIR}/{SHORTCUTS_FOLDER}/{template}') if template.endswith('.xmltemplate') else template + item = self.get_formatted(file, fmt_dict) + contents.append(item) + return ['\n'.join(contents)] + + def add_datafile(self): + filelist = self.genxml.pop("datafile") + filelist = filelist if isinstance(filelist, list) else [filelist] + contents = {} + for datafile in filelist: + file = load_filecontent(f'{SKIN_BASEDIR}/{SHORTCUTS_FOLDER}/{datafile}') + func = xml_to_json if datafile.endswith('.xml') else loads + meta = func(file) if file else {} + contents.update(meta) + contents.update(self.genxml) + self.genxml = contents + return self.genxml + + def get_enumitem(self): + enumitem = self.genxml.pop("enumitem") + for k, v in enumitem.items(): + name = self.get_formatted(v) + enum = self.stored.setdefault(name, 0) + 1 + self.stored[name] = enum + self.genxml[k] = f'{enum}' + self.params[k] = f'{enum}' + return self.genxml + + def get_for_each(self): + if 'list' in self.genxml: + return self.get_itemlist() + return self.get_menunode() + + def get_contents(self, genxml, params): + params = params or {} + return TemplatePart(self, genxml, **params).get_content() + + def get_content(self): # _make_contents + if 'datafile' in self.genxml: + self.add_datafile() + if not self.is_condition: + return [] + if 'enumitem' in self.genxml: + self.get_enumitem() + if 'template' in self.genxml: + return self.get_template() + if 'for_each' in self.genxml: + return self.get_for_each() + return [] + + +class ShortcutsTemplate(object): + allow_users = True + + def __init__(self, template: str = None): + self.template = f'skinvariables-generator-{template}' if template else 'skinvariables-generator' + self.hashname = f'script-{self.template}{self.skinuser}-hash' + self.contents = load_filecontent(f'{SKIN_BASEDIR}/{SHORTCUTS_FOLDER}/{self.template}.json') + self.meta = loads(self.contents) or {} + self.folder = self.meta.get('folder') or SHORTCUTS_FOLDER + self.p_dialog = None + + @property + def skinuser(self): + try: + return self._skinuser + except AttributeError: + return self.get_skinuser() + + def get_skinuser(self): + self._skinuser = '' if not self.allow_users else xbmc.getInfoLabel("Skin.String(SkinVariables.SkinUser)") or '' + return self._skinuser + + @property + def filepath(self): + try: + return self._filepath + except AttributeError: + return self.get_filepath() + + def get_filepath(self): + self._filepath = f'{SKIN_BASEDIR}/{self.folder}/{self.filename}' + return self._filepath + + @property + def filename(self): + try: + return self._filename + except AttributeError: + return self.get_filename() + + def get_filename(self): + self._filename = self.meta['output'].format(skinuser=self.skinuser) + return self._filename + + @property + def skinid(self): + try: + return self._skinid + except AttributeError: + return self.get_skinid() + + def get_skinid(self): + self._skinid = self.meta.get('skinid') + if not self._skinid or not self.skinuser: + return self._skinid + self._skinid = f'{self._skinid}-{self.skinuser}' + return self._skinid + + def create_xml(self): + self.p_dialog.update(message=f'{get_localized(32046)}...') # Generating globals + + pre_generated_nfo = {**self.meta['getnfo']} + pre_generated_nfo.update({ + k: TemplatePart(self, v, **self.meta['getnfo']).get_template()[0] + for k, v in self.meta.get('global', {}).items()}) + + self.p_dialog.update(message=f'{get_localized(32047)}...') # Generating content + + content = [] + + if 'header' in self.meta: + content += [self.meta['header']] + + content += [j for i in self.meta['genxml'] for j in TemplatePart(self, i, **pre_generated_nfo).get_content()] + + if 'footer' in self.meta: + content += [self.meta['footer']] + + self.p_dialog.update(message=f'{get_localized(32048)}...') # Formatting content + + content = '\n'.join(content) + content = escape_ampersands(content) + content = pretty_xmlcontent(content) + return content + + def update_xml(self, force=False, no_reload=False, genxml='', background=True, **kwargs): + if not self.meta: + return + + hashinput = '_'.join([ + '_'.join([f'{k}.{v}' for k, v in kwargs.items()]), + f'{genxml}', + f'{self.contents}', + xbmc.getInfoLabel("System.ProfileName") + ]) + + def get_hashvalue(): + return make_hash(f'{hashinput}--{load_filecontent(self.filepath)}') + + hashvalue = get_hashvalue() + + def is_updated(): + if force: + return + if not hashvalue: + return + last_version = xbmc.getInfoLabel(f'Skin.String({self.hashname})') + if not last_version: + return + if last_version != hashvalue: + return + return True + + if is_updated(): + return + + with TimerFunc('script.skinvariables - update_xml: ', log_threshold=0.001, inline=True): + with ProgressDialog( + ADDON.getLocalizedString(32001), + f'{get_localized(32049)}...', + logging=2, total=4, background=boolean(background) + ) as self.p_dialog: + self.meta['genxml'] += [{k: v for j in i.split('|') for k, v in (j.split('='), )} for i in genxml.split('||')] if genxml else [] + self.meta['getnfo'] = {k: xbmc.getInfoLabel(v) for k, v in self.meta['getnfo'].items()} if 'getnfo' in self.meta else {} + self.meta['getnfo'].update(kwargs) + self.meta['getnfo'].update(RuleOperations(self.meta['addnfo'], **self.meta['getnfo']).params) if 'addnfo' in self.meta else {} + write_skinfile(folders=[self.folder], filename=self.filename, content=self.create_xml(), hashvalue=hashvalue, hashname=self.hashname) + + if no_reload: + return + + xbmc.Monitor().waitForAbort(0.5) + xbmc.executebuiltin('Skin.SetString({},{})'.format(self.hashname, get_hashvalue())) # Update hashvalue with new content to avoid loop + xbmc.executebuiltin('ReloadSkin()') diff --git a/script.skinvariables/resources/lib/shortcuts/xmltojson.py b/script.skinvariables/resources/lib/shortcuts/xmltojson.py new file mode 100644 index 0000000000..cc8e0aaaa1 --- /dev/null +++ b/script.skinvariables/resources/lib/shortcuts/xmltojson.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import xml.etree.ElementTree as ET + + +""" +Module for converting xml based template config code into json +""" + + +class Meta(): + def __init__(self, root, meta): + self.root = root + self.meta = meta + + def set_listtext(self, tag, key=None): + """ + XML: + D1 + D2 + C1 + C2 + + JSON: + { + "datafile": [ + "D1", + "D2" + ], + "condition": [ + "C1", + "C2" + ] + } + """ + value = [i.text for i in self.root.findall(tag)] + if not value: + return + self.meta[key or tag] = value + return value + + def set_dicttext(self, tag, key=None): + """ + XML: + V1 + V2 + + JSON: + { + "key or tag": { + "K1": "V1", + "K2": "V2" + } + } + """ + value = {} + for i in self.root.findall(tag): + k = i.attrib['name'] + v = i.text + value[k] = v + if not value: + return + self.meta[key or tag] = value + return value + + def set_itemtext(self, tag, key=None): + """ + XML: + + + JSON: + { + "template": "T1" + } + """ + value = next((i.text for i in self.root.findall(tag) if i.text), None) + if not value: + return + self.meta[key or tag] = value + return value + + def set_value(self, root): + """ + XML: + + C1 + + + JSON: + { + "N1": { + C1 + } + } + """ + items = [] + name = root.attrib['name'] if 'name' in root.attrib else 'value' + if not list(root): + self.meta[name] = root.text + return items + items.append(Meta(root, self.meta.setdefault(name, {}))) + return items + + def set_rules(self, root): + """ + XML: + + + C1 + V1 + + + C2 + V2 + + + + JSON: + { + "N1": [ + { + "condition": "C1", + "value": "V1" + }, + { + "condition": "C2", + "value": "V2" + } + ] + } + """ + items = [] + name = root.attrib['name'] + self.meta[name] = [] + for item in root.findall('rule'): + meta = {} + self.meta[name].append(meta) + items.append(Meta(item, meta)) + return items + + def set_items(self, root): + """ + XML: + + + C1 + + + C2 + + + + JSON: + { + "node": "N1", + "mode": "M1", + "item": "I1", + "for_each" [ + { + C1 + }, + { + C2 + } + ] + } + """ + items = [] + + for k, v in root.attrib.items(): + self.meta[k] = v + + self.meta['for_each'] = [] + for item in root.findall('item'): + meta = {} + self.meta['for_each'].append(meta) + items.append(Meta(item, meta)) + return items + + def set_lists(self, root): + """ + XML: + + + V1 + V2 + + + V3 + V4 + + + + JSON: + { + "list": [ + ["N1", {"K1": "V1", "K2": "V2"}], + ["N2", {"K3": "V3", "K4": "V4"}] + ] + } + """ + items = [] + self.meta['list'] = [] + for item in root.findall('list'): + meta = {} + pair = [item.attrib['name'], meta] + self.meta['list'].append(pair) + items.append(Meta(item, meta)) + if not items: + del self.meta['list'] + return [] + return items + + +class XMLtoJSON(): + + routes = { + 'value': 'set_value', + 'items': 'set_items', + 'rules': 'set_rules', + 'lists': 'set_lists' + } + + def __init__(self, filecontent): + self.root = ET.fromstring(filecontent) + self.meta = {} + + def get_meta(self): + self.get_contents(Meta(self.root, self.meta)) + return self.meta + + def get_contents(self, meta): + meta.set_listtext('condition') + meta.set_itemtext('template') + meta.set_listtext('datafile') + meta.set_dicttext('enumitem') + + for i in meta.root: + func = self.routes.get(i.tag) + if not func: + continue + func = getattr(meta, func) + for j in func(i): + self.get_contents(j) + + +def xml_to_json(filecontent): + return XMLtoJSON(filecontent).get_meta() diff --git a/script.skinvariables/resources/lib/skinvariables.py b/script.skinvariables/resources/lib/skinvariables.py new file mode 100644 index 0000000000..33d9287219 --- /dev/null +++ b/script.skinvariables/resources/lib/skinvariables.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import xbmc +import xbmcgui +import xbmcaddon +from json import loads, dumps +import xml.etree.ElementTree as ET +from jurialmunkey.parser import try_int, del_empty_keys +from resources.lib.xmlhelper import make_xml_includes, get_skinfolders +from jurialmunkey.futils import load_filecontent, write_skinfile, make_hash + +ADDON = xbmcaddon.Addon() + + +class SkinVariables(object): + def __init__(self, template: str = None, skinfolder: str = None): + self.template = f"skinvariables-{template}" if template else 'skinvariables' + self.filename = f'script-{self.template}-includes.xml' + self.hashname = f'script-{self.template}-hash' + self.folders = [skinfolder] if skinfolder else get_skinfolders() + self.content = self.build_json(f'special://skin/shortcuts/{self.template}.xml') + self.content = self.content or load_filecontent(f'special://skin/shortcuts/{self.template}.json') + self.meta = loads(self.content) or [] + + def build_json(self, file): + xmlstring = load_filecontent(file) + if not xmlstring: + return + + json = [] + for variable in ET.fromstring(xmlstring): + if not variable.attrib.get('name'): + continue # No name specified so skip + if variable.tag not in ['expression', 'variable']: + continue # Not an expression or variable so skip + + item = {} + + if variable.tag == 'expression' and variable.text: + item['expression'] = variable.text + elif variable.tag == 'variable': + item['values'] = [{i.attrib.get('condition') or 'True': i.text} for i in variable] + + if not item.get('expression') and not item.get('values'): + continue # No values or expression so skip + + item['name'] = variable.attrib.get('name') + item['containers'] = [ + j for i in variable.attrib.get('containers', '').split(',') for j + in (range(*(int(y) + x for x, y, in enumerate(i.split('...')))) if '...' in i else (int(i),))] + item['listitems'] = {} + item['listitems']['start'] = try_int(variable.attrib.get('start')) + item['listitems']['end'] = try_int(variable.attrib.get('end')) + item['types'] = variable.attrib['types'].split(',') if variable.attrib.get('types') else ['listitem'] + item['parent'] = variable.attrib.get('parent') + item['null_id'] = variable.attrib.get('null_id') + + json.append(del_empty_keys(item)) + + return dumps(json) + + def build_containers(self, variable={}): + containers = variable.get('containers', []) + containers.append('') + return containers + + def build_listitems(self, variable={}): + li_a = variable.get('listitems', {}).get('start', 0) + li_z = variable.get('listitems', {}).get('end') + listitems = [i for i in range(li_a, int(li_z) + 1)] if li_z else [] + listitems.append('') + return listitems + + def get_contentvalues(self, values, f_dict): + content = [] + for value in values: + build_var = {} + build_var['tag'] = 'value' + build_var['attrib'] = {} + for k, v in value.items(): + if not k: + continue + build_var['attrib']['condition'] = k.format(**f_dict) + build_var['content'] = v.format(**f_dict) if v else '' + content.append(build_var) + return content + + def get_skinvariable(self, variable, expression=False): + if not variable: + return + + var_name = variable.get('name') + + if not var_name: + return + + containers = self.build_containers(variable) + listitems = self.build_listitems(variable) + values = variable.get('values', []) + listitem_types = variable.get('types') or ['listitem'] + skin_vars = [] + + listitem_type_tags = { + 'listitem': '', + 'listitemabsolute': '_LIA', + 'listitemnowrap': '_LIN', + 'listitemposition': '_LIP', + } + + def _build_var(container=None, listitem=None, listitem_type='listitem'): + build_var = { + 'tag': 'expression' if expression else 'variable', + 'attrib': {}, + 'content': [] + } + + li_name = 'ListItem' + tag_name = var_name + _lid = '' + _cid = '' + + tag_name += listitem_type_tags[listitem_type] + + if container == -1: # Special value for building container without ID + tag_name += '_Container' + li_name = 'Container.ListItem' + container = '' # Blank out container ID + + if container: + tag_name += '_C{}'.format(container) + li_name = 'Container({}).ListItem'.format(container) + _cid = '_C{}'.format(container) + + if listitem or listitem == 0: + tag_name += '_{}'.format(listitem) + li_name += '({})'.format(listitem) + _lid = '_{}'.format(listitem) + + build_var['attrib']['name'] = tag_name + + f_dict = { + 'id': container or '', + 'cid': _cid, + 'lid': _lid, + 'pos': listitem or 0, + 'listitem': li_name, + 'listitemabsolute': li_name.replace('ListItem(', 'ListItemAbsolute('), + 'listitemnowrap': li_name.replace('ListItem(', 'ListItemNoWrap('), + 'listitemposition': li_name.replace('ListItem(', 'ListItemPosition(') + } + + f_dict['listitem'] = f_dict[listitem_type] + + if expression: + build_var['content'] = variable.get('expression', '').format(**f_dict) + return build_var + + build_var['content'] = self.get_contentvalues(values, f_dict) + return build_var + + for lit in listitem_types: + for container in containers: + # Build Variables for each ListItem Position in Container + for listitem in listitems: + skin_vars.append(_build_var(container, listitem, lit)) + + if variable.get('null_id', '').lower() == 'true': + # Build a Container.ListItem variable without an id + for listitem in listitems: + skin_vars.append(_build_var(-1, listitem, lit)) + + def _build_parent_var(listitem_type='listitem'): + + parent_var_name = var_name + listitem_type_tags[listitem_type] + + build_var = { + 'tag': 'variable', + 'attrib': {'name': parent_var_name + '_Parent'}, + 'content': [] + } + + content = [] + + for container in containers: + cond = 'True' + valu = parent_var_name + if container: + valu += '_C{}'.format(container) + cond = variable['parent'].format(**{'id': container or ''}) + valu = '$VAR[{}]'.format(valu) + content.append({'tag': 'value', 'attrib': {'condition': cond}, 'content': valu}) + + build_var['content'] = content + return build_var + + # Build variable for parent containers + for lit in listitem_types: + if variable.get('parent'): + skin_vars.append(_build_parent_var(lit)) + + return skin_vars + + def update_xml(self, force=False, no_reload=False, **kwargs): + if not self.meta: + return + + hashvalue = make_hash(self.content) + + if not force: # Allow overriding over built check + last_version = xbmc.getInfoLabel(f'Skin.String({self.hashname})') + if hashvalue and last_version and hashvalue == last_version: + return # Already updated + + p_dialog = xbmcgui.DialogProgressBG() + p_dialog.create(ADDON.getLocalizedString(32001), ADDON.getLocalizedString(32000)) + + xmltree = [] + for i in self.meta: + item = None + if i.get('values'): + item = self.get_skinvariable(i) + elif i.get('expression'): + item = self.get_skinvariable(i, expression=True) + xmltree = xmltree + item if item else xmltree + + # Save to folder + if self.folders: + write_skinfile( + folders=self.folders, filename=self.filename, + content=make_xml_includes(xmltree, p_dialog=p_dialog), + hashvalue=hashvalue, hashname=self.hashname) + + p_dialog.close() + xbmc.executebuiltin('ReloadSkin()') if not no_reload else None diff --git a/script.skinvariables/resources/lib/viewtypes.py b/script.skinvariables/resources/lib/viewtypes.py new file mode 100644 index 0000000000..a263a3be18 --- /dev/null +++ b/script.skinvariables/resources/lib/viewtypes.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import xbmc +import xbmcgui +import xbmcvfs +import xbmcaddon +from json import loads, dumps +from jurialmunkey.parser import try_int +from jurialmunkey.futils import check_hash, make_hash, write_skinfile, write_file, load_filecontent +from jurialmunkey.jsnrpc import get_jsonrpc + + +ADDON = xbmcaddon.Addon() +ADDON_DATA = 'special://profile/addon_data/script.skinvariables/' + + +def join_conditions(org='', new='', operator=' | '): + return '{}{}{}'.format(org, operator, new) if org else new + + +def _get_localized(text): + if text.startswith('$LOCALIZE'): + text = text.strip('$LOCALIZE[]') + if try_int(text): + text = xbmc.getLocalizedString(try_int(text)) + return text + + +class ViewTypes(object): + def __init__(self): + if not xbmcvfs.exists(ADDON_DATA): + xbmcvfs.mkdir(ADDON_DATA) + + @property + def content(self): + try: + return self._content + except AttributeError: + self._content = load_filecontent('special://skin/shortcuts/skinviewtypes.json') + return self._content + + @property + def meta(self): + try: + return self._meta + except AttributeError: + self._meta = loads(self.content) or {} + return self._meta + + @property + def addon_datafile(self): + try: + return self._addon_datafile + except AttributeError: + self._addon_datafile = f'{ADDON_DATA}{xbmc.getSkinDir()}-viewtypes.json' + return self._addon_datafile + + @property + def addon_content(self): + try: + return self._addon_content + except AttributeError: + self._addon_content = load_filecontent(self.addon_datafile) + return self._addon_content + + @property + def addon_meta(self): + try: + return self._addon_meta + except AttributeError: + if not self.addon_content: + self._addon_meta = {} + return self._addon_meta + self._addon_meta = loads(self.addon_content) or {} + return self._addon_meta + + @addon_meta.setter + def addon_meta(self, value): + self._addon_meta = value + + @property + def prefix(self): + try: + return self._prefix + except AttributeError: + self._prefix = self.meta.get('prefix', 'Exp_View') + '_' + return self._prefix + + @property + def skinfolders(self): + try: + return self._skinfolders + except AttributeError: + from resources.lib.xmlhelper import get_skinfolders + self._skinfolders = get_skinfolders() + return self._skinfolders + + @property + def icons(self): + try: + return self._icons + except AttributeError: + self._icons = self.meta.get('icons') or {} + return self._icons + + def make_defaultjson(self, overwrite=False): + p_dialog = xbmcgui.DialogProgressBG() + p_dialog.create(ADDON.getLocalizedString(32002), ADDON.getLocalizedString(32003)) + p_total = len(self.meta.get('rules', {})) + + addon_meta = {'library': {}, 'plugins': {}} + for p_count, (k, v) in enumerate(self.meta.get('rules', {}).items()): + p_dialog.update((p_count * 100) // p_total, message=u'{} {}'.format(ADDON.getLocalizedString(32005), k)) + # TODO: Add checks that file is properly configured and warn user otherwise + addon_meta['library'][k] = v.get('library') + addon_meta['plugins'][k] = v.get('plugins') or v.get('library') + if overwrite: + write_file(filepath=self.addon_datafile, content=dumps(addon_meta)) + + p_dialog.close() + return addon_meta + + def make_xmltree(self): + """ + Build the default viewtype expressions based on json file + """ + xmltree = [] + expressions = {} + viewtypes = {} + + p_dialog = xbmcgui.DialogProgressBG() + p_dialog.create(ADDON.getLocalizedString(32002), ADDON.getLocalizedString(32003)) + + for v in self.meta.get('viewtypes', {}): + expressions[v] = '' # Construct our expressions dictionary + viewtypes[v] = {} # Construct our viewtypes dictionary + + # Build the definitions for each viewid + p_dialog.update(25, message=ADDON.getLocalizedString(32006)) + for base_k, base_v in self.addon_meta.items(): + for contentid, viewid in base_v.items(): + if base_k == 'library': + viewtypes[viewid].setdefault(contentid, {}).setdefault('library', True) + continue + if base_k == 'plugins': + viewtypes[viewid].setdefault(contentid, {}).setdefault('plugins', True) + continue + for i in viewtypes: + listtype = 'whitelist' if i == viewid else 'blacklist' + viewtypes[i].setdefault(contentid, {}).setdefault(listtype, []) + viewtypes[i][contentid][listtype].append(base_k) + + # Build the visibility expression + p_dialog.update(50, message=ADDON.getLocalizedString(32007)) + for viewid, base_v in viewtypes.items(): + for contentid, child_v in base_v.items(): + rule = self.meta.get('rules', {}).get(contentid, {}).get('rule') # Container.Content() + + whitelist = '' + if child_v.get('library'): + whitelist = 'String.IsEmpty(Container.PluginName)' + for i in child_v.get('whitelist', []): + whitelist = join_conditions(whitelist, 'String.IsEqual(Container.PluginName,{})'.format(i)) + + blacklist = '' + if child_v.get('plugins'): + blacklist = '!String.IsEmpty(Container.PluginName)' + for i in child_v.get('blacklist', []): + blacklist = join_conditions(blacklist, '!String.IsEqual(Container.PluginName,{})'.format(i), operator=' + ') + + affix = '[{}] | [{}]'.format(whitelist, blacklist) if whitelist and blacklist else whitelist or blacklist + + if affix: + expression = '[{} + [{}]]'.format(rule, affix) + expressions[viewid] = join_conditions(expressions.get(viewid), expression) + + # Build conditional rules for disabling view lock + if self.meta.get('condition'): + sep = ' | ' + for viewid in self.meta.get('viewtypes', {}): + rule = ['[{}]'.format(v.get('rule')) for k, v in self.meta.get('rules', {}).items() if viewid in v.get('viewtypes', [])] + rule_cond = '![{}] + [{}]'.format(self.meta.get('condition'), sep.join(rule)) + rule_expr = '[{}] + [{}]'.format(self.meta.get('condition'), expressions.get(viewid)) + expressions[viewid] = '[{}] | [{}]'.format(rule_expr, rule_cond) + + # Build XMLTree + p_dialog.update(75, message=ADDON.getLocalizedString(32008)) + for exp_name, exp_content in expressions.items(): + exp_include = 'True' if exp_content else 'False' + exp_content = exp_content.replace('[]', '[False]') if exp_content else 'False' # Replace None conditions with explicit False because Kodi complains about empty visibility conditions + exp_content = '[{}]'.format(exp_content) + xmltree.append({ + 'tag': 'expression', + 'attrib': {'name': self.prefix + exp_name}, + 'content': exp_content}) + xmltree.append({ + 'tag': 'expression', + 'attrib': {'name': self.prefix + exp_name + '_Include'}, + 'content': exp_include}) + + p_dialog.close() + return xmltree + + def get_viewitem(self, viewid): + name = _get_localized(self.meta.get('viewtypes', {}).get(viewid)) + icon = self.meta.get('icons', {}).get(viewid) + item = xbmcgui.ListItem(label=name) + item.setArt({'thumb': icon, 'icon': icon}) + return item + + def add_pluginview(self, contentid=None, pluginname=None, viewid=None): + if not contentid or not pluginname or not self.meta.get('rules', {}).get(contentid): + return + if not viewid: + items, ids = [], [] + for i in self.meta.get('rules', {}).get(contentid, {}).get('viewtypes', []): + ids.append(i) + items.append(self.get_viewitem(i) if self.icons else _get_localized(self.meta.get('viewtypes', {}).get(i))) + header = '{} {} ({})'.format(ADDON.getLocalizedString(32004), pluginname, contentid) + from resources.lib.kodiutils import isactive_winprop + with isactive_winprop('SkinViewtypes.DialogIsActive'): + choice = xbmcgui.Dialog().select(header, items, useDetails=True if self.icons else False) + viewid = ids[choice] if choice != -1 else None + if not viewid: + return # No viewtype chosen + self.addon_meta.setdefault(pluginname, {}) + self.addon_meta[pluginname][contentid] = viewid + return viewid + + def make_xmlfile(self, skinfolder=None, hashvalue=None): + xmltree = self.make_xmltree() + + # # Get folder to save to + folders = [skinfolder] if skinfolder else self.skinfolders + if folders: + from resources.lib.xmlhelper import make_xml_includes + write_skinfile( + folders=folders, filename='script-skinviewtypes-includes.xml', + content=make_xml_includes(xmltree), + checksum='script-skinviewtypes-checksum', + hashname='script-skinviewtypes-hash', hashvalue=hashvalue) + + write_file(filepath=self.addon_datafile, content=dumps(self.addon_meta)) + + def add_newplugin(self): + """ + Get list of available plugins and allow user to choose which to views to add + """ + method = "Addons.GetAddons" + properties = ["name", "thumbnail"] + params_a = {"type": "xbmc.addon.video", "properties": properties} + params_b = {"type": "xbmc.addon.audio", "properties": properties} + params_c = {"type": "xbmc.addon.image", "properties": properties} + response_a = get_jsonrpc(method, params_a).get('result', {}).get('addons') or [] + response_b = get_jsonrpc(method, params_b).get('result', {}).get('addons') or [] + response_c = get_jsonrpc(method, params_c).get('result', {}).get('addons') or [] + response = response_a + response_b + response_c + dialog_list, dialog_ids = [], [] + for i in response: + dialog_item = xbmcgui.ListItem(label=i.get('name'), label2='{}'.format(i.get('addonid'))) + dialog_item.setArt({'icon': i.get('thumbnail'), 'thumb': i.get('thumbnail')}) + dialog_list.append(dialog_item) + dialog_ids.append(i.get('addonid')) + idx = xbmcgui.Dialog().select(ADDON.getLocalizedString(32009), dialog_list, useDetails=True) + if idx == -1: + return + pluginname = dialog_ids[idx] + contentids = [i for i in sorted(self.meta.get('rules', {}))] + idx = xbmcgui.Dialog().select(ADDON.getLocalizedString(32010), contentids) + if idx == -1: + return self.add_newplugin() # Go back to previous dialog + contentid = contentids[idx] + return self.add_pluginview(pluginname=pluginname, contentid=contentid) + + def get_addondetails(self, addonid=None, prop=None): + """ + Get details of a plugin + """ + if not addonid or not prop: + return + method = "Addons.GetAddonDetails" + params = {"addonid": addonid, "properties": [prop]} + return get_jsonrpc(method, params).get('result', {}).get('addon', {}).get(prop) + + def dc_listcomp(self, listitems, listprefix='', idprefix='', contentid=''): + return [ + ('{}{} ({})'.format(listprefix, k.capitalize(), _get_localized(self.meta.get('viewtypes', {}).get(v))), (idprefix, k)) + for k, v in listitems if not contentid or contentid == k] + + def dialog_configure(self, contentid=None, pluginname=None, viewid=None, force=False): + dialog_list = [] + + if not pluginname or pluginname == 'library': # Build list of views for content types in library + dialog_list += self.dc_listcomp( + sorted(self.addon_meta.get('library', {}).items()), listprefix='Library - ', idprefix='library', contentid=contentid) + + if not pluginname or pluginname == 'plugins': # Build list of views for content types in generic plugins + dialog_list += self.dc_listcomp( + sorted(self.addon_meta.get('plugins', {}).items()), listprefix='Plugins - ', idprefix='plugins', contentid=contentid) + + if not pluginname or pluginname != 'library': # Build list of views for content types in specific plugins + for k, v in self.addon_meta.items(): + if k in ['library', 'plugins']: # Skip the generic library/plugin views since we already built them + continue + if pluginname and pluginname != 'plugins' and pluginname != k: + continue # Only add the named plugin if not just doing generic plugins + name = self.get_addondetails(addonid=k, prop='name') + dialog_list += self.dc_listcomp( + sorted(v.items()), listprefix=u'{} - '.format(name), idprefix=k, contentid=contentid) + dialog_list.append(('Reset all {} views...'.format(name), (k, 'default'))) # Add option to reset specific plugin views + + if not contentid: # Add options to reset all views (if configuring all content types) + if not pluginname or pluginname == 'plugins': + dialog_list.append((ADDON.getLocalizedString(32011).format('plugin'), ('plugins', 'default'))) + if not pluginname or pluginname == 'library': + dialog_list.append((ADDON.getLocalizedString(32011).format('library'), ('library', 'default'))) + if not pluginname or pluginname != 'library': + dialog_list.append((ADDON.getLocalizedString(32012), (None, 'add_pluginview'))) + + idx = xbmcgui.Dialog().select(ADDON.getLocalizedString(32013), [i[0] for i in dialog_list]) # Make the dialog + if idx == -1: + return force # User cancelled + + usr_pluginname, usr_contentid = dialog_list[idx][1] # Get the selected option as a tuple + if usr_contentid == 'default': # If "default" then reset that section to defaults (after asking to confirm) + choice = xbmcgui.Dialog().yesno( + ADDON.getLocalizedString(32014).format(usr_pluginname), + ADDON.getLocalizedString(32015).format(usr_pluginname)) + + if choice and usr_pluginname == 'plugins': # Reset all plugins views to default (both generic and specific) + self.addon_meta[usr_pluginname] = self.make_defaultjson().get(usr_pluginname, {}) # Rebuild default views for generic plugins + for i in self.addon_meta.copy(): # Also remove any specific plugin entries + self.addon_meta.pop(i) if i not in ['library', 'plugins'] else None # Don't remove library views or the generic plugin views we just built + elif choice and usr_pluginname == 'library': # Reset all library views to default + self.addon_meta[usr_pluginname] = self.make_defaultjson().get(usr_pluginname, {}) + elif choice and usr_pluginname: # Reset a specific plugin to defaults + self.addon_meta.pop(usr_pluginname) # Pop the plugin entry to remove + + force = force or choice + elif usr_contentid == 'add_pluginview': # User wants to add a view for a specific plugin and content type + choice = self.add_newplugin() # Ask user to select a plugin and content type to add a view for + force = force or choice + else: # Change an existing viewtype + choice = self.add_pluginview(contentid=usr_contentid.lower(), pluginname=usr_pluginname.lower()) + force = force or choice + + return self.dialog_configure(contentid=contentid, pluginname=pluginname, viewid=viewid, force=force) # Recursively open dialog so that user can set multiple choices + + def xmlfile_exists(self, skinfolder=None, hashname='script-skinviewtypes-checksum'): + folders = [skinfolder] if skinfolder else self.skinfolders + + for folder in folders: + if not xbmcvfs.exists('special://skin/{}/script-skinviewtypes-includes.xml'.format(folder)): + return False + content = load_filecontent('special://skin/{}/script-skinviewtypes-includes.xml'.format(folder)) + if content and check_hash(hashname, make_hash(content)): + return False + return True + + def update_xml(self, force=False, skinfolder=None, contentid=None, viewid=None, pluginname=None, configure=False, no_reload=False, **kwargs): + if not self.meta: + return + + makexml = force + + # Make these strings for simplicity + contentid = contentid or '' + pluginname = pluginname or '' + + # Simple hash value based on character size of file + hashvalue = make_hash(self.content) + + if not makexml: + makexml = check_hash('script-skinviewtypes-hash', hashvalue) + + if not self.addon_meta: + self.addon_meta = self.make_defaultjson(overwrite=True) + elif makexml: + from jurialmunkey.parser import merge_dicts + self.addon_meta = merge_dicts(self.make_defaultjson(), self.addon_meta) + + if configure: # Configure kwparam so open gui + makexml = self.dialog_configure(contentid=contentid.lower(), pluginname=pluginname.lower(), viewid=viewid) + elif contentid: # If contentid defined but no configure kwparam then just select a view + pluginname = pluginname or 'library' + makexml = self.add_pluginview(contentid=contentid.lower(), pluginname=pluginname.lower(), viewid=viewid) + + if not makexml and self.xmlfile_exists(skinfolder): + return + + self.make_xmlfile(skinfolder=skinfolder, hashvalue=hashvalue) + + if no_reload: + return + + xbmc.Monitor().waitForAbort(0.4) + xbmc.executebuiltin('ReloadSkin()') diff --git a/script.skinvariables/resources/lib/xmlhelper.py b/script.skinvariables/resources/lib/xmlhelper.py new file mode 100644 index 0000000000..57bbe956fa --- /dev/null +++ b/script.skinvariables/resources/lib/xmlhelper.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import xbmcvfs +import xml.etree.ElementTree as ET + + +XML_HEADER = '' + + +def make_xml_itertxt(xmltree, indent=1, indent_spaces=4, p_dialog=None): + """ + xmltree = [{'tag': '', 'attrib': {'attrib-name': 'attrib-value'}, 'content': '' or []}] + <{tag} {attrib-name}="{attrib-value}">{content} + """ + txt = [] + indent_str = ' ' * indent_spaces * indent + + p_total = len(xmltree) if p_dialog else 0 + p_dialog_txt = '' + for p_count, i in enumerate(xmltree): + if not i.get('tag', ''): + continue # No tag name so ignore + + txt += ['\n', indent_str, '<{}'.format(i.get('tag'))] # Start our tag + + for k, v in i.get('attrib', {}).items(): + if not k: + continue + txt.append(' {}=\"{}\"'.format(k, v)) # Add tag attributes + p_dialog_txt = v + + if not i.get('content'): + txt.append('/>') + continue # No content so close tag and move onto next line + + txt.append('>') + + if p_dialog: + p_dialog.update((p_count * 100) // p_total, message=u'{}'.format(p_dialog_txt)) + + if isinstance(i.get('content'), list): + txt.append(make_xml_itertxt(i.get('content'), indent=indent + 1)) + txt += ['\n', indent_str] # Need to indent before closing tag + else: + txt.append(i.get('content')) + txt.append(''.format(i.get('tag'))) # Finish + return ''.join(txt) + + +def make_xml_includes(lines=[], p_dialog=None): + txt = [XML_HEADER] + txt.append('') + txt.append(make_xml_itertxt(lines, p_dialog=p_dialog)) + txt.append('') + return '\n'.join(txt) + + +def get_skinfolders(): + """ + Get the various xml folders for skin as defined in addon.xml + e.g. 21x9 1080i xml etc + """ + folders = [] + try: + addonfile = xbmcvfs.File('special://skin/addon.xml') + addoncontent = addonfile.read() + finally: + addonfile.close() + xmltree = ET.ElementTree(ET.fromstring(addoncontent)) + for child in xmltree.getroot(): + if child.attrib.get('point') == 'xbmc.gui.skin': + for grandchild in child: + if grandchild.tag == 'res' and grandchild.attrib.get('folder'): + folders.append(grandchild.attrib.get('folder')) + return folders diff --git a/script.skinvariables/script.py b/script.skinvariables/script.py new file mode 100644 index 0000000000..b3cfe88bb4 --- /dev/null +++ b/script.skinvariables/script.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Module: default +# Author: jurialmunkey +# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html +import sys +from resources.lib.script import Script + +if __name__ == '__main__': + Script(*sys.argv[1:]).run()