From c0f03abc8e9641070d45258d115c3c171efe7104 Mon Sep 17 00:00:00 2001 From: jurialmunkey Date: Sat, 8 Jun 2024 11:41:21 +1000 Subject: [PATCH] script.skinvariables 2.1.14 --- script.skinvariables/LICENSE.txt | 674 +++++++++++++ script.skinvariables/README.md | 12 + script.skinvariables/addon.xml | 28 + script.skinvariables/fanart.jpg | Bin 0 -> 99347 bytes script.skinvariables/icon.png | Bin 0 -> 19514 bytes script.skinvariables/plugin.py | 9 + script.skinvariables/resources/__init__.py | 0 .../resource.language.de_de/strings.po | 522 ++++++++++ .../resource.language.en_gb/strings.po | 524 ++++++++++ .../resource.language.es_es/strings.po | 521 ++++++++++ .../resource.language.fi_fi/strings.po | 522 ++++++++++ .../resource.language.fr_fr/strings.po | 498 ++++++++++ .../resource.language.nl_nl/strings.po | 97 ++ .../resource.language.pt_br/strings.po | 536 ++++++++++ .../resource.language.ru_ru/strings.po | 544 +++++++++++ .../resource.language.uk_ua/strings.po | 521 ++++++++++ .../resources/lib/__init__.py | 0 script.skinvariables/resources/lib/filters.py | 74 ++ .../resources/lib/kodiutils.py | 39 + .../resources/lib/lists/filterdir.py | 808 ++++++++++++++++ .../resources/lib/lists/koditools.py | 146 +++ .../resources/lib/lists/playerstreams.py | 68 ++ .../resources/lib/lists/rpcdetails.py | 187 ++++ .../resources/lib/lists/skinusers.py | 208 ++++ script.skinvariables/resources/lib/method.py | 314 ++++++ .../resources/lib/operations.py | 178 ++++ script.skinvariables/resources/lib/plugin.py | 103 ++ script.skinvariables/resources/lib/script.py | 87 ++ .../resources/lib/shortcuts/browser.py | 128 +++ .../resources/lib/shortcuts/common.py | 53 + .../resources/lib/shortcuts/futils.py | 55 ++ .../resources/lib/shortcuts/grouping.py | 55 ++ .../resources/lib/shortcuts/jsonrpc.py | 63 ++ .../resources/lib/shortcuts/method.py | 168 ++++ .../resources/lib/shortcuts/node.py | 912 ++++++++++++++++++ .../resources/lib/shortcuts/skinshortcuts.py | 296 ++++++ .../resources/lib/shortcuts/template.py | 331 +++++++ .../resources/lib/shortcuts/xmltojson.py | 250 +++++ .../resources/lib/skinvariables.py | 236 +++++ .../resources/lib/viewtypes.py | 399 ++++++++ .../resources/lib/xmlhelper.py | 77 ++ script.skinvariables/script.py | 9 + 42 files changed, 10252 insertions(+) create mode 100644 script.skinvariables/LICENSE.txt create mode 100644 script.skinvariables/README.md create mode 100644 script.skinvariables/addon.xml create mode 100644 script.skinvariables/fanart.jpg create mode 100644 script.skinvariables/icon.png create mode 100644 script.skinvariables/plugin.py create mode 100644 script.skinvariables/resources/__init__.py create mode 100644 script.skinvariables/resources/language/resource.language.de_de/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.en_gb/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.es_es/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.fi_fi/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.fr_fr/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.nl_nl/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.pt_br/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.ru_ru/strings.po create mode 100644 script.skinvariables/resources/language/resource.language.uk_ua/strings.po create mode 100644 script.skinvariables/resources/lib/__init__.py create mode 100644 script.skinvariables/resources/lib/filters.py create mode 100644 script.skinvariables/resources/lib/kodiutils.py create mode 100644 script.skinvariables/resources/lib/lists/filterdir.py create mode 100644 script.skinvariables/resources/lib/lists/koditools.py create mode 100644 script.skinvariables/resources/lib/lists/playerstreams.py create mode 100644 script.skinvariables/resources/lib/lists/rpcdetails.py create mode 100644 script.skinvariables/resources/lib/lists/skinusers.py create mode 100644 script.skinvariables/resources/lib/method.py create mode 100644 script.skinvariables/resources/lib/operations.py create mode 100644 script.skinvariables/resources/lib/plugin.py create mode 100644 script.skinvariables/resources/lib/script.py create mode 100644 script.skinvariables/resources/lib/shortcuts/browser.py create mode 100644 script.skinvariables/resources/lib/shortcuts/common.py create mode 100644 script.skinvariables/resources/lib/shortcuts/futils.py create mode 100644 script.skinvariables/resources/lib/shortcuts/grouping.py create mode 100644 script.skinvariables/resources/lib/shortcuts/jsonrpc.py create mode 100644 script.skinvariables/resources/lib/shortcuts/method.py create mode 100644 script.skinvariables/resources/lib/shortcuts/node.py create mode 100644 script.skinvariables/resources/lib/shortcuts/skinshortcuts.py create mode 100644 script.skinvariables/resources/lib/shortcuts/template.py create mode 100644 script.skinvariables/resources/lib/shortcuts/xmltojson.py create mode 100644 script.skinvariables/resources/lib/skinvariables.py create mode 100644 script.skinvariables/resources/lib/viewtypes.py create mode 100644 script.skinvariables/resources/lib/xmlhelper.py create mode 100644 script.skinvariables/script.py 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 0000000000000000000000000000000000000000..728657a70f11a0970af7d9b9e0333d79a6f499a3 GIT binary patch literal 99347 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 z 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 literal 0 HcmV?d00001 diff --git a/script.skinvariables/icon.png b/script.skinvariables/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e863e695afd9260dc01510639cc06cae1811aa GIT binary patch literal 19514 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 literal 0 HcmV?d00001 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()