Changeset 343:2f1f980c070a
Added xbmc stuff
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/LICENSE.txt Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +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: + + <program> Copyright (C) <year> <name of author> + 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 +<http://www.gnu.org/licenses/>. + + 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 +<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeCore.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,902 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import re +import sys +import time +import socket +import urllib +import urllib2 +#import chardet + +try: + import simplejson as json +except ImportError: + import json + +# ERRORCODES: +# 200 = OK +# 303 = See other (returned an error message) +# 500 = uncaught error + + +class url2request(urllib2.Request): + """Workaround for using DELETE with urllib2""" + def __init__(self, url, method, data=None, headers={}, origin_req_host=None, unverifiable=False): + self._method = method + urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) + + def get_method(self): + if self._method: + return self._method + else: + return urllib2.Request.get_method(self) + + +class YouTubeCore(): + APIKEY = "AI39si6hWF7uOkKh4B9OEAX-gK337xbwR9Vax-cdeF9CF9iNAcQftT8NVhEXaORRLHAmHxj6GjM-Prw04odK4FxACFfKkiH9lg" + + #=============================================================================== + # The time parameter restricts the search to videos uploaded within the specified time. + # Valid values for this parameter are today (1 day), this_week (7 days), this_month (1 month) and all_time. + # The default value for this parameter is all_time. + # + # This parameter is supported for search feeds as well as for the top_rated, top_favorites, most_viewed, + # most_popular, most_discussed and most_responded standard feeds. + #=============================================================================== + + urls = {} + urls['batch'] = "http://gdata.youtube.com/feeds/api/videos/batch" + urls['thumbnail'] = "http://i.ytimg.com/vi/%s/0.jpg" + + def __init__(self): + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + self.storage = sys.modules["__main__"].storage + self.cache = sys.modules["__main__"].cache + self.login = sys.modules["__main__"].login + self.utils = sys.modules["__main__"].utils + self.common = sys.modules["__main__"].common + urllib2.install_opener(sys.modules["__main__"].opener) + + timeout = [5, 10, 15, 20, 25][int(self.settings.getSetting("timeout"))] + if not timeout: + timeout = "15" + socket.setdefaulttimeout(float(timeout)) + return None + + def delete_favorite(self, params={}): + self.common.log("") + get = params.get + + delete_url = "http://gdata.youtube.com/feeds/api/users/default/favorites" + delete_url += "/" + get('editid') + result = self._fetchPage({"link": delete_url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def remove_contact(self, params={}): + self.common.log("") + get = params.get + delete_url = "http://gdata.youtube.com/feeds/api/users/default/contacts" + delete_url += "/" + get("contact") + result = self._fetchPage({"link": delete_url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def remove_subscription(self, params={}): + self.common.log("") + get = params.get + delete_url = "http://gdata.youtube.com/feeds/api/users/default/subscriptions" + delete_url += "/" + get("editid") + result = self._fetchPage({"link": delete_url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def add_contact(self, params={}): + self.common.log("") + get = params.get + url = "http://gdata.youtube.com/feeds/api/users/default/contacts" + add_request = '<?xml version="1.0" encoding="UTF-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:yt="http://gdata.youtube.com/schemas/2007"><yt:username>%s</yt:username></entry>' % get("contact") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "request": add_request}) + return (result["content"], result["status"]) + + def add_favorite(self, params={}): + get = params.get + url = "http://gdata.youtube.com/feeds/api/users/default/favorites" + add_request = '<?xml version="1.0" encoding="UTF-8"?><entry xmlns="http://www.w3.org/2005/Atom"><id>%s</id></entry>' % get("videoid") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "request": add_request}) + return (result["content"], result["status"]) + + def add_subscription(self, params={}): + self.common.log("") + get = params.get + url = "http://gdata.youtube.com/feeds/api/users/default/subscriptions" + add_request = '<?xml version="1.0" encoding="UTF-8"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:yt="http://gdata.youtube.com/schemas/2007"> <category scheme="http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat" term="user"/><yt:username>%s</yt:username></entry>' % get("channel") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "request": add_request}) + return (result["content"], result["status"]) + + def add_playlist(self, params={}): + get = params.get + url = "http://gdata.youtube.com/feeds/api/users/default/playlists" + add_request = '<?xml version="1.0" encoding="UTF-8"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:yt="http://gdata.youtube.com/schemas/2007"><title type="text">%s</title><summary>%s</summary></entry>' % (get("title"), get("summary")) + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "request": add_request}) + return (result["content"], result["status"]) + + def del_playlist(self, params={}): + self.common.log("") + get = params.get + url = "http://gdata.youtube.com/feeds/api/users/default/playlists/%s" % (get("playlist")) + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def add_to_playlist(self, params={}): + get = params.get + self.common.log("") + url = "http://gdata.youtube.com/feeds/api/playlists/%s" % get("playlist") + add_request = '<?xml version="1.0" encoding="UTF-8"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:yt="http://gdata.youtube.com/schemas/2007"><id>%s</id></entry>' % get("videoid") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "request": add_request}) + return (result["content"], result["status"]) + + def remove_from_playlist(self, params={}): + self.common.log("") + get = params.get + url = "http://gdata.youtube.com/feeds/api/playlists/%s/%s" % (get("playlist"), get("playlist_entry_id")) + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def remove_from_watch_later(self, params={}): + self.common.log("") + get = params.get + url = "https://gdata.youtube.com/feeds/api/users/default/watch_later/%s" % get("playlist_entry_id") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def set_video_watched(self, params={}): + self.common.log("") + get = params.get + url = "https://gdata.youtube.com/feeds/api/users/default/watch_later/%s" % get("videoid") + result = self._fetchPage({"link": url, "api": "true", "login": "true", "auth": "true", "method": "DELETE"}) + return (result["content"], result["status"]) + + def getCategoriesFolderInfo(self, xml, params={}): + self.common.log("") + self.common.log(xml) + entries = self.common.parseDOM(xml, "atom:category", ret=True) + + folders = [] + for node in entries: + folder = {} + + if len(self.common.parseDOM(node, "yt:deprecated")): + continue + title = self.common.parseDOM(node, "atom:category", ret="label")[0] + + if title: + folder['Title'] = self.common.replaceHTMLCodes(title) + + folder['category'] = self.common.parseDOM(node, "atom:category", ret="term")[0] + folder["icon"] = "explore" + folder["thumbnail"] = "explore" + folder["feed"] = "feed_category" + + folders.append(folder) + + return folders + + def getFolderInfo(self, xml, params={}): + get = params.get + + self.common.log(xml) + entries = self.common.parseDOM(xml, "entry") + show_next = False + + #find out if there are more pages + for link in self.common.parseDOM(xml, "link", ret="rel"): + if link == "next": + show_next = True + break + + folders = [] + for node in entries: + folder = {"published": "2008-07-05T19:56:35.000-07:00"} + + if get("feed") != "feed_categories": + folder["login"] = "true" + title = self.common.parseDOM(node, "title")[0] + if title.find(": ") > 0: + title = title[title.find(": ") + 2:] + title = self.common.replaceHTMLCodes(title) + + folder['Title'] = title + for tmp in self.common.parseDOM(node, "published"): + folder['published'] = tmp + + for entryid in self.common.parseDOM(node, "id"): + folder["editid"] = entryid[entryid.rfind(":") + 1:] + + thumb = "" + if get("user_feed") == "contacts": + folder["thumbnail"] = "user" + folder["contact"] = self.common.parseDOM(node, 'yt:username')[0] + folder["store"] = "contact_options" + folder["folder"] = "true" + + if get("user_feed") == "subscriptions": + folder["channel"] = self.common.parseDOM(node, 'yt:username')[0] + + if get("user_feed") == "playlists": + folder['playlist'] = self.common.parseDOM(node, 'yt:playlistId')[0] + folder["user_feed"] = "playlist" + + params["thumb"] = "true" + thumb = self.storage.retrieve(params, "thumbnail", folder) + if thumb: + folder["thumbnail"] = thumb + + folders.append(folder) + + if show_next: + self.utils.addNextFolder(folders, params) + + return folders + + def getBatchDetailsOverride(self, items, params={}): + videoids = [] + + for video in items: + for k, v in video.items(): + if k == "videoid": + videoids.append(v) + + (ytobjects, status) = self.getBatchDetails(videoids, params) + + for video in items: + videoid = video["videoid"] + for item in ytobjects: + if item['videoid'] == videoid: + for k, v in video.items(): + item[k] = v + + while len(items) > len(ytobjects): + ytobjects.append({'videoid': 'false'}) + + return (ytobjects, 200) + + def getBatchDetailsThumbnails(self, items, params={}): + ytobjects = [] + videoids = [] + + for (videoid, thumb) in items: + videoids.append(videoid) + + (tempobjects, status) = self.getBatchDetails(videoids, params) + + for i in range(0, len(items)): + (videoid, thumbnail) = items[i] + for item in tempobjects: + if item['videoid'] == videoid: + item['thumbnail'] = thumbnail + ytobjects.append(item) + break + + while len(items) > len(ytobjects): + ytobjects.append({'videoid': 'false'}) + + return (ytobjects, 200) + + def getBatchDetails(self, items, params={}): + self.common.log("params: " + repr(params)) + self.common.log("items: " + str(len(items))) + request_start = "<feed xmlns='http://www.w3.org/2005/Atom'\n xmlns:media='http://search.yahoo.com/mrss/'\n xmlns:batch='http://schemas.google.com/gdata/batch'\n xmlns:yt='http://gdata.youtube.com/schemas/2007'>\n <batch:operation type='query'/> \n" + request_end = "</feed>" + + video_request = "" + + ytobjects = [] + status = 500 + i = 1 + + temp_objs = self.cache.getMulti("videoidcache", items) + + for index, videoid in enumerate(items): + if index < len(temp_objs): + if temp_objs[index]: + ytobjects.append(eval(temp_objs[index])) + continue + if videoid: + video_request += "<entry> \n <id>http://gdata.youtube.com/feeds/api/videos/" + videoid + "</id>\n</entry> \n" + if i == 50: + final_request = request_start + video_request + request_end + rstat = 403 + while rstat == 403: + result = self._fetchPage({"link": "http://gdata.youtube.com/feeds/api/videos/batch", "api": "true", "request": final_request}) + rstat = self.common.parseDOM(result["content"], "batch:status", ret="code") + if len(rstat) > 0: + if int(rstat[len(rstat) - 1]) == 403: + self.common.log("quota exceeded. Waiting 5 seconds. " + repr(rstat)) + rstat = 403 + time.sleep(5) + + temp = self.getVideoInfo(result["content"], params) + ytobjects += temp + video_request = "" + i = 1 + i += 1 + + if i > 1: + final_request = request_start + video_request + request_end + result = self._fetchPage({"link": "http://gdata.youtube.com/feeds/api/videos/batch", "api": "true", "request": final_request}) + + temp = self.getVideoInfo(result["content"], params) + ytobjects += temp + + save_data = {} + for item in ytobjects: + save_data[item["videoid"]] = repr(item) + self.cache.setMulti("videoidcache", save_data) + + if len(ytobjects) > 0: + status = 200 + + self.common.log("ytobjects: " + str(len(ytobjects))) + + return (ytobjects, status) + + #=============================================================================== + # + # Internal functions to YouTubeCore.py + # + # Return should be value(True for bool functions), or False if failed. + # + # False MUST be handled properly in External functions + # + #=============================================================================== + + def _fetchPage(self, params={}): # This does not handle cookie timeout for _httpLogin + if self.settings.getSetting("force_proxy") == "true" and self.settings.getSetting("proxy"): + params["proxy"] = self.settings.getSetting("proxy") + + get = params.get + link = get("link") + ret_obj = {"status": 500, "content": "", "error": 0} + cookie = "" + + if (get("url_data") or get("request") or get("hidden")) and False: + self.common.log("called for : " + repr(params['link'])) + else: + self.common.log("called for : " + repr(params)) + + if get("auth", "false") == "true": + self.common.log("got auth") + if self._getAuth(): + if link.find("?") > -1: + link += "&oauth_token=" + self.settings.getSetting("oauth2_access_token") + else: + link += "?oauth_token=" + self.settings.getSetting("oauth2_access_token") + + self.common.log("updated link: " + link) + else: + self.common.log("couldn't get login token") + + if not link or get("error", 0) > 2: + self.common.log("giving up") + return ret_obj + + if get("url_data"): + request = urllib2.Request(link, urllib.urlencode(get("url_data"))) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + elif get("request", "false") == "false": + if get("proxy"): + proxy = get("proxy") + link = proxy + urllib.quote(link) + self.common.log("got proxy: %s" % link) + else: + self.common.log("got default: %s" % link) + + request = url2request(link, get("method", "GET")) + else: + self.common.log("got request") + request = urllib2.Request(link, get("request")) + request.add_header('X-GData-Client', "") + request.add_header('Content-Type', 'application/atom+xml') + request.add_header('Content-Length', str(len(get("request")))) + + if get("proxy") or (self.settings.getSetting("proxy") != "" and link.find(self.settings.getSetting("proxy")) > -1): + proxy = self.settings.getSetting("proxy") + referer = proxy[:proxy.rfind("/")] + self.common.log("Added proxy refer: %s" % referer) + + request.add_header('Referer', referer) + + if get("api", "false") == "true": + self.common.log("got api") + request.add_header('GData-Version', '2.1') + request.add_header('X-GData-Key', 'key=' + self.APIKEY) + else: + request.add_header('User-Agent', self.common.USERAGENT) + + if get("no-language-cookie", "false") == "false": + cookie += "PREF=f1=50000000&hl=en;" + + if get("login", "false") == "true": + self.common.log("got login") + if (self.settings.getSetting("username") == "" or self.settings.getSetting("user_password") == ""): + self.common.log("_fetchPage, login required but no credentials provided") + ret_obj["status"] = 303 + ret_obj["content"] = self.language(30622) + return ret_obj + + # This should be a call to self.login._httpLogin() + if self.settings.getSetting("login_cookies") == "": + if isinstance(self.login, str): + self.login = sys.modules["__main__"].login + self.login._httpLogin() + + if self.settings.getSetting("login_cookies") != "": + tcookies = eval(self.settings.getSetting("login_cookies")) + self.common.log("Adding login cookies: " + repr(tcookies.keys())) + for key in tcookies.keys(): + cookie += "%s=%s;" % ( key, tcookies[key]) + + if get("referer", "false") != "false": + self.common.log("Added referer: %s" % get("referer")) + request.add_header('Referer', get("referer")) + + try: + self.common.log("connecting to server... %s" % link ) + + if cookie: + self.common.log("Setting cookie: " + cookie) + request.add_header('Cookie', cookie) + + con = urllib2.urlopen(request) + + inputdata = con.read() + ret_obj["content"] = inputdata.decode("utf-8") + ret_obj["location"] = link + + ret_obj["new_url"] = con.geturl() + ret_obj["header"] = str(con.info()) + con.close() + + self.common.log("Result: %s " % repr(ret_obj), 9) + + # Return result if it isn't age restricted + if (ret_obj["content"].find("verify-actions") == -1 and ret_obj["content"].find("verify-age-actions") == -1): + self.common.log("done") + ret_obj["status"] = 200 + return ret_obj + else: + self.common.log("Youtube requires you to verify your age to view this content: " + repr(params)) + + except urllib2.HTTPError, e: + cont = False + err = str(e) + msg = e.read() + + self.common.log("HTTPError : " + err) + if e.code == 400 or True: + self.common.log("Unhandled HTTPError : [%s] %s " % (e.code, msg), 1) + + if msg.find("<?xml") > -1: + acted = False + + self.common.log("REPLACE THIS MINIDOM WITH PARSEDOM: " + repr(msg)) + import xml.dom.minidom as minidom + dom = minidom.parseString(msg) + self.common.log(str(len(msg))) + domains = dom.getElementsByTagName("domain") + codes = dom.getElementsByTagName("code") + for domain in domains: + self.common.log(repr(domain.firstChild.nodeValue), 5) + if domain.firstChild.nodeValue == "yt:quota": + self.common.log("Hit quota... sleeping for 100 seconds") + time.sleep(100) + acted = True + + if not acted: + for code in codes: + self.common.log(repr(code.firstChild.nodeValue), 5) + if code.firstChild.nodeValue == "too_many_recent_calls": + self.common.log("Hit quota... sleeping for 10 seconds") + time.sleep(10) + acted = True + + else: # Legacy this. + if msg.find("yt:quota") > 1: + self.common.log("Hit quota... sleeping for 10 seconds") + time.sleep(10) + elif msg.find("too_many_recent_calls") > 1: + self.common.log("Hit quota... sleeping for 10 seconds") + time.sleep(10) + elif err.find("Token invalid") > -1: + self.common.log("refreshing token") + self._oRefreshToken() + elif err.find("User Rate Limit Exceeded") > -1: + self.common.log("Hit limit... Sleeping for 10 seconds") + time.sleep(10) + else: + if e.fp: + cont = e.fp.read() + self.common.log("HTTPError - Headers: " + str(e.headers) + " - Content: " + cont) + + params["error"] = get("error", 0) + 1 + ret_obj = self._fetchPage(params) + + if cont and ret_obj["content"] == "": + ret_obj["content"] = cont + ret_obj["status"] = 303 + + return ret_obj + + except urllib2.URLError, e: + err = str(e) + self.common.log("URLError : " + err) + if err.find("SSL") > -1: + ret_obj["status"] = 303 + ret_obj["content"] = self.language(30629) + ret_obj["error"] = 3 # Tell _findErrors that we have an error + return ret_obj + + time.sleep(3) + params["error"] = get("error", 0) + 1 + ret_obj = self._fetchPage(params) + return ret_obj + + except socket.timeout: + self.common.log("Socket timeout") + return ret_obj + + def _findErrors(self, ret, silent=False): + self.common.log("") + + ## Couldn't find 2 factor or normal login + error = self.common.parseDOM(ret['content'], "div", attrs={"class": "errormsg"}) + if len(error) == 0: + # An error in 2-factor + self.common.log("1") + error = self.common.parseDOM(ret['content'], "div", attrs={"class": "error smaller"}) + if len(error) == 0: + self.common.log("2") + error = self.common.parseDOM(ret['content'], "div", attrs={"id": "unavailable-message"}) + if len(error) == 0 and ret['content'].find("yt:quota") > -1: + self.common.log("3") + # Api quota + html = self.common.parseDOM(ret['content'], "error") + error = self.common.parseDOM(html, "code") + + if len(error) == 0: # Bad password for _httpLogin. + error = self.common.parseDOM(ret['content'], "span", attrs={"class": "errormsg"}) + + # Has a link. Lets remove that. + if len(error) == 1: + if error[0].find("<") > -1: + error[0] = error[0][0:error[0].find("<")] + + if len(error) == 0: + self.common.log("4") + error = self.common.parseDOM(ret['content'], "div", attrs={"id": "watch7-player-age-gate-content"}) + + if len(error) > 0: + self.common.log("Found error: " + repr(error)) + error = self.common.stripTags(error[0]) + self.common.log("Found error: " + repr(error)) + if error.find("[") > -1: + error = error[0:error.find("[")] + error = urllib.unquote(error.replace("\n", " ").replace(" ", " ")).replace("'", "'") + self.common.log("returning error : " + error.strip()) + return error.strip() + + # If no error was found. But fetchPage has an error level of 3+, return the fetchPage content. + if len(error) == 0 and ret["error"] >= 3: + self.common.log("Returning error from _fetchPage : " + repr(ret)) + return ret["content"] + + if not silent: + self.common.log("couldn't find any errors: " + repr(ret)) + + return False + + def _oRefreshToken(self): + self.common.log("") + # Refresh token + if self.settings.getSetting("oauth2_refresh_token"): + url = "https://accounts.google.com/o/oauth2/token" + data = {"client_id": "208795275779.apps.googleusercontent.com", + "client_secret": "sZn1pllhAfyonULAWfoGKCfp", + "refresh_token": self.settings.getSetting("oauth2_refresh_token"), + "grant_type": "refresh_token"} + self.settings.setSetting("oauth2_access_token", "") + ret = self._fetchPage({"link": url, "no-language-cookie": "true", "url_data": data}) + if ret["status"] == 200: + oauth = "" + try: + oauth = json.loads(ret["content"]) + except: + self.common.log("Except: " + repr(ret)) + return False + + self.common.log("- returning, got result a: " + repr(oauth)) + + self.settings.setSetting("oauth2_access_token", oauth["access_token"]) + self.settings.setSetting("oauth2_expires_at", str(int(oauth["expires_in"]) + time.time()) ) + self.common.log("Success") + return True + else: + self.common.log("Failure, Trying a clean login") + if isinstance(self.login, str): + self.login = sys.modules["__main__"].login + self.login.login({"new": "true"}) + return False + + self.common.log("didn't even try") + + return False + + def performNewLogin(self): + self.common.log("") + if isinstance(self.login, str): + self.login = sys.modules["__main__"].login + + (result, status) = self.login.login() + + if status == 200: + self.common.log("returning new auth") + return self.settings.getSetting("oauth2_access_token") + + self.common.log("failed because login failed") + return False + + def refreshTokenIfNessecary(self): + now = time.time() + + if self.settings.getSetting("oauth2_expires_at"): + expire_at = float(self.settings.getSetting("oauth2_expires_at")) + else: + expire_at = now + + if expire_at <= now: + self.common.log("Oauth expired refreshing") + self._oRefreshToken() + + def _getAuth(self): + self.common.log("") + + self.refreshTokenIfNessecary() + + auth = self.settings.getSetting("oauth2_access_token") + if (auth): + return auth + + return self.performNewLogin() + + def getVideoId(self, node): + videoid = "false" + for id in self.common.parseDOM(node, "yt:videoid"): + videoid = id + + if videoid == "false": + for id in self.common.parseDOM(node, "content", ret="src"): + videoid = id + videoid = videoid[videoid.rfind("/") + 1:] + + if videoid == "false": + for id in self.common.parseDOM(node, "link", ret="href"): + match = re.match('.*?v=(.*)\&.*', id) + if match: + videoid = match.group(1) + + return videoid + + def getPlaylistId(self, node): + result = "" + + for entryid in self.common.parseDOM(node, "id"): + entryid = entryid[entryid.rfind(":") + 1:] + result = entryid + + return result + + def videoIsUnavailable(self, node): + result = False + + for state in self.common.parseDOM(node, "yt:state", ret=True): + # Ignore unplayable items. + if (state == 'deleted' or state == 'rejected'): + result = True + + # Get reason for why we can't playback the file. + reason = self.common.parseDOM(node, "yt:state", ret="reasonCode") + value = self.common.parseDOM(node, "yt:state") + + if not reason: + return result + + if reason[0] in [ "private", 'requesterRegion']: + result = True + elif reason[0] != 'limitedSyndication': + self.common.log("removing video, reason: %s value: %s" % (reason[0], value[0])) + result = True + + return result + + def getVideoEditId(self, node): + result = "" + for edit_link in self.common.parseDOM(node, "link", ret=True): + for obj in self.common.parseDOM(edit_link, "link", attrs={"rel": "edit"}, ret="href"): + result = obj[obj.rfind('/') + 1:] + return result + + def addNextPageLinkIfNecessary(self, params, xml, ytobjects): + show_next = False + # find out if there are more pages + for link in self.common.parseDOM(xml, "link", ret="rel"): + if link == "next": + show_next = True + break + if show_next: + self.utils.addNextFolder(ytobjects, params) + + def updateVideoIdStatusInCache(self, pre_id, ytobjects): + self.common.log(pre_id) + save_data = {} + for item in ytobjects: + if "videoid" in item: + save_data[item["videoid"]] = repr(item) + + self.cache.setMulti(pre_id, save_data) + + def getVideoIdStatusFromCache(self, pre_id, ytobjects): + self.common.log(pre_id) + load_data = [] + for item in ytobjects: + if "videoid" in item: + load_data.append(item["videoid"]) + + res = self.cache.getMulti(pre_id, load_data) + if len(res) != len(load_data): + self.common.log("Length mismatch:" + repr(res) + " - " + repr(load_data)) + i = 0 + for item in ytobjects: + if "videoid" in item: + if i < len(res): + item["Overlay"] = res[i] + i += 1 # This can NOT be enumerated because there might be missing videoids + return ytobjects + + def getVideoEntries(self, xml): + entries = self.common.parseDOM(xml, "entry") + if not entries: + entries = self.common.parseDOM(xml, "atom:entry") + + return entries + + def getVideoCreator(self, node): + result = "" + + # media:credit is not set for favorites, playlists + for tmp in self.common.parseDOM(node, "media:credit"): + result = tmp + if result == "": + for tmp in self.common.parseDOM(node, "name"): + result = tmp + + return result + + def getVideoTitle(self, node): + result = "" + for tmp in self.common.parseDOM(node, "media:title"): + result = self.common.replaceHTMLCodes(tmp) + return result + + def getVideoDuration(self, node): + result = 1 + + for tmp in self.common.parseDOM(node, "yt:duration", ret="seconds"): + tmp = int(tmp) / 60 + if tmp: + result = tmp + + return result + + def getVideoUploadDate(self, node): + result = time.localtime() + + for tmp in self.common.parseDOM(node, "published"): + result = time.strptime(tmp[:tmp.find(".")], "%Y-%m-%dT%H:%M:%S") + + return result + + def getViewCount(self, node): + result = 0 + + for tmp in self.common.parseDOM(node, "yt:statistics", ret="viewCount"): + result = int(tmp) + + return result + + def getVideoDescription(self, node, uploadDate, viewCount): + result = "" + + for tmp in self.common.parseDOM(node, "media:description"): + result = self.common.replaceHTMLCodes(tmp) + + infoString = "Date Uploaded: " + time.strftime("%Y-%m-%d %H:%M:%S", uploadDate) + ", " + infoString += "View count: " + str(viewCount) + + result = infoString + "\n" + result + + return result + + def getVideoRating(self, node): + result = 0.0 + + for tmp in self.common.parseDOM(node, "gd:rating", ret="average"): + result = float(tmp) + + return result + + def getVideoGenre(self, node): + result = "" + + for tmp in self.common.parseDOM(node, "media:category", ret="label"): + result = self.common.replaceHTMLCodes(tmp) + + return result + + def getVideoInfo(self, xml, params={}): + self.common.log("", 3) + + entries = self.getVideoEntries(xml) + + ytobjects = [] + for node in entries: + video ={} + + video["videoid"] = self.getVideoId(node) + video["playlist_entry_id"] = self.getPlaylistId(node) + video['editid'] = self.getVideoEditId(node) + + if self.videoIsUnavailable(node): + self.common.log("Video is unavailable, removing from result.", 3) + video["videoid"] = "false" + + video["Studio"] = self.getVideoCreator(node) + video["Title"] = self.getVideoTitle(node) + video["Duration"] = self.getVideoDuration(node) + video["Rating"] = self.getVideoRating(node) + video["Genre"] = self.getVideoGenre(node) + + viewCount = self.getViewCount(node) + video["Count"] = viewCount + uploadDate = self.getVideoUploadDate(node) + video['Date'] = time.strftime("%d-%m-%Y", uploadDate) + + video["Plot"] = self.getVideoDescription(node, uploadDate, viewCount) + + video['thumbnail'] = self.urls["thumbnail"] % video['videoid'] + + ytobjects.append(video) + + self.addNextPageLinkIfNecessary(params, xml, ytobjects) + + self.updateVideoIdStatusInCache("videoidcache", ytobjects) + self.getVideoIdStatusFromCache("vidstatus-", ytobjects) + + self.common.log("Done: " + str(len(ytobjects)),3) + return ytobjects
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeFeeds.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,357 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib + + +class YouTubeFeeds(): + # YouTube General Feeds + urls = {} + urls['playlist'] = "http://gdata.youtube.com/feeds/api/playlists/%s" + urls['related'] = "http://gdata.youtube.com/feeds/api/videos/%s/related" + urls['search'] = "http://gdata.youtube.com/feeds/api/videos?q=%s&safeSearch=%s" + + # YouTube User specific Feeds + urls['uploads'] = "http://gdata.youtube.com/feeds/api/users/%s/uploads" + urls['favorites'] = "http://gdata.youtube.com/feeds/api/users/%s/favorites" + urls['playlists'] = "http://gdata.youtube.com/feeds/api/users/%s/playlists" + urls['contacts'] = "http://gdata.youtube.com/feeds/api/users/default/contacts" + urls['subscriptions'] = "http://gdata.youtube.com/feeds/api/users/%s/subscriptions" + urls['newsubscriptions'] = "http://gdata.youtube.com/feeds/api/users/%s/newsubscriptionvideos" + urls["recommended"] = "http://gdata.youtube.com/feeds/api/users/default/recommendations" + urls['watch_later'] = "http://gdata.youtube.com/feeds/api/users/default/watch_later?v=2.1" + urls['watch_history'] = "http://gdata.youtube.com/feeds/api/users/default/watch_history?v=2" + + # YouTube Standard feeds + urls['feed_categories'] = "http://gdata.youtube.com/schemas/2007/categories.cat" + urls['feed_category'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed_%s?v=2&time=%s" + urls['feed_rated'] = "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?time=%s" + urls['feed_favorites'] = "http://gdata.youtube.com/feeds/api/standardfeeds/top_favorites?time=%s" + urls['feed_viewed'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed?time=%s" + urls['feed_linked'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?time=%s" + urls['feed_discussed'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_discussed?time=%s" + urls['feed_responded'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_responded?time=%s" + urls['feed_live'] = "http://gdata.youtube.com/feeds/api/charts/live/events/live_now" + + # Wont work with time parameter + urls['feed_recent'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_recent" + urls['feed_featured'] = "http://gdata.youtube.com/feeds/api/standardfeeds/recently_featured" + urls['feed_trending'] = "http://gdata.youtube.com/feeds/api/standardfeeds/on_the_web" + urls['feed_shared'] = "http://gdata.youtube.com/feeds/api/standardfeeds/most_shared" + + def __init__(self): + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + self.utils = sys.modules["__main__"].utils + self.storage = sys.modules["__main__"].storage + self.core = sys.modules["__main__"].core + self.pluginsettings = sys.modules["__main__"].pluginsettings + self.common = sys.modules["__main__"].common + + def createUrl(self, params={}): + self.common.log("", 4) + get = params.get + time = "this_week" + per_page = self.pluginsettings.itemsPerPage() + region = self.pluginsettings.currentRegion() + + page = get("page", "0") + start_index = per_page * int(page) + 1 + url = "" + + if (get("feed")): + url = self.urls[get("feed")] + + if (get("user_feed")): + url = self.urls[get("user_feed")] + + if get("search"): + url = self.urls["search"] + query = urllib.unquote_plus(get("search")) + safe_search = self.pluginsettings.safeSearchLevel() + url = url % (query, safe_search) + authors = self.settings.getSetting("stored_searches_author") + if len(authors) > 0: + try: + authors = eval(authors) + if query in authors: + url += "&" + urllib.urlencode({'author': authors[query]}) + except: + self.common.log("Search - eval failed") + + if (url.find("%s") > 0): + if (get("contact") and not (get("external") and get("channel"))): + url = url % get("contact") + elif (get("channel")): + url = url % get("channel") + elif (get("playlist")): + url = url % get("playlist") + elif (get("videoid") and not get("action") == "add_to_playlist"): + url = url % get("videoid") + elif (get("category")): + url = url % (get("category"), "today") + elif (url.find("time=") > 0): + url = url % time + else: + url = url % "default" + + if (url.find("?") == -1): + url += "?" + else: + url += "&" + + if not get("playlist") and not get("folder") and not get("action") == "play_all" and not get("action") == "add_to_playlist": + url += "start-index=" + repr(start_index) + "&max-results=" + repr(per_page) + + if (url.find("standardfeeds") > 0 and region): + url = url.replace("/standardfeeds/", "/standardfeeds/" + region + "/") + + url = url.replace(" ", "+") + self.common.log(url, 4) + return url + + def list(self, params={}): + self.common.log("", 4) + get = params.get + result = {"content": "", "status": 303} + + if get("folder"): + return self.listFolder(params) + + if get("playlist"): + return self.listPlaylist(params) + + if get("login") == "true": + if (not self.core._getAuth()): + self.common.log("Login required but auth wasn't set!") + return (self.language(30609), 303) + + url = self.createUrl(params) + + if url: + self.common.log(repr(url), 4) + result = self.core._fetchPage({"link": url, "auth": get("login"), "api": "true"}) + + if result["status"] != 200: + return (result["content"], result["status"]) + + videos = self.core.getVideoInfo(result["content"], params) + + if len(videos) == 0: + return (videos, 303) + + thumbnail = videos[0].get('thumbnail', "") + + if thumbnail: + self.storage.store(params, thumbnail, "thumbnail") + + self.common.log("Done", 4) + return (videos, 200) + + def listPlaylist(self, params={}): + self.common.log("", 4) + get = params.get + page = int(get("page", "0")) + per_page = self.pluginsettings.itemsPerPage() + next = 'false' + + videos = self.storage.retrieve(params) + + if page != 0 and videos: + if (per_page * (page + 1) < len(videos)): + next = 'true' + + videos = videos[(per_page * page):(per_page * (page + 1))] + + (result, status) = self.core.getBatchDetailsOverride(videos, params) + else: + result = self.listAll(params) + + if len(result) == 0: + return (result, 303) + + videos = [] + for video in result: + vget = video.get + item = {} + item["playlist_entry_id"] = vget("playlist_entry_id") + item["videoid"] = vget("videoid") + videos.append(item) + + self.storage.store(params, videos) + + thumbnail = result[0].get('thumbnail', "") + if (thumbnail): + self.storage.store(params, thumbnail, "thumbnail") + + if (len(result) > 0 and get("fetch_all") != "true"): + if (per_page * (page + 1) < len(result)): + next = 'true' + + result = result[(per_page * page):(per_page * (page + 1))] + + if next == "true": + self.utils.addNextFolder(result, params) + + self.common.log(repr(result), 4) + return (result, 200) + + def listFolder(self, params={}): + self.common.log("", 4) + get = params.get + result = [] + + if get("store"): + if get("store") == "contact_options": + return self.storage.getUserOptionFolder(params) + else: + return self.storage.getStoredSearches(params) + + page = int(get("page", "0")) + per_page = self.pluginsettings.itemsPerPage() + + if (page != 0): + result = self.storage.retrieve(params) + + elif not get("page"): + if get("feed") == "feed_categories": + result = self.listCategories(params) + else: + result = self.listAll(params) + + if len(result) == 0: + return (result, 303) + + self.storage.store(params, result) + + next = 'false' + + if (len(result) > 0): + if (per_page * (page + 1) < len(result)): + next = 'true' + result = result[(per_page * page):(per_page * (page + 1))] + + if get("user_feed") == "subscriptions": + for item in result: + viewmode = self.storage.retrieve(params, "viewmode", item) + + if (get("external")): + item["external"] = "true" + item["contact"] = get("contact") + + if (viewmode == "favorites"): + item["user_feed"] = "favorites" + item["view_mode"] = "subscriptions_uploads" + elif(viewmode == "playlists"): + item["user_feed"] = "playlists" + item["folder"] = "true" + item["view_mode"] = "subscriptions_playlists" + else: + item["user_feed"] = "uploads" + item["view_mode"] = "subscriptions_favorites" + + if next == "true": + self.utils.addNextFolder(result, params) + + self.common.log(repr(result), 4) + return (result, 200) + + def listCategories(self, params={}): + self.common.log("", 4) + + url = self.createUrl(params) + ytobjects = [] + + result = self.core._fetchPage({"link": url}) + + if result["status"] == 200: + ytobjects = self.core.getCategoriesFolderInfo(result["content"], params) + + if len(ytobjects) == 0: + return ytobjects + + self.common.log(repr(ytobjects), 4) + return ytobjects + + def listAll(self, params={}): + self.common.log("", 4) + get = params.get + result = {"content": "", "status": 303} + + auth = "false" + if get("login") == "true": + auth = "true" + if (not self.core._getAuth()): + self.common.log("login required but auth wasn't set!") + return (self.language(30609), 303) + + feed = self.createUrl(params) + index = 1 + url = feed + "v=2.1&start-index=" + str(index) + "&max-results=" + repr(50) + url = url.replace(" ", "+") + + ytobjects = [] + + result = self.core._fetchPage({"link": url, "auth": auth}) + + if result["status"] == 200: + if get("folder") == "true": + ytobjects = self.core.getFolderInfo(result["content"], params) + else: + ytobjects = self.core.getVideoInfo(result["content"], params) + + if len(ytobjects) == 0: + return ytobjects + + next = ytobjects[len(ytobjects) - 1].get("next", "false") + if next == "true": + ytobjects = ytobjects[:len(ytobjects) - 1] + + while next == "true": + index += 50 + url = feed + "start-index=" + str(index) + "&max-results=" + repr(50) + url = url.replace(" ", "+") + result = self.core._fetchPage({"link": url, "auth": "true"}) + + if result["status"] != 200: + break + temp_objects = [] + if get("folder") == "true": + temp_objects = self.core.getFolderInfo(result["content"], params) + else: + temp_objects = self.core.getVideoInfo(result["content"], params) + + if len(temp_objects) > 0: + next = temp_objects[len(temp_objects) - 1].get("next", "false") + if next == "true": + temp_objects = temp_objects[:len(temp_objects) - 1] + ytobjects += temp_objects + else: + self.common.log("Didn't get any temp_objects. This should NOT happen") + + if get("user_feed"): + if get("user_feed") != "playlist" and get("action") != "play_all": + ytobjects.sort(key=lambda item: item["Title"].lower(), reverse=False) + elif (self.storage.getReversePlaylistOrder(params)): + ytobjects.reverse() + + self.common.log(repr(ytobjects), 4) + return ytobjects
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeLogin.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,354 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import re +import sys +import time +try: import simplejson as json +except ImportError: import json + +# ERRORCODES: +# 0 = Ignore +# 200 = OK +# 303 = See other (returned an error message) +# 500 = uncaught error + + +class YouTubeLogin(): + APIKEY = "AI39si6hWF7uOkKh4B9OEAX-gK337xbwR9Vax-cdeF9CF9iNAcQftT8NVhEXaORRLHAmHxj6GjM-Prw04odK4FxACFfKkiH9lg" + + urls = {} + urls[u"oauth_api_login"] = u"https://accounts.google.com/o/oauth2/auth?client_id=208795275779.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=http%3A%2F%2Fgdata.youtube.com&response_type=code" + + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + + self.pluginsettings = sys.modules["__main__"].pluginsettings + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + + self.utils = sys.modules["__main__"].utils + self.core = sys.modules["__main__"].core + self.common = sys.modules["__main__"].common + + def login(self, params={}): + get = params.get + self.common.log("") + + old_user_name = self.pluginsettings.userName() + old_user_password = self.pluginsettings.userPassword() + self.settings.openSettings() + + user_name = self.pluginsettings.userName() + user_password = self.pluginsettings.userPassword() + + self.dbg = self.pluginsettings.debugModeIsEnabled() + result = "" + status = 500 + + if not user_name: + return (result, 200) + + refreshed = False + if get("new", "false") == "false" and self.pluginsettings.authenticationRefreshRoken and old_user_name == user_name and old_user_password == user_password: + self.common.log("refreshing token: " + str(refreshed)) + refreshed = self.core._oRefreshToken() + + if not refreshed: + result, status = self.authorize() + + self.xbmc.executebuiltin("Container.Refresh") + return (result, status) + + def authorize(self): + self.common.log("token not refresh, or new uname or password") + self.settings.setSetting("oauth2_access_token", "") + self.settings.setSetting("oauth2_refresh_token", "") + self.settings.setSetting("oauth2_expires_at", "") + (result, status) = self._httpLogin({"new": "true"}) + if status == 200: + (result, status) = self._apiLogin() + if status == 200: + self.utils.showErrorMessage(self.language(30031), result, 303) + else: + self.utils.showErrorMessage(self.language(30609), result, status) + return result, status + + def _apiLogin(self): + self.common.log("") + + url = self.urls[u"oauth_api_login"] + + logged_in = False + fetch_options = {"link": url, "no-language-cookie": "true"} + step = 0 + self.common.log("Part A") + while not logged_in and fetch_options and step < 6: + self.common.log("Step : " + str(step)) + step += 1 + + ret = self.core._fetchPage(fetch_options) + fetch_options = False + + newurl = self.common.parseDOM(ret["content"], "form", attrs={"method": "POST"}, ret="action") + state_wrapper = self.common.parseDOM(ret["content"], "input", attrs={"id": "state_wrapper"}, ret="value") + + if len(newurl) > 0 and len(state_wrapper) > 0: + url_data = {"state_wrapper": state_wrapper[0], + "submit_access": "true"} + + fetch_options = {"link": newurl[0].replace("&", "&"), "url_data": url_data, "no-language-cookie": "true"} + self.common.log("Part B") + continue + + code = self.common.parseDOM(ret["content"], "input", attrs={"id": "code"}, ret="value") + if len(code) > 0: + url = "https://accounts.google.com/o/oauth2/token" + url_data = {"client_id": "208795275779.apps.googleusercontent.com", + "client_secret": "sZn1pllhAfyonULAWfoGKCfp", + "code": code[0], + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "grant_type": "authorization_code"} + fetch_options = {"link": url, "url_data": url_data} + self.common.log("Part C") + continue + + # use token + if ret["content"].find("access_token") > -1: + self.common.log("Part D") + oauth = json.loads(ret["content"]) + + if len(oauth) > 0: + self.common.log("Part D " + repr(oauth["expires_in"])) + self.settings.setSetting("oauth2_expires_at", str(int(oauth["expires_in"]) + time.time())) + self.settings.setSetting("oauth2_access_token", oauth["access_token"]) + self.settings.setSetting("oauth2_refresh_token", oauth["refresh_token"]) + + logged_in = True + self.common.log("Done:" + self.settings.getSetting("username")) + + if logged_in: + return (self.language(30030), 200) + else: + self.common.log("Failed") + return (self.language(30609), 303) + + def _httpLogin(self, params={}): + get = params.get + self.common.log("") + status = 500 + + if get("new", "false") == "true" or get("page", "false") != "false": + self.settings.setSetting("login_info", "") + self.settings.setSetting("SID", "") + self.settings.setSetting("login_cookies", "") + elif self.settings.getSetting("login_info") != "": + self.common.log("returning existing login info: " + self.settings.getSetting("login_info")) + return (self.settings.getSetting("login_info"), 200) + + fetch_options = {"link": get("link", "http://www.youtube.com/")} + + step = 0 + galx = "" + ret = {} + + while fetch_options and step < 18: # 6 steps for 2-factor login + self.common.log("Step : " + str(step)) + step += 1 + + if step == 17: + return (self.core._findErrors(ret), 303) + + ret = self.core._fetchPage(fetch_options) + + if ret["content"].find(" captcha") > -1: + self.common.log("Captcha needs to be filled") + break + fetch_options = False + + # Check if we are logged in. + nick = self.common.parseDOM(ret["content"], "p", attrs={"class": "masthead-expanded-acct-sw-id2"}) + + # Check if there are any errors to report + errors = self.core._findErrors(ret, silent=True) + if errors: + if errors.find("cookie-clear-message-1") == -1 and (errors.find("The code you entered didn") == -1 or (errors.find("The code you entered didn") > -1 and step > 12)): + self.common.log("Returning error: " + repr(errors)) + return (errors, 303) + + if len(nick) > 0 and nick[0] != "Sign In": + self.common.log("Logged in. Parsing data: " + repr(nick)) + status = self._getLoginInfo(nick) + return(ret, status) + + # Click login link on youtube.com + newurl = self.common.parseDOM(ret["content"], "button", attrs={"href": ".*?ServiceLogin.*?"}, ret="href") + if len(newurl) > 0: + # Start login procedure + if newurl[0] != "#": + fetch_options = {"link": newurl[0].replace("&", "&"), "referer": ret["location"]} + self.common.log("Part A : " + repr(fetch_options)) + + # Fill out login information and send. + newurl = self.common.parseDOM(ret["content"].replace("\n", " "), "form", attrs={"id": "gaia_loginform"}, ret="action") + if len(newurl) > 0: + (galx, url_data) = self._fillLoginInfo(ret["content"]) + if len(galx) > 0 and len(url_data) > 0: + fetch_options = {"link": newurl[0], "no-language-cookie": "true", "url_data": url_data, "hidden": "true", "referer": ret["location"]} + self.common.log("Part B") + self.common.log("fetch options: " + repr(fetch_options), 10) # WARNING, SHOWS LOGIN INFO/PASSWORD + continue + + newurl = self.common.parseDOM(ret["content"], "meta", attrs={"http-equiv": "refresh"}, ret="content") + if len(newurl) > 0: + newurl = newurl[0].replace("&", "&") + newurl = newurl[newurl.find("'") + 5:newurl.rfind("'")] + fetch_options = {"link": newurl, "no-language-cookie": "true", "referer": ret["location"]} + self.common.log("Part C: " + repr(fetch_options)) + continue + + ## 2-factor login start + if ret["content"].find("smsUserPin") > -1: + url_data = self._fillUserPin(ret["content"]) + if len(url_data) == 0: + return (False, 500) + + new_part = self.common.parseDOM(ret["content"], "form", attrs={"name": "verifyForm"}, ret="action") + fetch_options = {"link": new_part[0], "url_data": url_data, "no-language-cookie": "true", "referer": ret["location"]} + + self.common.log("Part D: " + repr(fetch_options)) + continue + + smsToken = self.common.parseDOM(ret["content"].replace("\n", ""), "input", attrs={"name": "smsToken"}, ret="value") + + if len(smsToken) > 0 and galx != "": + url_data = {"smsToken": smsToken[0], + "PersistentCookie": "yes", + "service": "youtube", + "GALX": galx} + + target_url = self.common.parseDOM(ret["content"], "form", attrs={"name": "hiddenpost"}, ret="action") + fetch_options = {"link": target_url[0], "url_data": url_data, "no-language-cookie": "true", "referer": ret["location"]} + self.common.log("Part E: " + repr(fetch_options)) + continue + + ## 2-factor login finish + if not fetch_options: + # Check for errors. + return (self.core._findErrors(ret), 303) + + return (ret, status) + + def _fillLoginInfo(self, content): + rmShown = self.common.parseDOM(content, "input", attrs={"name": "rmShown"}, ret="value") + cont = self.common.parseDOM(content, "input", attrs={"name": "continue"}, ret="value") + uilel = self.common.parseDOM(content, "input", attrs={"name": "uilel"}, ret="value") + if len(uilel) == 0: + uilel = self.common.parseDOM(content, "input", attrs= {"id":"uilel"}, ret="value") + dsh = self.common.parseDOM(content, "input", attrs={"name": "dsh"}, ret="value") + if len(dsh) == 0: + dsh = self.common.parseDOM(content, "input", attrs={"id": "dsh"}, ret="value") + + galx = self.common.parseDOM(content, "input", attrs={"name": "GALX"}, ret="value") + uname = self.pluginsettings.userName() + pword = self.pluginsettings.userPassword() + + if pword == "": + pword = self.common.getUserInput(self.language(30628), hidden=True) + + if len(galx) == 0 or len(cont) == 0 or len(uilel) == 0 or len(dsh) == 0 or len(rmShown) == 0 or uname == "" or pword == "": + self.common.log("_fillLoginInfo missing values for login form " + repr(galx) + repr(cont) + repr(uilel) + repr(dsh) + repr(rmShown) + repr(uname) + str(len(pword))) + return ("", {}) + else: + galx = galx[0] + url_data = {"pstMsg": "0", + "ltmpl": "sso", + "dnConn": "", + "continue": cont[0], + "service": "youtube", + "uilel": uilel[0], + "dsh": dsh[0], + "hl": "en_US", + "timeStmp": "", + "secTok": "", + "GALX": galx, + "Email": uname, + "Passwd": pword, + "PersistentCookie": "yes", + "rmShown": rmShown[0], + "signin": "Sign in", + "asts": "" + } + return (galx, url_data) + + def _fillUserPin(self, content): + self.common.log(repr(content), 5) + smsToken = self.common.parseDOM(content, "input", attrs={"name": "smsToken"}, ret="value") + self.smsToken = smsToken + userpin = self.common.getUserInputNumbers(self.language(30627)) + + if len(userpin) > 0: + url_data = {"smsToken": smsToken[0], + "PersistentCookie": "yes", + "smsUserPin": userpin, + "smsVerifyPin": "Verify", + "timeStmp": "", + "secTok": ""} + self.common.log("Done: " + repr(url_data)) + return url_data + else: + self.common.log("Replace this with a message telling users that they didn't enter a pin") + return {} + + def _getLoginInfo(self, nick): + self.common.log(nick) + status = 303 + + # Save cookiefile in settings + cookies = self.common.getCookieInfoAsHTML() + login_info = self.common.parseDOM(cookies, "cookie", attrs={"name": "LOGIN_INFO"}, ret="value") + SID = self.common.parseDOM(cookies, "cookie", attrs={"name": "SID", "domain": ".youtube.com"}, ret="value") + scookies = {} + self.common.log("COOKIES:" + repr(cookies)) + tnames = re.compile(" name='(.*?)' ").findall(cookies) + for key in tnames: + tval = self.common.parseDOM(cookies, "cookie", attrs={"name": key}, ret="value") + if len(tval) > 0: + scookies[key] = tval[0] + self.common.log("COOKIES:" + repr(scookies)) + + if len(login_info) == 1: + self.common.log("LOGIN_INFO: " + repr(login_info)) + self.settings.setSetting("login_info", login_info[0]) + else: + self.common.log("Failed to get LOGIN_INFO from youtube: " + repr(login_info)) + + if len(SID) == 1: + self.common.log("SID: " + repr(SID)) + self.settings.setSetting("SID", SID[0]) + else: + self.common.log("Failed to get SID from youtube: " + repr(SID)) + + if len(SID) == 1 and len(login_info) == 1: + status = 200 + self.settings.setSetting("login_cookies", repr(scookies)) + + self.common.log("Done") + return status
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeNavigation.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,712 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib + +# Dorfelite +try: + import json +except: + import simplejson as json + +from threading import Thread + +# Dorfelite +DORFHOST = "http://dorfelite.net:31337" + +class YouTubeNavigation(): + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + self.xbmcgui = sys.modules["__main__"].xbmcgui + self.xbmcplugin = sys.modules["__main__"].xbmcplugin + + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + + self.utils = sys.modules["__main__"].utils + self.core = sys.modules["__main__"].core + self.common = sys.modules["__main__"].common + self.cache = sys.modules["__main__"].cache + + self.pluginsettings = sys.modules["__main__"].pluginsettings + self.playlist = sys.modules["__main__"].playlist + self.login = sys.modules["__main__"].login + self.feeds = sys.modules["__main__"].feeds + self.player = sys.modules["__main__"].player + self.downloader = sys.modules["__main__"].downloader + self.storage = sys.modules["__main__"].storage + self.scraper = sys.modules["__main__"].scraper + self.subtitles = sys.modules["__main__"].subtitles + + # This list contains the main menu structure the user first encounters when running the plugin + # label , path , thumbnail , login , feed / action + self.categories = ( + {'Title':self.language(30044) ,'path':"/root/explore" , 'thumbnail':"explore" , 'login':"false" }, + {'Title':self.language(30041) ,'path':"/root/explore/categories" , 'thumbnail':"explore" , 'login':"false" , 'feed':'feed_categories', 'folder':'true'}, + {'Title':self.language(30001) ,'path':"/root/explore/feeds" , 'thumbnail':"feeds" , 'login':"false" }, + {'Title':self.language(30009) ,'path':"/root/explore/feeds/discussed" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_discussed" }, + {'Title':self.language(30010) ,'path':"/root/explore/feeds/linked" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_linked" }, + {'Title':self.language(30011) ,'path':"/root/explore/feeds/viewed" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_viewed" }, + {'Title':self.language(30012) ,'path':"/root/explore/feeds/recent" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_recent" }, + {'Title':self.language(30013) ,'path':"/root/explore/feeds/responded" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_responded" }, + {'Title':self.language(30050) ,'path':"/root/explore/feeds/shared" , 'thumbnail':"most" , 'login':"false" , 'feed':"feed_shared" }, + {'Title':self.language(30014) ,'path':"/root/explore/feeds/featured" , 'thumbnail':"featured" , 'login':"false" , 'feed':"feed_featured" }, + {'Title':self.language(30049) ,'path':"/root/explore/feeds/trending" , 'thumbnail':"featured" , 'login':"false" , 'feed':"feed_trending" }, + {'Title':self.language(30015) ,'path':"/root/explore/feeds/favorites" , 'thumbnail':"top" , 'login':"false" , 'feed':"feed_favorites" }, + {'Title':self.language(30016) ,'path':"/root/explore/feeds/rated" , 'thumbnail':"top" , 'login':"false" , 'feed':"feed_rated" }, + {'Title':self.language(30052) ,'path':"/root/explore/music" , 'thumbnail':"music" , 'login':"false" , 'store':"disco_searches", "folder":"true" }, + {'Title':self.language(30040) ,'path':"/root/explore/music/new" , 'thumbnail':"search" , 'login':"false" , 'scraper':"search_disco"}, + {'Title':self.language(30055) ,'path':"/root/explore/music/top100" , 'thumbnail':"music" , 'login':"false" , 'scraper':'music_top100'}, + {'Title':self.language(30032) ,'path':"/root/explore/trailers" , 'thumbnail':"trailers" , 'login':"false" , 'scraper':'trailers'}, + {'Title':self.language(30051) ,'path':"/root/explore/live" , 'thumbnail':"live" , 'login':"false" , 'feed':"feed_live" }, + {'Title':self.language(30019) ,'path':"/root/recommended" , 'thumbnail':"recommended" , 'login':"true" , 'user_feed':"recommended" }, + {'Title':self.language(30008) ,'path':"/root/watch_later" , 'thumbnail':"watch_later" , 'login':"true" , 'user_feed':"watch_later" }, + {'Title':self.language(30056) ,'path':"/root/liked" , 'thumbnail':"liked" , 'login':"true" , 'scraper':"liked_videos" }, + {'Title':self.language(30059) ,'path':"/root/history" , 'thumbnail':"history" , 'login':"true" , 'user_feed':"watch_history" }, + {'Title':self.language(30018) ,'path':"/root/contacts" , 'thumbnail':"contacts" , 'login':"true" , 'user_feed':"contacts", 'folder':'true' }, + {'Title':self.language(30024) ,'path':"/root/contacts/new" , 'thumbnail':"contacts" , 'login':"true" , 'action':"add_contact"}, + {'Title':self.language(30002) ,'path':"/root/favorites" , 'thumbnail':"favorites" , 'login':"true" , 'user_feed':"favorites" }, + {'Title':self.language(30017) ,'path':"/root/playlists" , 'thumbnail':"playlists" , 'login':"true" , 'user_feed':"playlists", 'folder':'true' }, + {'Title':self.language(30003) ,'path':"/root/subscriptions" , 'thumbnail':"subscriptions" , 'login':"true" , 'user_feed':"subscriptions", 'folder':'true' }, + {'Title':self.language(30004) ,'path':"/root/subscriptions/new" , 'thumbnail':"newsubscriptions" , 'login':"true" , 'user_feed':"newsubscriptions" }, + {'Title':self.language(30005) ,'path':"/root/uploads" , 'thumbnail':"uploads" , 'login':"true" , 'user_feed':"uploads" }, + {'Title':self.language(30045) ,'path':"/root/downloads" , 'thumbnail':"downloads" , 'login':"false" , 'feed':"downloads" }, + {'Title':self.language(30006) ,'path':"/root/search" , 'thumbnail':"search" , 'login':"false" , 'store':"searches", 'folder':'true' }, + {'Title':self.language(30007) ,'path':"/root/search/new" , 'thumbnail':"search" , 'login':"false" , 'feed':"search" }, + {'Title':self.language(30027) ,'path':"/root/login" , 'thumbnail':"login" , 'login':"false" , 'action':"settings" }, + {'Title':self.language(30028) ,'path':"/root/settings" , 'thumbnail':"settings" , 'login':"true" , 'action':"settings" }, + + # Dorfelite + {'Title':self.language(31337) ,'path':"/root/dorfelite" , 'thumbnail':"discoball" , 'login':"false" , 'action':"dorfelite" } + ) + + #==================================== Main Entry Points=========================================== + def listMenu(self, params={}): + self.common.log(repr(params), 5) + get = params.get + cache = True + + path = get("path", "/root") + if get("feed") not in ["search", "related"] and not get("channel") and not get("contact") and not get("playlist") and get("page", "0") == "0" and get("scraper") not in ["search_disco", "music_artist"]: + for category in self.categories: + cat_get = category.get + if (cat_get("path").find(path + "/") > - 1): + if (cat_get("path").rfind("/") <= len(path + "/")): + setting = self.settings.getSetting(cat_get("path").replace("/root/explore/", "").replace("/root/", "")) + if not setting or setting == "true": + if (cat_get("feed") == "downloads"): + if (self.settings.getSetting("downloadPath")): + self.addListItem(params, category) + else: + self.addListItem(params, category) + + if (get("feed") or get("user_feed") or get("options") or get("store") or get("scraper")): + return self.list(params) + + video_view = self.settings.getSetting("list_view") == "1" + + if (video_view): + self.xbmc.executebuiltin("Container.SetViewMode(500)") + + self.xbmcplugin.endOfDirectory(handle=int(sys.argv[1]), succeeded=True, cacheToDisc=cache) + self.common.log("Done", 5) + + def executeAction(self, params={}): + self.common.log(params, 3) + get = params.get + if (get("action") == "settings"): + self.login.login(params) + if (get("action") in ["delete_search", "delete_disco"]): + self.storage.deleteStoredSearch(params) + if (get("action") in ["edit_search", "edit_disco"]): + self.storage.editStoredSearch(params) + self.listMenu(params) + if (get("action") == "remove_favorite"): + self.removeFromFavorites(params) + if (get("action") == "add_favorite"): + self.addToFavorites(params) + if (get("action") == "remove_contact"): + self.removeContact(params) + if (get("action") == "add_contact"): + self.addContact(params) + if (get("action") == "remove_subscription"): + self.removeSubscription(params) + if (get("action") == "add_subscription"): + self.addSubscription(params) + if (get("action") == "download"): + self.downloadVideo(params) + if (get("action") == "play_video"): + self.player.playVideo(params) + if (get("action") == "queue_video"): + self.playlist.queueVideo(params) + if (get("action") == "change_subscription_view"): + self.storage.changeSubscriptionView(params) + self.list(params) + if (get("action") == "play_all"): + self.playlist.playAll(params) + if (get("action") == "add_to_playlist"): + self.playlist.addToPlaylist(params) + if (get("action") == "remove_from_playlist"): + self.playlist.removeFromPlaylist(params) + if (get("action") == "delete_playlist"): + self.playlist.deletePlaylist(params) + if (get("action") == "reverse_order"): + self.storage.reversePlaylistOrder(params) + if (get("action") == "create_playlist"): + self.playlist.createPlaylist(params) + + # Dorfelite + if "dorfelite" == get("action"): + self.listDorfelite(params) + + self.common.log("Done", 5) + + #==================================== Item Building and Listing =========================================== + def list(self, params={}): + self.common.log("", 5) + get = params.get + results = [] + if (get("feed") == "search" or get("scraper") == "search_disco"): + if not get("search"): + query = self.common.getUserInput(self.language(30006), '') + if not query: + return False + params["search"] = query + if get("scraper") == "search_disco": + params["store"] = "disco_searches" + self.storage.saveStoredSearch(params) + if get("scraper") == "search_disco": + del params["store"] + + if get("scraper"): + (results, status) = self.scraper.scrape(params) + elif get("store"): + (results, status) = self.storage.list(params) + self.common.log("store returned " + repr(results)) + else: + (results, status) = self.feeds.list(params) + + if status == 200: + if get("folder", "false") != "false": + self.parseFolderList(params, results) + else: + self.parseVideoList(params, results) + self.common.log("Done", 5) + return True + else: + self.showListingError(params) + self.common.log("Error") + return False + + def showListingError(self, params={}): + self.common.log(repr(params), 5) + get = params.get + label = "" + if get("external"): + categories = self.storage.user_options + else: + categories = self.categories + + for category in categories: + cat_get = category.get + if ( + (get("feed") and cat_get("feed") == get("feed")) or + (get("user_feed") and cat_get("user_feed") == get("user_feed")) or + (get("scraper") and cat_get("scraper") == get("scraper")) + ): + label = cat_get("Title") + + if get("channel"): + label = get("channel") + if get("playlist"): + label = self.language(30615) + if label: + self.utils.showMessage(label, self.language(30601)) + self.common.log("Done", 5) + + #================================== Plugin Actions ========================================= + def downloadVideo(self, params): + get = params.get + self.common.log(repr(params)) + if not self.settings.getSetting("download_path"): + self.common.log("Download path missing. Opening settings") + self.utils.showMessage(self.language(30600), self.language(30611)) + self.settings.openSettings() + + download_path = self.settings.getSetting("download_path") + if not download_path: + return + + self.common.log("path: " + repr(download_path)) + (video, status) = self.player.buildVideoObject(params) + + if "video_url" in video and download_path: + params["Title"] = video['Title'] + params["url"] = video['video_url'] + params["download_path"] = download_path + filename = "%s-[%s].mp4" % (''.join(c for c in video['Title'] if c not in self.utils.INVALID_CHARS), video["videoid"]) + + self.subtitles.downloadSubtitle(video) + if get("async"): + self.downloader.download(filename, params, async=False) + else: + self.downloader.download(filename, params) + else: + if "apierror" in video: + self.utils.showMessage(self.language(30625), video["apierror"]) + else: + self.utils.showMessage(self.language(30625), "ERROR") + + def addToFavorites(self, params={}): + self.common.log("", 5) + get = params.get + if (get("videoid")): + (message, status) = self.core.add_favorite(params) + if status != 200: + self.utils.showErrorMessage(self.language(30020), message, status) + self.common.log("Error", 5) + return False + self.common.log("Done", 5) + return True + + def removeFromFavorites(self, params={}): + self.common.log("", 5) + get = params.get + + if (get("editid")): + (message, status) = self.core.delete_favorite(params) + if status != 200: + self.utils.showErrorMessage(self.language(30020), message, status) + return False + self.xbmc.executebuiltin("Container.Refresh") + + self.common.log("Done", 5) + return True + + def addContact(self, params={}): + self.common.log("", 5) + get = params.get + + if not get("contact"): + contact = self.common.getUserInput(self.language(30519), '') + params["contact"] = contact + + if (get("contact")): + (result, status) = self.core.add_contact(params) + if status != 200: + self.utils.showErrorMessage(self.language(30029), result, status) + return False + self.utils.showMessage(self.language(30613), get("contact")) + self.xbmc.executebuiltin("Container.Refresh") + + self.common.log("Done", 5) + return True + + def removeContact(self, params={}): + self.common.log("", 5) + get = params.get + + if (get("contact")): + (result, status) = self.core.remove_contact(params) + if status != 200: + self.utils.showErrorMessage(self.language(30029), result, status) + return False + + self.utils.showMessage(self.language(30614), get("contact")) + self.xbmc.executebuiltin("Container.Refresh") + return True + + def addSubscription(self, params={}): + self.common.log("", 5) + get = params.get + if (get("channel")): + (message, status) = self.core.add_subscription(params) + if status != 200: + self.utils.showErrorMessage(self.language(30021), message, status) + return False + self.common.log("Done", 5) + return True + + def removeSubscription(self, params={}): + self.common.log("", 5) + get = params.get + if (get("editid")): + (message, status) = self.core.remove_subscription(params) + if status != 200: + self.utils.showErrorMessage(self.language(30021), message, status) + return False + + self.xbmc.executebuiltin("Container.Refresh") + self.common.log("Done", 5) + return True + + #================================== List Item manipulation ========================================= + # is only used by List Menu + def addListItem(self, params={}, item_params={}): + self.common.log("", 5) + item = item_params.get + + if (not item("action")): + if (item("login") == "false"): + self.addFolderListItem(params, item_params) + else: + if (len(self.settings.getSetting("oauth2_access_token")) > 0): + self.addFolderListItem(params, item_params) + else: + if (item("action") == "settings"): + if (len(self.settings.getSetting("oauth2_access_token")) > 0): + if (item("login") == "true"): + self.addActionListItem(params, item_params) + else: + if (item("login") == "false"): + self.addActionListItem(params, item_params) + elif (item("action") == "play_video"): + self.addVideoListItem(params, item_params, 0) + else: + self.addActionListItem(params, item_params) + self.common.log("Done", 5) + + # common function for adding folder items + def addFolderListItem(self, params={}, item_params={}, size=0): + self.common.log("", 5) + item = item_params.get + + icon = "DefaultFolder.png" + if item("icon"): + icon = self.utils.getThumbnail(item("icon")) + + thumbnail = item("thumbnail") + + cm = self.addFolderContextMenuItems(params, item_params) + + if (item("thumbnail", "DefaultFolder.png").find("http://") == - 1): + thumbnail = self.utils.getThumbnail(item("thumbnail")) + + listitem = self.xbmcgui.ListItem(item("Title"), iconImage=icon, thumbnailImage=thumbnail) + url = '%s?path=%s&' % (sys.argv[0], item("path")) + url = self.utils.buildItemUrl(item_params, url) + + if len(cm) > 0: + listitem.addContextMenuItems(cm, replaceItems=False) + + listitem.setProperty("Folder", "true") + if (item("feed") == "downloads"): + url = self.settings.getSetting("downloadPath") + self.xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=listitem, isFolder=True, totalItems=size) + self.common.log("Done", 5) + + # common function for adding action items + def addActionListItem(self, params={}, item_params={}, size=0): + self.common.log("", 5) + item = item_params.get + folder = True + icon = "DefaultFolder.png" + thumbnail = self.utils.getThumbnail(item("thumbnail")) + listitem = self.xbmcgui.ListItem(item("Title"), iconImage=icon, thumbnailImage=thumbnail) + + if (item("action") == "playbyid"): + folder = False + listitem.setProperty('IsPlayable', 'true') + + url = '%s?path=%s&' % (sys.argv[0], item("path")) + url += 'action=' + item("action") + '&' + + self.xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=listitem, isFolder=folder, totalItems=size) + self.common.log("Done", 5) + + # common function for adding video items + def addVideoListItem(self, params={}, item_params={}, listSize=0): + self.common.log("", 5) + get = params.get + item = item_params.get + + icon = item("icon", "default") + if(get("scraper", "").find("music") > -1): + icon = "music" + elif(get("scraper", "").find("disco") > -1): + icon = "discoball" + elif(get("feed", "").find("live") > -1): + icon = "live" + + icon = self.utils.getThumbnail(icon) + + listitem = self.xbmcgui.ListItem(item("Title"), iconImage=icon, thumbnailImage=item("thumbnail")) + + url = '%s?path=%s&action=play_video&videoid=%s' % (sys.argv[0], "/root/video", item("videoid")) + + if get("user_feed") == "watch_later": + url += "&watch_later=true&playlist_entry_id=%s&" % item("playlist_entry_id") + + cm = self.addVideoContextMenuItems(params, item_params) + + listitem.addContextMenuItems(cm, replaceItems=True) + + listitem.setProperty("Video", "true") + listitem.setProperty("IsPlayable", "true") + listitem.setInfo(type='Video', infoLabels=item_params) + self.xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=listitem, isFolder=False, totalItems=listSize + 1) + self.common.log("Done", 5) + + #==================================== Core Output Parsing Functions =========================================== + + # Parses a folder list consisting of a tuple of dictionaries + def parseFolderList(self, params, results): + self.common.log("", 5) + listSize = len(results) + get = params.get + + cache = True + if get("store") or get("user_feed"): + cache = False + + for result_params in results: + result_params["path"] = get("path") + self.addFolderListItem(params, result_params, listSize + 1) + + self.xbmcplugin.endOfDirectory(handle=int(sys.argv[1]), succeeded=True, cacheToDisc=cache) + self.common.log("Done", 5) + + # parses a video list consisting of a tuple of dictionaries + def parseVideoList(self, params, results): + self.common.log("", 5) + listSize = len(results) + get = params.get + + for result_params in results: + result_params["path"] = get("path") + result = result_params.get + + if result("videoid") == "false": + continue + + if get("scraper") == "watch_later": + result_params["index"] = str(results.index(result_params) + 1) + + if result("next") == "true": + self.addFolderListItem(params, result_params, listSize) + else: + self.addVideoListItem(params, result_params, listSize) + + video_view = int(self.settings.getSetting("list_view")) <= 1 + if (video_view): + self.xbmc.executebuiltin("Container.SetViewMode(500)") + + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_UNSORTED) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_LABEL) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_VIDEO_RATING) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_DATE) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_PROGRAM_COUNT) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + self.xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=self.xbmcplugin.SORT_METHOD_GENRE) + + self.xbmcplugin.endOfDirectory(handle=int(sys.argv[1]), succeeded=True, cacheToDisc=True) + self.common.log("Done", 5) + + def addVideoContextMenuItems(self, params={}, item_params={}): + self.common.log("", 5) + cm = [] + get = params.get + item = item_params.get + + title = self.common.makeAscii(item("Title")) + url_title = urllib.quote_plus(title) + studio = self.common.makeAscii(item("Studio", "Unknown Author")) + + # FIXME: WTF? + try: + print(studio) + except: + studio = "Dorfelite" + + url_studio = urllib.quote_plus(studio) + + cm.append((self.language(30504), "XBMC.Action(Queue)",)) + + if (get("playlist")): + cm.append((self.language(30521), "XBMC.RunPlugin(%s?path=%s&action=play_all&playlist=%s&videoid=%s&)" % (sys.argv[0], item("path"), get("playlist"), item("videoid")))) + + if (get("user_feed") == "newsubscriptions" or get("user_feed") == "favorites"): + contact = "default" + if get("contact"): + contact = get("contact") + cm.append((self.language(30521), "XBMC.RunPlugin(%s?path=%s&action=play_all&user_feed=%s&contact=%s&videoid=%s&)" % (sys.argv[0], item("path"), get("user_feed"), contact, item("videoid")))) + + cm.append((self.language(30501), "XBMC.RunPlugin(%s?path=%s&action=download&videoid=%s)" % (sys.argv[0], item("path"), item("videoid")))) + + if (self.settings.getSetting("username") != "" and self.settings.getSetting("oauth2_access_token")): + if (get("user_feed") == "favorites" and not get("contact")): + cm.append((self.language(30506), 'XBMC.RunPlugin(%s?path=%s&action=remove_favorite&editid=%s&)' % (sys.argv[0], item("path"), item("editid")))) + else: + cm.append((self.language(30503), 'XBMC.RunPlugin(%s?path=%s&action=add_favorite&videoid=%s&)' % (sys.argv[0], item("path"), item("videoid")))) + + if (get("external") == "true" or (get("feed") not in ["subscriptions_favorites", "subscriptions_uploads", "subscriptions_playlists"] and (get("user_feed") != "uploads" and not get("external")))): + cm.append((self.language(30512) % studio, 'XBMC.RunPlugin(%s?path=%s&channel=%s&action=add_subscription)' % (sys.argv[0], item("path"), url_studio))) + + if (get("playlist") and item("playlist_entry_id")): + cm.append((self.language(30530), "XBMC.RunPlugin(%s?path=%s&action=remove_from_playlist&playlist=%s&playlist_entry_id=%s&)" % (sys.argv[0], item("path"), get("playlist"), item("playlist_entry_id")))) + cm.append((self.language(30528), "XBMC.RunPlugin(%s?path=%s&action=add_to_playlist&videoid=%s&)" % (sys.argv[0], item("path"), item("videoid")))) + + if (get("feed") != "uploads" and get("user_feed") != "uploads"): + cm.append((self.language(30516) % studio, "XBMC.Container.Update(%s?path=%s&feed=uploads&channel=%s)" % (sys.argv[0], get("path"), url_studio))) + + cm.append((self.language(30514), "XBMC.Container.Update(%s?path=%s&feed=search&search=%s)" % (sys.argv[0], get("path"), url_title))) + cm.append((self.language(30527), "XBMC.Container.Update(%s?path=%s&feed=related&videoid=%s)" % (sys.argv[0], get("path"), item("videoid")))) + cm.append((self.language(30523), "XBMC.ActivateWindow(VideoPlaylist)")) + cm.append((self.language(30502), "XBMC.Action(Info)",)) + + self.common.log("Done", 5) + return cm + + def addFolderContextMenuItems(self, params={}, item_params={}): + self.common.log("", 5) + cm = [] + get = params.get + item = item_params.get + + if (item("next", "false") == "true"): + return cm + + if (item("user_feed") in ["favorites", "newsubscriptions", "watch_later", "recommended"]): + cm.append((self.language(30520), "XBMC.RunPlugin(%s?path=%s&action=play_all&user_feed=%s&contact=%s&login=true&)" % (sys.argv[0], item("path"), item("user_feed"), "default"))) + cm.append((self.language(30522), "XBMC.RunPlugin(%s?path=%s&action=play_all&shuffle=true&user_feed=%s&contact=%s&login=true&)" % (sys.argv[0], item("path"), item("user_feed"), "default"))) + + if item("scraper") in ["liked_videos"]: + cm.append((self.language(30520), "XBMC.RunPlugin(%s?path=%s&action=play_all&scraper=%s&login=true&)" % (sys.argv[0], item("path"), item("scraper")))) + cm.append((self.language(30522), "XBMC.RunPlugin(%s?path=%s&action=play_all&shuffle=true&scraper=%s&login=true&)" % (sys.argv[0], item("path"), item("scraper")))) + + if (item("playlist")): + cm.append((self.language(30531), "XBMC.RunPlugin(%s?path=%s&action=reverse_order&playlist=%s&)" % (sys.argv[0], item("path"), item("playlist")))) + cm.append((self.language(30520), "XBMC.RunPlugin(%s?path=%s&action=play_all&user_feed=playlist&playlist=%s&)" % (sys.argv[0], item("path"), item("playlist")))) + cm.append((self.language(30522), "XBMC.RunPlugin(%s?path=%s&action=play_all&user_feed=playlist&shuffle=true&playlist=%s&)" % (sys.argv[0], item("path"), item("playlist")))) + if not get("external"): + cm.append((self.language(30539), "XBMC.RunPlugin(%s?path=%s&action=delete_playlist&playlist=%s&)" % (sys.argv[0], item("path"), item("playlist")))) + + if (item("scraper") == "music_top100"): + cm.append((self.language(30520), "XBMC.RunPlugin(%s?path=%s&action=play_all&scraper=music_top100&)" % (sys.argv[0], item("path")))) + cm.append((self.language(30522), "XBMC.RunPlugin(%s?path=%s&action=play_all&shuffle=true&scraper=music_top100&)" % (sys.argv[0], item("path")))) + + if (item("scraper") == "search_disco"): + cm.append((self.language(30520), "XBMC.RunPlugin(%s?path=%s&action=play_all&scraper=search_disco&search=%s&)" % (sys.argv[0], item("path"), item("search")))) + cm.append((self.language(30522), "XBMC.RunPlugin(%s?path=%s&action=play_all&shuffle=true&scraper=search_disco&search=%s&)" % (sys.argv[0], item("path"), item("search")))) + if not get("scraper") == "disco_top_artist": + cm.append((self.language(30524), 'XBMC.Container.Update(%s?path=%s&action=edit_disco&store=disco_searches&search=%s&)' % (sys.argv[0], item("path"), item("search")))) + cm.append((self.language(30525), 'XBMC.RunPlugin(%s?path=%s&action=delete_disco&store=disco_searches&delete=%s&)' % (sys.argv[0], item("path"), item("search")))) + + if (item("feed") == "search"): + cm.append((self.language(30515), 'XBMC.Container.Update(%s?path=%s&action=edit_search&store=searches&search=%s&)' % (sys.argv[0], item("path"), item("search")))) + cm.append((self.language(30508), 'XBMC.RunPlugin(%s?path=%s&action=delete_search&store=searches&delete=%s&)' % (sys.argv[0], item("path"), item("search")))) + + if (item("view_mode")): + cm_url = 'XBMC.Container.Update(%s?path=%s&channel=%s&action=change_subscription_view&view_mode=%s&' % (sys.argv[0], item("path"), item("channel"), "%s") + if (item("external")): + cm_url += "external=true&contact=" + get("contact") + "&" + cm_url += ")" + + if (item("user_feed") == "favorites"): + cm.append((self.language(30511), cm_url % ("uploads"))) + cm.append((self.language(30526), cm_url % ("playlists&folder=true"))) + elif(item("user_feed") == "playlists"): + cm.append((self.language(30511), cm_url % ("uploads"))) + cm.append((self.language(30510), cm_url % ("favorites"))) + elif (item("user_feed") == "uploads"): + cm.append((self.language(30510), cm_url % ("favorites"))) + cm.append((self.language(30526), cm_url % ("playlists&folder=true"))) + + if (item("channel") or item("contact")): + if (self.settings.getSetting("username") != "" and self.settings.getSetting("oauth2_access_token")): + title = self.common.makeAscii(item("channel", "")) + if (get("external")): + channel = get("channel", "") + if not channel: + channel = get("contact") + cm.append((self.language(30512) % title, 'XBMC.RunPlugin(%s?path=%s&channel=%s&action=add_subscription)' % (sys.argv[0], item("path"), channel))) + elif item("editid"): + cm.append((self.language(30513) % title, 'XBMC.RunPlugin(%s?path=%s&editid=%s&action=remove_subscription)' % (sys.argv[0], item("path"), item("editid")))) + + if (item("contact") and not get("store")): + if (self.pluginsettings.userHasProvidedValidCredentials()): + if (item("external")): + cm.append((self.language(30026), 'XBMC.RunPlugin(%s?path=%s&action=add_contact&contact=%s&)' % (sys.argv[0], item("path"), item("Title")))) + else: + cm.append((self.language(30025), 'XBMC.RunPlugin(%s?path=%s&action=remove_contact&contact=%s&)' % (sys.argv[0], item("path"), item("Title")))) + + cm.append((self.language(30523), "XBMC.ActivateWindow(VideoPlaylist)")) + self.common.log("Done", 5) + return cm + + #==================================== Dorfelite =========================================== + + ## saveDorfelite {{{ + # Post new entry to server + # @param self Stupid.. + # @param params Passed parameters + ## + + def saveDorfelite(self, params): + # Get video info + (info, status) = player.getInfo(params) + + values = urllib.urlencode({ "videoid": params["videoid"], "Title": info["Title"] }) + con = urllib.urlopen(DORFHOST, values) + result = con.read() #< Ignore reply + con.close() + # }}} + + ## playDorfelite {{{ + # Post new entry to server and play vid + # @param self Stupid.. + # @param params Passed parameters + ## + + def playDorfelite(self, params = {}): + t = Thread(target = self.saveDorfelite, args = (params,)) + t.start() + + player.playVideo(params) + + return True + # }}} + + ## listDorfelite {{{ + # Fetch list from server and build list + # @param self Stupid.. + # @param params Passed parameters + ## + + def listDorfelite(self, params = {}): + con = urllib.urlopen(DORFHOST) + result = con.read() + con.close() + + # Hack? + params["path"] = "/root/dorfelite" + + # Prepare list + videos = json.loads(result) + + for idx, v in enumerate(videos): + videos[idx]["video_url"] = "http://www.youtube.com/watch?v=%s&safeSearch=none" % videos[idx]["videoid"] + videos[idx]["Studio"] = "Dorfelite" + + self.parseVideoList(params, videos) + + return True + # }}}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubePlayer.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,423 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib +import cgi +try: import simplejson as json +except ImportError: import json + +from threading import Thread + +# Dorfelite +DORFHOST = "http://dorfelite.net:31337" + +class YouTubePlayer(): + fmt_value = { + 5: "240p h263 flv container", + 18: "360p h264 mp4 container | 270 for rtmpe?", + 22: "720p h264 mp4 container", + 26: "???", + 33: "???", + 34: "360p h264 flv container", + 35: "480p h264 flv container", + 37: "1080p h264 mp4 container", + 38: "720p vp8 webm container", + 43: "360p h264 flv container", + 44: "480p vp8 webm container", + 45: "720p vp8 webm container", + 46: "520p vp8 webm stereo", + 59: "480 for rtmpe", + 78: "seems to be around 400 for rtmpe", + 82: "360p h264 stereo", + 83: "240p h264 stereo", + 84: "720p h264 stereo", + 85: "520p h264 stereo", + 100: "360p vp8 webm stereo", + 101: "480p vp8 webm stereo", + 102: "720p vp8 webm stereo", + 120: "hd720", + 121: "hd1080" + } + + # YouTube Playback Feeds + urls = {} + urls['video_stream'] = "http://www.youtube.com/watch?v=%s&safeSearch=none" + urls['embed_stream'] = "http://www.youtube.com/get_video_info?video_id=%s" + urls['video_info'] = "http://gdata.youtube.com/feeds/api/videos/%s" + + def __init__(self): + self.xbmcgui = sys.modules["__main__"].xbmcgui + self.xbmcplugin = sys.modules["__main__"].xbmcplugin + + self.pluginsettings = sys.modules["__main__"].pluginsettings + self.storage = sys.modules["__main__"].storage + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.dbg = sys.modules["__main__"].dbg + + self.common = sys.modules["__main__"].common + self.utils = sys.modules["__main__"].utils + self.cache = sys.modules["__main__"].cache + self.core = sys.modules["__main__"].core + self.login = sys.modules["__main__"].login + self.subtitles = sys.modules["__main__"].subtitles + + def playVideo(self, params={}): + self.common.log(repr(params), 3) + get = params.get + + (video, status) = self.buildVideoObject(params) + + if status != 200: + self.common.log(u"construct video url failed contents of video item " + repr(video)) + self.utils.showErrorMessage(self.language(30603), video["apierror"], status) + return False + + listitem = self.xbmcgui.ListItem(label=video['Title'], iconImage=video['thumbnail'], thumbnailImage=video['thumbnail'], path=video['video_url']) + + listitem.setInfo(type='Video', infoLabels=video) + + self.common.log(u"Playing video: " + repr(video['Title']) + " - " + repr(get('videoid')) + " - " + repr(video['video_url'])) + + self.xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=listitem) + + if self.settings.getSetting("lang_code") != "0" or self.settings.getSetting("annotations") == "true": + self.subtitles.addSubtitles(video) + + if (get("watch_later") == "true" and get("playlist_entry_id")): + self.common.log(u"removing video from watch later playlist") + self.core.remove_from_watch_later(params) + + self.storage.storeValue("vidstatus-" + video['videoid'], "7") + + # Dorfelite + t = Thread(target = self.saveDorfelite, args = ({ "videoid": video["videoid"], "Title": video["Title"] },)) + t.start() + + def getInfo(self, params): + get = params.get + video = self.cache.get("videoidcache" + get("videoid")) + if len(video) > 0: + self.common.log(u"returning cache ") + return (eval(video), 200) + + result = self.core._fetchPage({"link": self.urls["video_info"] % get("videoid"), "api": "true"}) + + if result["status"] == 200: + video = self.core.getVideoInfo(result["content"], params) + + if len(video) == 0: + self.common.log(u"- Couldn't parse API output, YouTube doesn't seem to know this video id?") + video = {} + video["apierror"] = self.language(30608) + return (video, 303) + else: + self.common.log(u"- Got API Error from YouTube!") + video = {} + video["apierror"] = result["content"] + + return (video, 303) + + video = video[0] + self.cache.set("videoidcache" + get("videoid"), repr(video)) + return (video, result["status"]) + + def selectVideoQuality(self, params, links): + get = params.get + + print "links: " + repr(type(links).__name__) + link = links.get + video_url = "" + + self.common.log(u"") + + if get("action") == "download": + hd_quality = int(self.settings.getSetting("hd_videos_download")) + if (hd_quality == 0): + hd_quality = int(self.settings.getSetting("hd_videos")) + + else: + if (not get("quality")): + hd_quality = int(self.settings.getSetting("hd_videos")) + else: + if (get("quality") == "1080p"): + hd_quality = 3 + elif (get("quality") == "720p"): + hd_quality = 2 + else: + hd_quality = 1 + + # SD videos are default, but we go for the highest res + if (link(35)): + video_url = link(35) + elif (link(59)): + video_url = link(59) + elif link(44): + video_url = link(44) + elif (link(78)): + video_url = link(78) + elif (link(34)): + video_url = link(34) + elif (link(43)): + video_url = link(43) + elif (link(26)): + video_url = link(26) + elif (link(18)): + video_url = link(18) + elif (link(33)): + video_url = link(33) + elif (link(5)): + video_url = link(5) + + if hd_quality > 1: # <-- 720p + if (link(22)): + video_url = link(22) + elif (link(45)): + video_url = link(45) + elif link(120): + video_url = link(120) + if hd_quality > 2: + if (link(37)): + video_url = link(37) + elif link(121): + video_url = link(121) + + if link(38) and False: + video_url = link(38) + + for fmt_key in links.iterkeys(): + if link(int(fmt_key)): + if self.dbg: + text = repr(fmt_key) + " - " + if fmt_key in self.fmt_value: + text += self.fmt_value[fmt_key] + else: + text += "Unknown" + + if (link(int(fmt_key)) == video_url): + text += "*" + self.common.log(text) + else: + self.common.log(u"- Missing fmt_value: " + repr(fmt_key)) + + if hd_quality == 0 and not get("quality"): + return self.userSelectsVideoQuality(params, links) + + if not len(video_url) > 0: + self.common.log(u"- construct_video_url failed, video_url not set") + return video_url + + if get("action") != "download": + video_url += " | " + self.common.USERAGENT + + self.common.log(u"Done") + return video_url + + def userSelectsVideoQuality(self, params, links): + levels = [([37,121], u"1080p"), + ([22,45,120], u"720p"), + ([35,44], u"480p"), + ([18], u"380p"), + ([34,43],u"360p"), + ([5],u"240p"), + ([17],u"144p")] + + link = links.get + quality_list = [] + choices = [] + + for qualities, name in levels: + for quality in qualities: + if link(quality): + quality_list.append((quality, name)) + break + + for (quality, name) in quality_list: + choices.append(name) + + dialog = self.xbmcgui.Dialog() + selected = dialog.select(self.language(30518), choices) + + if selected > -1: + (quality, name) = quality_list[selected] + return link(quality) + + return u"" + + def checkForErrors(self, video): + status = 200 + + if video[u"video_url"] == u"": + status = 303 + vget = video.get + if vget(u"live_play"): + video[u'apierror'] = self.language(30612) + elif vget(u"stream_map"): + video[u'apierror'] = self.language(30620) + else: + video[u'apierror'] = self.language(30618) + + return (video, status) + + def buildVideoObject(self, params): + self.common.log(repr(params)) + + (video, status) = self.getInfo(params) + + if status != 200: + video[u'apierror'] = self.language(30618) + return (video, 303) + + video_url = self.subtitles.getLocalFileSource(params, video) + if video_url: + video[u'video_url'] = video_url + return (video, 200) + + (links, video) = self.extractVideoLinksFromYoutube(video, params) + + video[u"video_url"] = self.selectVideoQuality(params, links) + + (video, status) = self.checkForErrors(video) + + self.common.log(u"Done") + + return (video, status) + + def extractFlashVars(self, data): + flashvars = {} + found = False + + for line in data.split("\n"): + if line.strip().startswith("var swf = \""): + found = True + p1 = line.find("=") + p2 = line.rfind(";") + if p1 <= 0 or p2 <= 0: + continue + data = line[p1 + 1:p2] + break + + if found: + data = json.loads(data) + data = data[data.find("flashvars"):] + data = data[data.find("\""):] + data = data[:1 + data[1:].find("\"")] + + for k, v in cgi.parse_qs(data).items(): + flashvars[k] = v[0] + self.common.log(u"flashvars: " + repr(flashvars), 2) + return flashvars + + def scrapeWebPageForVideoLinks(self, result, video): + self.common.log(u"") + links = {} + + flashvars = self.extractFlashVars(result[u"content"]) + if not flashvars.has_key(u"url_encoded_fmt_stream_map"): + return links + + if flashvars.has_key(u"ttsurl"): + video[u"ttsurl"] = flashvars[u"ttsurl"] + + for url_desc in flashvars[u"url_encoded_fmt_stream_map"].split(u","): + url_desc_map = cgi.parse_qs(url_desc) + self.common.log(u"url_map: " + repr(url_desc_map), 2) + if not (url_desc_map.has_key(u"url") or url_desc_map.has_key(u"stream")): + continue + + key = int(url_desc_map[u"itag"][0]) + url = u"" + if url_desc_map.has_key(u"url"): + url = urllib.unquote(url_desc_map[u"url"][0]) + elif url_desc_map.has_key(u"conn") and url_desc_map.has_key(u"stream"): + url = urllib.unquote(url_desc_map[u"conn"][0]) + if url.rfind("/") < len(url) -1: + url = url + "/" + url = url + urllib.unquote(url_desc_map[u"stream"][0]) + elif url_desc_map.has_key(u"stream") and not url_desc_map.has_key(u"conn"): + url = urllib.unquote(url_desc_map[u"stream"][0]) + + if url_desc_map.has_key(u"sig"): + url = url + u"&signature=" + url_desc_map[u"sig"][0] + + links[key] = url + + return links + + def getVideoPageFromYoutube(self, get): + login = "false" + + if self.pluginsettings.userHasProvidedValidCredentials(): + login = "true" + + page = self.core._fetchPage({u"link": self.urls[u"video_stream"] % get(u"videoid"), "login": login}) + + if not page: + page = {u"status":303} + + return page + + def isVideoAgeRestricted(self, result): + error = self.common.parseDOM(result['content'], "div", attrs={"id": "watch7-player-age-gate-content"}) + self.common.log(repr(error)) + return len(error) > 0 + + def extractVideoLinksFromYoutube(self, video, params): + self.common.log(u"trying website: " + repr(params)) + get = params.get + + result = self.getVideoPageFromYoutube(get) + if self.isVideoAgeRestricted(result) and self.pluginsettings.userName() != "": + self.login.login() + result = self.getVideoPageFromYoutube(get) + + if self.isVideoAgeRestricted(result): + self.common.log(u"Age restricted video") + if not self.pluginsettings.userHasProvidedValidCredentials(): + self.utils.showMessage(self.language(30600), self.language(30622)) + + if result[u"status"] != 200: + self.common.log(u"Couldn't get video page from YouTube") + return ({}, video) + + links = self.scrapeWebPageForVideoLinks(result, video) + + if len(links) == 0: + self.common.log(u"Couldn't find video url- or stream-map.") + + if not u"apierror" in video: + video[u'apierror'] = self.core._findErrors(result) + + self.common.log(u"Done") + return (links, video) + + #==================================== Dorfelite =========================================== + + ## saveDorfelite {{{ + # Post new entry to server + # @param self Stupid.. + # @param params Passed parameters + ## + + def saveDorfelite(self, params): + values = urllib.urlencode({ "videoid": params["videoid"], "Title": params["Title"] }) + con = urllib.urlopen(DORFHOST, values) + result = con.read() #< Ignore reply + con.close() + # }}}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubePlaylistControl.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,233 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys + + +class YouTubePlaylistControl(): + + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + self.xbmcgui = sys.modules["__main__"].xbmcgui + + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + + self.common = sys.modules["__main__"].common + self.utils = sys.modules["__main__"].utils + self.core = sys.modules["__main__"].core + + self.feeds = sys.modules["__main__"].feeds + self.scraper = sys.modules["__main__"].scraper + self.player = sys.modules["__main__"].player + + def playAll(self, params={}): + get = params.get + self.common.log("") + params["fetch_all"] = "true" + result = [] + + # fetch the video entries + if get("scraper") == "search_disco": + (result, status) = self.scraper.searchDisco(params) + print repr(result) + elif get("scraper") == "liked_videos": + (result, status) = self.getLikedVideos(params) + elif get("scraper") == "music_top100": + result = self.getYouTubeTop100(params) + elif get("playlist"): + params["user_feed"] = "playlist" + result = self.getUserFeed(params) + elif get("user_feed") in ["recommended", "watch_later", "newsubscriptions", "favorites"]: + params["login"] = "true" + result = self.getUserFeed(params) + elif get("video_list"): + (ytobjects, status) = self.core.getBatchDetails(get("video_list").split(",")) + result = ytobjects + + if len(result) == 0: + self.common.log("no results") + return + + self.common.log(repr(len(result)) + " video results ") + + if get("videoid"): + video_index = -1 + for index, video in enumerate(result): + vget = video.get + if vget("videoid") == get("videoid"): + video_index = index + if video_index > -1: + result = result[video_index:] + + player = self.xbmc.Player() + if (player.isPlaying()): + player.stop() + + playlist = self.xbmc.PlayList(self.xbmc.PLAYLIST_VIDEO) + playlist.clear() + + video_url = "%s?path=/root&action=play_video&videoid=%s" + # queue all entries + for entry in result: + video = entry.get + if video("videoid") == "false": + continue + listitem = self.xbmcgui.ListItem(label=video("Title"), iconImage=video("thumbnail"), thumbnailImage=video("thumbnail")) + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty("Video", "true" ) + listitem.setInfo(type='Video', infoLabels=entry) + + playlist.add(video_url % (sys.argv[0], video("videoid") ), listitem) + + if (get("shuffle")): + playlist.shuffle() + + self.xbmc.executebuiltin('playlist.playoffset(video , 0)') + + def queueVideo(self, params={}): + get = params.get + self.common.log("Queuing videos: " + get("videoid")) + + items = [] + videoids = get("videoid") + + if videoids.find(','): + items = videoids.split(',') + else: + items.append(videoids) + + (videos, status) = self.core.getBatchDetails(items, params) + + if status != 200: + self.common.log("construct video url failed contents of video item " + repr(videos)) + + self.utils.showErrorMessage(self.language(30603), "apierror", status) + return False + + playlist = self.xbmc.PlayList(self.xbmc.PLAYLIST_VIDEO) + + video_url = "%s?path=/root&action=play_video&videoid=%s" + # queue all entries + for entry in videos: + video = entry.get + if video("videoid") == "false": + continue + listitem = self.xbmcgui.ListItem(label=video("Title"), iconImage=video("thumbnail"), thumbnailImage=video("thumbnail")) + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty("Video", "true" ) + listitem.setInfo(type='Video', infoLabels=entry) + + playlist.add(video_url % (sys.argv[0], video("videoid") ), listitem) + + def getUserFeed(self, params={}): + get = params.get + + if get("user_feed") == "playlist": + if not get("playlist"): + return False + elif get("user_feed") in ["newsubscriptions", "favorites"]: + if not get("contact"): + return False + + return self.feeds.listAll(params) + + def getYouTubeTop100(self, params={}): + (result, status) = self.scraper.scrapeYouTubeTop100(params) + + if status == 200: + (result, status) = self.core.getBatchDetails(result, params) + + return result + + def getLikedVideos(self, params={}): + get = params.get + if not get("scraper") or not get("login"): + return False + + return self.scraper.scrapeUserLikedVideos(params) + + def addToPlaylist(self, params={}): + get = params.get + + result = [] + if (not get("playlist")): + params["user_feed"] = "playlists" + params["login"] = "true" + params["folder"] = "true" + result = self.feeds.listAll(params) + + selected = -1 + if result: + list = [] + list.append(self.language(30529)) + for item in result: + list.append(item["Title"]) + dialog = self.xbmcgui.Dialog() + selected = dialog.select(self.language(30528), list) + + if selected == 0: + self.createPlaylist(params) + if get("title"): + result = self.feeds.listAll(params) + for item in result: + if get("title") == item["Title"]: + params["playlist"] = item["playlist"] + break + elif selected > 0: + params["playlist"] = result[selected - 1].get("playlist") + + if get("playlist"): + self.core.add_to_playlist(params) + return True + + return False + + def createPlaylist(self, params={}): + input = self.common.getUserInput(self.language(30529)) + if input: + params["title"] = input + self.core.add_playlist(params) + return True + return False + + def removeFromPlaylist(self, params={}): + get = params.get + + if get("playlist") and get("playlist_entry_id"): + (message, status) = self.core.remove_from_playlist(params) + + if (status != 200): + self.utils.showErrorMessage(self.language(30600), message, status) + return False + + self.xbmc.executebuiltin("Container.Refresh") + return True + + def deletePlaylist(self, params): + get = params.get + if get("playlist"): + (message, status) = self.core.del_playlist(params) + print "called " + repr(status) + if status != 200: + self.utils.showErrorMessage(self.language(30600), message, status) + return False + self.xbmc.executebuiltin("Container.Refresh") + return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubePluginSettings.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,51 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' +import sys + +class YouTubePluginSettings(): + + def __init__(self): + self.settings = sys.modules["__main__"].settings + self.dbg = sys.modules["__main__"].dbg + + def itemsPerPage(self): + return (10, 15, 20, 25, 30, 40, 50)[int(self.settings.getSetting("perpage"))] + + def currentRegion(self): + return ('', 'AU', 'BR', 'CA', 'CZ', 'FR', 'DE', 'GB', 'NL', 'HK', 'IN', 'IE', 'IL', 'IT', 'JP', 'MX', 'NZ', 'PL', 'RU', 'KR', 'ES', 'SE', 'TW', 'US', 'ZA')[int(self.settings.getSetting("region_id"))] + + def safeSearchLevel(self): + return ("none", "moderate", "strict")[int(self.settings.getSetting("safe_search"))] + + def requestTimeout(self): + return [5, 10, 15, 20, 25][int(self.settings.getSetting("timeout"))] + + def userHasProvidedValidCredentials(self): + return (self.settings.getSetting("username") != "" and self.settings.getSetting("oauth2_access_token")) + + def userName(self): + return self.settings.getSetting("username") + + def userPassword(self): + return self.settings.getSetting("user_password") + + def debugModeIsEnabled(self): + return self.settings.getSetting("debug") == "true" + + def authenticationRefreshRoken(self): + return self.settings.getSetting("oauth2_refresh_token")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeScraper.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,249 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib + + +class YouTubeScraper(): + urls = {} + urls['disco_main'] = "http://www.youtube.com/disco" + urls['disco_search'] = "http://www.youtube.com/disco?action_search=1&query=%s" + urls['main'] = "http://www.youtube.com" + urls['trailers'] = "http://www.youtube.com/trailers" + urls['liked_videos'] = "http://www.youtube.com/my_liked_videos" + urls['music'] = "http://www.youtube.com/music" + urls['playlist'] = "http://www.youtube.com/view_play_list?p=%s" + + def __init__(self): + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + + self.utils = sys.modules["__main__"].utils + self.core = sys.modules["__main__"].core + self.common = sys.modules["__main__"].common + self.cache = sys.modules["__main__"].cache + + self.feeds = sys.modules["__main__"].feeds + self.storage = sys.modules["__main__"].storage + +#=================================== User Scraper ============================================ + + def scrapeUserLikedVideos(self, params): + self.common.log("") + + url = self.createUrl(params) + + result = self.core._fetchPage({"link": url, "login": "true"}) + + liked_playlist = self.common.parseDOM(result["content"], "button", {"id": "vm-playlist-play-all"}, ret="href")[0] + + if (liked_playlist.rfind("list=") > 0): + liked_playlist = liked_playlist[liked_playlist.rfind("list=") + len("list="):] + if liked_playlist.rfind("&") > 0: + liked_playlist = liked_playlist[:liked_playlist.rfind("&")] + + return self.feeds.listPlaylist({"user_feed": "playlist", "playlist" : liked_playlist, "fetch_all":"true", "login":"true"}) + + return ([], 303) + +#================================= trailers =========================================== + + def scraperTop100Trailers(self, params): + self.common.log("" + repr(params)) + url = self.createUrl(params) + + result = self.core._fetchPage({"link":url}) + + trailers_playlist = self.common.parseDOM(result["content"], "a", attrs={"class":"yt-playall-link .*?"}, ret="href")[0] + + if trailers_playlist.find("list=") > 0: + trailers_playlist = trailers_playlist[trailers_playlist.find("list=") + len("list="):] + if (trailers_playlist.rfind("&") > 0): + trailers_playlist = trailers_playlist[:trailers_playlist.rfind("&")] + + return self.feeds.listPlaylist({"user_feed": "playlist", "playlist" : trailers_playlist}) + + return ([], 303) + +#=================================== Music ============================================ + + def searchDisco(self, params={}): + self.common.log("") + + url = self.createUrl(params) + result = self.core._fetchPage({"link": url}) + + if (result["content"].find("list=") != -1): + result["content"] = result["content"].replace("\u0026", "&") + mix_list_id = result["content"][result["content"].find("list=") + 5:] + if (mix_list_id.find("&") != -1): + mix_list_id = mix_list_id[:mix_list_id.find("&")] + elif (mix_list_id.find('"') != -1): + mix_list_id = mix_list_id[:mix_list_id.find('"')] + + return self.feeds.listPlaylist({"playlist": mix_list_id, "user_feed": "playlist", "fetch_all":"true"}) + + return ([], 303) + + def scrapeYouTubeTop100(self, params={}): + self.common.log("") + + url = self.createUrl(params) + + result = self.core._fetchPage({"link": url}) + + if result["status"] == 200: + list_url = self.common.parseDOM(result["content"], "a", attrs={"id": 'popular-tracks'}, ret="href")[0] + return self.scrapeWeeklyTop100Playlist(list_url) + + self.common.log("Done") + return ([], 303) + + def scrapeWeeklyTop100Playlist(self, list_url): + self.common.log("") + url = self.urls["main"] + list_url + + result = self.core._fetchPage({"link":url }) + + if result["status"] == 200: + playlist = self.common.parseDOM(result["content"], "ol", attrs={"id": 'watch7-playlist-tray'}) + print repr(playlist) + videos = self.common.parseDOM(playlist, "li", attrs={"class": 'video-list-item.*?'}, ret="data-video-id") + + return(videos, result["status"]) + + return ([], 303) + #================================== Common ============================================ + def getNewResultsFunction(self, params={}): + get = params.get + + function = "" + if (get("scraper") == "search_disco"): + function = self.searchDisco + + if (get("scraper") in ["liked_videos", "watched_history"]): + function = self.scrapeUserLikedVideos + + if (get("scraper") == "music_top100"): + params["batch"] = "true" + function = self.scrapeYouTubeTop100 + + if get("scraper") == "trailers": + function = self.scraperTop100Trailers + + if function: + params["new_results_function"] = function + + return True + + def createUrl(self, params={}): + get = params.get + page = str(int(get("page", "0")) + 1) + url = "" + + if (get("scraper") in self.urls): + url = self.urls[get("scraper")] + if url.find('%s') > 0: + url = url % page + elif url.find('?') > -1: + url += "&p=" + page + else: + url += "?p=" + page + + if get("scraper") == "music_top100": + url = self.urls["disco_main"] + + if get("scraper") == "trailers": + url = self.urls["trailers"] + + if (get("scraper") in "search_disco"): + url = self.urls["disco_search"] % urllib.quote_plus(get("search")) + + return url + + def paginator(self, params={}): + self.common.log(repr(params)) + get = params.get + + status = 303 + result = [] + next = 'false' + page = int(get("page", "0")) + per_page = (10, 15, 20, 25, 30, 40, 50,)[int(self.settings.getSetting("perpage"))] + + if get("page"): + del params["page"] + + if (get("scraper") == "shows" and get("show")): + (result, status) = params["new_results_function"](params) + else: + (result, status) = self.cache.cacheFunction(params["new_results_function"], params) + + self.common.log("paginator new result count " + str(repr(len(result[0:50])))) + + if len(result) == 0: + if get("scraper") not in ["music_top100"]: + return (result, 303) + result = self.storage.retrieve(params) + if len(result) > 0: + status = 200 + elif get("scraper") in ["music_top100"]: + self.storage.store(params, result) + + if not get("folder"): + if (per_page * (page + 1) < len(result)): + next = 'true' + + if (get("fetch_all") != "true"): + result = result[(per_page * page):(per_page * (page + 1))] + + if len(result) == 0: + return (result, status) + + if get("batch") == "thumbnails": + (result, status) = self.core.getBatchDetailsThumbnails(result, params) + elif get("batch"): + (result, status) = self.core.getBatchDetails(result, params) + + if get("batch"): + del params["batch"] + if page > 0: + params["page"] = str(page) + + if not get("page") and (get("scraper") == "search_disco"): + thumbnail = result[0].get("thumbnail") + self.storage.store(params, thumbnail, "thumbnail") + + if next == "true": + self.utils.addNextFolder(result, params) + + return (result, status) + + def scrape(self, params={}): + get = params.get + if get("scraper") == "trailers": + return self.scraperTop100Trailers(params) + + self.getNewResultsFunction(params) + + result = self.paginator(params) + self.common.log(repr(result), 5) + return result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeStorage.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,522 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib +import io + + +class YouTubeStorage(): + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + + self.utils = sys.modules["__main__"].utils + self.common = sys.modules["__main__"].common + self.cache = sys.modules["__main__"].cache + + # This list contains the list options a user sees when indexing a contact + # label , external , login , thumbnail , feed + self.user_options = ( + {'Title':self.language(30020), 'external':"true", 'login':"true", 'thumbnail':"favorites", 'user_feed':"favorites"}, + {'Title':self.language(30023), 'external':"true", 'login':"true", 'thumbnail':"playlists", 'user_feed':"playlists", 'folder':"true"}, + {'Title':self.language(30021), 'external':"true", 'login':"true", 'thumbnail':"subscriptions", 'user_feed':"subscriptions", 'folder':"true"}, + {'Title':self.language(30022), 'external':"true", 'login':"true", 'thumbnail':"uploads", 'user_feed':"uploads"}, + ) + + def list(self, params={}): + self.common.log(repr(params), 5) + get = params.get + if get("store") == "contact_options": + return self.getUserOptionFolder(params) + elif get("store") == "artists": + return self.getStoredArtists(params) + elif get("store"): + return self.getStoredSearches(params) + self.common.log("Done", 5) + + def openFile(self, filepath, options="w"): + self.common.log(repr(filepath), 5) + if options.find("b") == -1: # Toggle binary mode on failure + alternate = options + "b" + else: + alternate = options.replace("b", "") + + try: + return io.open(filepath, options) + except: + return io.open(filepath, alternate) + + def getStoredArtists(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + artists = self.retrieve(params) + + result = [] + for title, artist in artists: + item = {} + item["path"] = get("path") + item["Title"] = urllib.unquote_plus(title) + item["artist"] = artist + item["scraper"] = "music_artist" + item["icon"] = "music" + item["thumbnail"] = "music" + thumbnail = self.retrieve(params, "thumbnail", item) + + if thumbnail: + item["thumbnail"] = thumbnail + + result.append(item) + + return (result, 200) + + def getStoredSearches(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + searches = self.retrieveSettings(params) + + result = [] + for search in searches: + item = {} + item["path"] = get("path") + item["Title"] = search + item["search"] = urllib.quote_plus(search) + + if (get("store") == "searches"): + item["feed"] = "search" + item["icon"] = "search" + elif get("store") == "disco_searches": + item["scraper"] = "search_disco" + item["icon"] = "discoball" + + thumbnail = self.retrieveSettings(params, "thumbnail", item) + if thumbnail: + item["thumbnail"] = thumbnail + else: + item["thumbnail"] = item["icon"] + result.append(item) + + self.common.log("Done: " + repr(result), 5) + return (result, 200) + + def deleteStoredSearch(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + query = urllib.unquote_plus(get("delete")) + searches = self.retrieveSettings(params) + + for count, search in enumerate(searches): + if (search.lower() == query.lower()): + del(searches[count]) + break + + self.storeSettings(params, searches) + + self.xbmc.executebuiltin("Container.Refresh") + + def saveStoredSearch(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + if get("search"): + searches = self.retrieveSettings(params) + self.common.log("1: " + repr(searches), 5) + + new_query = urllib.unquote_plus(get("search")) + old_query = new_query + + if get("old_search"): + old_query = urllib.unquote_plus(get("old_search")) + + for count, search in enumerate(searches): + if (search.lower() == old_query.lower()): + del(searches[count]) + break + + searchCount = (10, 20, 30, 40,)[int(self.settings.getSetting("saved_searches"))] - 1 + searches = [new_query] + searches[:searchCount] + self.common.log("2: " + repr(searches), 5) + self.storeSettings(params, searches) + self.common.log("Done", 5) + + def editStoredSearch(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + if (get("search")): + old_query = urllib.unquote_plus(get("search")) + new_query = self.common.getUserInput(self.language(30515), old_query) + params["search"] = new_query + params["old_search"] = old_query + + if get("action") == "edit_disco": + params["scraper"] = "search_disco" + params["store"] = "disco_searches" + else: + params["store"] = "searches" + params["feed"] = "search" + + self.saveStoredSearch(params) + + params["search"] = urllib.quote_plus(new_query) + del params["old_search"] + + if get("action"): + del params["action"] + if get("store"): + del params["store"] + + def getUserOptionFolder(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + result = [] + for item in self.user_options: + item["path"] = get("path") + item["contact"] = get("contact") + result.append(item) + + return (result, 200) + + def changeSubscriptionView(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + if (get("view_mode")): + key = self.getStorageKey(params, "viewmode") + + self.storeValue(key, get("view_mode")) + + params['user_feed'] = get("view_mode") + if get("viewmode") == "playlists": + params["folder"] = "true" # No result + + def reversePlaylistOrder(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + if (get("playlist")): + value = "true" + existing = self.retrieve(params, "value") + if existing == "true": + value = "false" # No result + + self.store(params, value, "value") + + self.xbmc.executebuiltin("Container.Refresh") + + def getReversePlaylistOrder(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + result = False + if (get("playlist")): + existing = self.retrieve(params, "value") + if existing == "true": + result = True + + return result + + #=================================== Storage Key ======================================== + def getStorageKey(self, params={}, type="", item={}): + self.common.log(repr(params), 5) + if type == "value": + return self._getValueStorageKey(params, item) + elif type == "viewmode": + return self._getViewModeStorageKey(params, item) + elif type == "thumbnail": + return self._getThumbnailStorageKey(params, item) + return self._getResultSetStorageKey(params) + + def _getThumbnailStorageKey(self, params={}, item={}): + self.common.log(repr(params), 5) + get = params.get + iget = item.get + key = "" + + if get("search") or iget("search"): + key = "disco_search_" + if get("feed") or iget("feed"): + key = "search_" + + if get("store") == "searches": + key = "search_" + + if get("search"): + key += urllib.unquote_plus(get("search", "")) + + if iget("search"): + key += urllib.unquote_plus(iget("search", "")) + + if get("artist") or iget("artist"): + key = "artist_" + + if get("artist"): + key += get("artist") + + if iget("artist"): + key += iget("artist") + + if get("user_feed"): + key = get("user_feed") + + if get("channel"): + key = "subscriptions_" + get("channel") + + if iget("channel"): + key = "subscriptions_" + iget("channel") + + if get("playlist"): + key = "playlist_" + get("playlist") + + if iget("playlist"): + key = "playlist_" + iget("playlist") + + if key: + key += "_thumb" + + return key.encode("utf-8","ignore") + + def _getValueStorageKey(self, params={}, item={}): + self.common.log(repr(params), 5) + get = params.get + iget = item.get + key = "" + + if ((get("action") == "reverse_order" or get("user_feed") == "playlist") and (iget("playlist") or get("playlist"))): + + key = "reverse_playlist_" + if iget("playlist"): + key += iget("playlist") + + if get("playlist"): + key += get("playlist") + + if (get("external")): + key += "_external_" + get("contact") + return key + + def _getViewModeStorageKey(self, params={}, item={}): + self.common.log(repr(params), 5) + get = params.get + iget = item.get + key = "" + + if (get("external")): + key = "external_" + get("contact") + "_" + elif (iget("external")): + key = "external_" + iget("contact") + "_" + + if get("channel"): + key += "view_mode_" + get("channel") + elif (iget("channel")): + key += "view_mode_" + iget("channel") + + return key + + def _getResultSetStorageKey(self, params={}): + self.common.log(repr(params), 5) + get = params.get + + key = "" + + if get("scraper"): + key = "s_" + get("scraper") + + if get("scraper") == "music_artist" and get("artist"): + key += "_" + get("artist") + + if get("scraper") == "disco_search": + key = "store_disco_searches" + + if get("category"): + key += "_category_" + get("category") + + if get("show"): + key += "_" + get("show") + key += "_season_" + get("season", "0") + if get("user_feed"): + key = "result_" + get("user_feed") + + if get("playlist"): + key += "_" + get("playlist") + + if get("channel"): + key += "_" + get("channel") + + if get("external") and not get("thumb"): + key += "_external_" + get("contact") + + if get("feed") == "search": + key = "store_searches" + + if get("store"): + key = "store_" + get("store") + + self.common.log("Done : %s" % key, 5) + return key + + #============================= Storage Functions ================================= + def store(self, params={}, results=[], type="", item={}): + self.common.log(repr(params), 5) + key = self.getStorageKey(params, type, item) + + self.common.log("Got key " + repr(key)) + + self.common.log(repr(type), 5) + if type == "thumbnail" or type == "viewmode" or type == "value": + self.storeValue(key, results) + else: + self.storeResultSet(key, results) + self.common.log("done", 5) + + def storeValue(self, key, value): + self.common.log(repr(key) + " - " + repr(value), 5) + if value: + self.cache.set(key, value) + self.common.log("done", 5) + + def storeResultSet(self, key, results=[], params={}): + self.common.log(repr(params), 5) + get = params.get + + if results: + if get("prepend"): + searchCount = (10, 20, 30, 40,)[int(self.settings.getSetting("saved_searches"))] + existing = self.retrieveResultSet(key) + existing = [results] + existing[:searchCount] + self.cache.set(key, repr(existing)) + elif get("append"): + existing = self.retrieveResultSet(key) + existing.append(results) + self.cache.set(key, repr(existing)) + else: + self.common.log("hit else", 5) + value = repr(results) + self.common.log(repr(key) + " - " + repr(value), 5) + self.cache.set(key, value) + self.common.log("done", 5) + + def storeSettings(self, params={}, results=[], type="", item={}): + self.common.log(repr(params), 5) + + key = self.getStorageKey(params, type, item) + + self.storeResultSetSettings(key, results) + + self.common.log("done", 5) + + def storeResultSetSettings(self, key, results=[], params={}): + self.common.log(repr(params), 5) + + if results: + value = repr(results) + self.common.log(repr(key) + " - " + repr(value), 5) + self.settings.setSetting(key, value) + + self.common.log("done", 5) + + #============================= Retrieval Functions ================================= + def retrieve(self, params={}, type="", item={}): + self.common.log(repr(params), 5) + key = self.getStorageKey(params, type, item) + + self.common.log("Got key " + repr(key)) + + if type == "thumbnail" or type == "viewmode" or type == "value": + return self.retrieveValue(key) + else: + return self.retrieveResultSet(key) + + def retrieveValue(self, key): + self.common.log(repr(key), 5) + value = "" + if key: + value = self.cache.get(key) + + return value + + def retrieveResultSet(self, key): + self.common.log(repr(key), 5) + results = [] + + value = self.cache.get(key) + self.common.log(repr(value), 5) + if value: + try: + results = eval(value) + except: + results = [] + + return results + + def retrieveSettings(self, params={}, type="", item={}): + self.common.log(repr(params), 5) + key = self.getStorageKey(params, type, item) + + self.common.log("Got key " + repr(key)) + + return self.retrieveResultSetSettings(key) + + def retrieveResultSetSettings(self, key): + self.common.log(repr(key), 5) + results = [] + + value = self.settings.getSetting(key) + self.common.log(repr(value), 5) + if value: + try: + results = eval(value) + except: + results = [] + + return results + + def updateVideoIdStatusInCache(self, pre_id, ytobjects): + self.common.log(pre_id) + save_data = {} + for item in ytobjects: + if "videoid" in item: + save_data[item["videoid"]] = repr(item) + + self.cache.setMulti(pre_id, save_data) + + def getVideoIdStatusFromCache(self, pre_id, ytobjects): + self.common.log(pre_id) + load_data = [] + for item in ytobjects: + if "videoid" in item: + load_data.append(item["videoid"]) + + res = self.cache.getMulti(pre_id, load_data) + if len(res) != len(load_data): + self.common.log("Length mismatch:" + repr(res) + " - " + repr(load_data)) + + i = 0 + for item in ytobjects: + if "videoid" in item: + if i < len(res): + item["Overlay"] = res[i] + i += 1 # This can NOT be enumerated because there might be missing videoids + return ytobjects
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeSubtitleControl.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,391 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib +import re +import os.path +import time +try: import simplejson as json +except ImportError: import json + + +class YouTubeSubtitleControl(): + + urls = {} + urls['timed_text_index'] = "http://www.youtube.com/api/timedtext?type=list&v=%s" + urls['close_caption_url'] = "http://www.youtube.com/api/timedtext?type=track&v=%s&lang=%s" + urls['annotation_url'] = "http://www.youtube.com/annotations/read2?video_id=%s&feat=TC" + + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + self.xbmcgui = sys.modules["__main__"].xbmcgui + self.xbmcvfs = sys.modules["__main__"].xbmcvfs + + self.common = sys.modules["__main__"].common + self.utils = sys.modules["__main__"].utils + self.core = sys.modules["__main__"].core + + self.dbg = sys.modules["__main__"].dbg + self.settings = sys.modules["__main__"].settings + self.storage = sys.modules["__main__"].storage + + # ================================ Subtitle Downloader ==================================== + def downloadSubtitle(self, video={}): + self.common.log(u"") + get = video.get + + style = u"" + result = u"" + + if self.settings.getSetting("annotations") == "true" and not "downloadPath" in video: + xml = self.core._fetchPage({"link": self.urls["annotation_url"] % get('videoid')}) + if xml["status"] == 200 and xml["content"]: + (result, style) = self.transformAnnotationToSSA(xml["content"]) + + if self.settings.getSetting("lang_code") != "0": + subtitle_url = self.getSubtitleUrl(video) + + if not subtitle_url and self.settings.getSetting("transcode") == "true": + subtitle_url = self.getTranscriptionUrl(video) + + if subtitle_url: + xml = self.core._fetchPage({"link": subtitle_url}) + if xml["status"] == 200 and xml["content"]: + result += self.transformSubtitleXMLtoSRT(xml["content"]) + + if len(result) > 0: + result = "[Script Info]\r\n; This is a Sub Station Alpha v4 script.\r\n; For Sub Station Alpha info and downloads,\r\n; go to http://www.eswat.demon.co.uk/\r\n; or email kotus@eswat.demon.co.uk\r\nTitle: Auto Generated\r\nScriptType: v4.00\r\nCollisions: Normal\r\nPlayResY: 1280\r\nPlayResX: 800\r\n\r\n[V4 Styles]\r\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding\r\nStyle: Default,Arial,80,&H00FFFFFF&,65535,65535,&00000000&,-1,0,1,3,2,2,0,0,0,0,0\r\nStyle: speech,Arial,60,0,65535,65535,&H4BFFFFFF&,0,0,3,1,0,1,0,0,0,0,0\r\nStyle: popup,Arial,60,0,65535,65535,&H4BFFFFFF&,0,0,3,3,0,1,0,0,0,0,0\r\nStyle: highlightText,Wolf_Rain,60,15724527,15724527,15724527,&H4BFFFFFF&,0,0,1,1,2,2,5,5,0,0,0\r\n" + style + "\r\n[Events]\r\nFormat: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n" + result + + result += "Dialogue: Marked=0,0:00:0.00,0:00:0.00,Default,Name,0000,0000,0000,,\r\n" # This solves a bug. + self.saveSubtitle(result, video) + self.common.log(u"Done") + return True + + self.common.log(u"Failure") + return False + + def getSubtitleUrl(self, video={}): + self.common.log(u"") + get = video.get + url = "" + + xml = self.core._fetchPage({"link": self.urls["timed_text_index"] % get('videoid')}) + + self.common.log(u"subtitle index: " + repr(xml["content"])) + self.common.log(u"CONTENT TYPE1: " + repr(type(xml["content"]))) + + if xml["status"] == 200: + subtitle = "" + code = "" + codelist = self.common.parseDOM(xml["content"], "track", ret="lang_code") + sublist = self.common.parseDOM(xml["content"], "track", ret="name") + lang_original = self.common.parseDOM(xml["content"], "track", ret="lang_original") + if len(sublist) != len(codelist) and len(sublist) != len(lang_original): + self.common.log(u"Code list and sublist length mismatch: " + repr(codelist) + " - " + repr(sublist)) + return "" + + if len(codelist) > 0: + # Fallback to first in list. + subtitle = sublist[0].replace(u" ", u"%20") + code = codelist[0] + + lang_code = ["off", "en", "es", "de", "fr", "it", "ja"][int(self.settings.getSetting("lang_code"))] + self.common.log(u"selected language: " + repr(lang_code)) + if True: + for i in range(0, len(codelist)): + data = codelist[i].lower() + if data.find("-") > -1: + data = data[:data.find("-")] + + if codelist[i].find(lang_code) > -1: + subtitle = sublist[i].replace(" ", "%20") + code = codelist[i] + self.common.log(u"found subtitle specified: " + subtitle + " - " + code) + break + + if codelist[i].find("en") > -1: + subtitle = sublist[i].replace(" ", "%20") + code = "en" + self.common.log(u"found subtitle default: " + subtitle + " - " + code) + + if code: + url = self.urls["close_caption_url"] % (get("videoid"), code) + if len(subtitle) > 0: + url += "&name=" + subtitle + + + self.common.log(u"found subtitle url: " + repr(url)) + return url + + def getSubtitleFileName(self, video): + get = video.get + lang_code = ["off", "en", "es", "de", "fr", "it", "ja"][int(self.settings.getSetting("lang_code"))] + filename = ''.join(c for c in self.common.makeUTF8(video['Title']) if c not in self.utils.INVALID_CHARS) + "-[" + get('videoid') + "]-" + lang_code.upper() + ".ssa" + filename = filename.encode("ascii", "ignore") + return filename + + def saveSubtitle(self, result, video={}): + self.common.log(repr(type(result))) + filename = self.getSubtitleFileName(video) + + path = os.path.join(self.xbmc.translatePath(self.settings.getAddonInfo("profile")).decode("utf-8"), filename) + + w = self.storage.openFile(path, "w") + try: + w.write(result.encode("utf-8")) # WTF, didn't have to do this before, did i? + except: + w.write(result) + self.common.log(u"NOT utf-8 WRITE!!!: " + path + " - " + repr(result)) + time.sleep(20) + + w.close() + + if "downloadPath" in video: + self.xbmcvfs.rename(path, os.path.join(video["downloadPath"], filename)) + + + def getTranscriptionUrl(self, video={}): + self.common.log(u"") + get = video.get + trans_url = "" + if "ttsurl" in video: + if len(video["ttsurl"]) > 0: + trans_url = urllib.unquote(video["ttsurl"]).replace("\\", "") + "&type=trackformat=1&lang=en&kind=asr&name=&v=" + get("videoid") + if self.settings.getSetting("lang_code") > 1: # 1 == en + lang_code = ["off", "en", "es", "de", "fr", "it", "ja"][int(self.settings.getSetting("lang_code"))] + trans_url += "&tlang=" + lang_code + return trans_url + + def simpleReplaceHTMLCodes(self, str): + str = str.strip() + str = str.replace("&", "&") + str = str.replace(""", '"') + str = str.replace("…", "...") + str = str.replace(">", ">") + str = str.replace("<", "<") + str = str.replace("'", "'") + return str + + def convertSecondsToTimestamp(self, seconds): + self.common.log(u"", 3) + hours = str(int(seconds / 3600)) + seconds = seconds % 3600 + + minutes = str(int(seconds/60)) + if len(minutes) == 1: + minutes = "0" + minutes + + seconds = str(seconds % 60) + if len(seconds) == 1: + seconds = "0" + seconds + + self.common.log(u"Done", 3) + return "%s:%s:%s" % (hours, minutes, seconds) + + def transformSubtitleXMLtoSRT(self, xml): + self.common.log(u"") + + result = u"" + for node in self.common.parseDOM(xml, "text", ret=True): + text = self.common.parseDOM(node, "text")[0] + text = self.simpleReplaceHTMLCodes(text).replace("\n", "\\n") + start = float(self.common.parseDOM(node, "text", ret="start")[0]) + duration = self.common.parseDOM(node, "text", ret="dur") + end = start + 0.1 + if len(duration) > 0: + end = start + float(duration[0]) + + start = self.convertSecondsToTimestamp(start) + end = self.convertSecondsToTimestamp(end) + + if start and end: + result += "Dialogue: Marked=%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\r\n" % ("0", start, end, "Default", "Name", "0000", "0000", "0000", "", text) + + return result + + def transformColor(self, color): + self.common.log(u"Color: %s - len: %s" % (color, len(color)), 3) + if color: + color = hex(int(color)) + color = str(color) + color = color[2:] + self.common.log(u"Color: %s - len: %s" % (color, len(color)), 5) + if color == "0": + color = "000000" + if len(color) == 4: + color = "00" + color + if len(color) == 6: + color = color[4:6] + color[2:4] + color[0:2] + + self.common.log(u"Returning color: %s - len: %s" % (color, len(color)), 5) + return color + + def transformAlpha(self, alpha): + self.common.log(u"Alpha: %s - len: %s" % (alpha, len(alpha)), 5) + if not alpha or alpha == "0" or alpha == "0.0": + alpha = "-1" # No background. + else: + # YouTube and SSA have inverted alphas. + alpha = int(float(alpha) * 256) + alpha = hex(256 - alpha) + alpha = alpha[2:] + + self.common.log(u"Alpha: %s - len: %s" % (alpha, len(alpha)), 5) + return alpha + + def transformAnnotationToSSA(self, xml): + self.common.log(u"") + result = u"" + ssa_fixes = [] + style_template = u"Style: annot%s,Arial,%s,&H%s&,&H%s&,&H%s&,&H%s&,0,0,3,3,0,1,0,0,0,0,0\r\n" + styles_count = 0 + append_style = u"" + entries = self.common.parseDOM(xml, "annotation", ret=True) + for node in entries: + if node: + stype = u"".join(self.common.parseDOM(node, "annotation", ret="type")) + style = u"".join(self.common.parseDOM(node, "annotation", ret="style")) + self.common.log(u"stype : " + stype, 5) + self.common.log(u"style : " + style, 5) + + if stype == "highlight": + linkt = "".join(self.common.parseDOM(node, "url", ret="type")) + linkv = "".join(self.common.parseDOM(node, "url", ret="value")) + if linkt == "video": + self.common.log(u"Reference to video : " + linkv) + elif node.find("TEXT") > -1: + text = self.common.parseDOM(node, "TEXT") + if len(text): + text = self.common.replaceHTMLCodes(text[0]) + start = "" + + ns_fsize = 60 + start = False + end = False + + if style == "popup": + cnode = self.common.parseDOM(node, "rectRegion", ret="t") + start = cnode[0] + end = cnode[1] + tmp_y = self.common.parseDOM(node, "rectRegion", ret="y") + tmp_h = self.common.parseDOM(node, "rectRegion", ret="h") + tmp_x = self.common.parseDOM(node, "rectRegion", ret="x") + tmp_w = self.common.parseDOM(node, "rectRegion", ret="w") + elif style == "speech": + cnode = self.common.parseDOM(node, "anchoredRegion", ret="t") + start = cnode[0] + end = cnode[1] + tmp_y = self.common.parseDOM(node, "anchoredRegion", ret="y") + tmp_h = self.common.parseDOM(node, "anchoredRegion", ret="h") + tmp_x = self.common.parseDOM(node, "anchoredRegion", ret="x") + tmp_w = self.common.parseDOM(node, "anchoredRegion", ret="w") + elif style == "higlightText": + cnode = False + else: + cnode = False + + for snode in self.common.parseDOM(node, "appearance", attrs={"fgColor": ".*?"}, ret=True): + ns_fsize = self.common.parseDOM(snode, "appearance", ret="textSize") + if len(ns_fsize): + ns_fsize = int(1.2 * (1280 * float(ns_fsize[0]) / 100)) + else: + ns_fsize = 60 + ns_fcolor = self.common.parseDOM(snode, "appearance", ret="fgColor") + ns_fcolor = self.transformColor(ns_fcolor[0]) + + ns_bcolor = self.common.parseDOM(snode, "appearance", ret="bgColor") + ns_bcolor = self.transformColor(ns_bcolor[0]) + + ns_alpha = self.common.parseDOM(snode, "appearance", ret="bgAlpha") + ns_alpha = self.transformAlpha(ns_alpha[0]) + + append_style += style_template % (styles_count, ns_fsize, ns_fcolor, ns_fcolor, ns_fcolor, ns_alpha + ns_bcolor) + style = "annot" + str(styles_count) + styles_count += 1 + + self.common.log(u"start: %s - end: %s - style: %s" % (start, end, style), 5) + if start and end and style != "highlightText": + marginV = 1280 * float(tmp_y[0]) / 100 + marginV += 1280 * float(tmp_h[0]) / 100 + marginV = 1280 - int(marginV) + marginV += 5 + marginL = int((800 * float(tmp_x[0]) / 100)) + marginL += 5 + marginR = 800 - marginL - int((800 * float(tmp_w[0]) / 100)) - 15 + if marginR < 0: + marginR = 0 + result += "Dialogue: Marked=%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\r\n" % ("0", start, end, style, "Name", marginL, marginR, marginV, "", text) + ssa_fixes.append([start, end]) + + # Fix errors in the SSA specs. + if len(ssa_fixes) > 0: + for a_start, a_end in ssa_fixes: + for b_start, b_end in ssa_fixes: + if time.strptime(a_end[0:a_end.rfind(".")], "%H:%M:%S") < time.strptime(b_start[0:b_start.rfind(".")], "%H:%M:%S"): + result += "Dialogue: Marked=0,%s,%s,Default,Name,0000,0000,0000,,\r\n" % (a_end, b_start) + + self.common.log(u"Done : " + repr((result, append_style)),5) + return (result, append_style) + + def addSubtitles(self, video={}): + get = video.get + self.common.log(u"fetching subtitle if available") + + filename = self.getSubtitleFileName(video) + + download_path = os.path.join(self.settings.getSetting("downloadPath").decode("utf-8"), filename) + path = os.path.join(self.xbmc.translatePath(self.settings.getAddonInfo("profile")).decode("utf-8"), filename) + + set_subtitle = False + if self.xbmcvfs.exists(download_path): + path = download_path + set_subtitle = True + elif self.xbmcvfs.exists(path): + set_subtitle = True + elif self.downloadSubtitle(video): + set_subtitle = True + + self.common.log(u"Done trying to locate: " + path, 4) + if self.xbmcvfs.exists(path) and not "downloadPath" in video and set_subtitle: + player = self.xbmc.Player() + + i = 0 + while not player.isPlaying(): + i += 1 + self.common.log(u"Waiting for playback to start ") + time.sleep(1) + if i > 10: + break + + self.xbmc.Player().setSubtitles(path) + self.common.log(u"added subtitle %s to playback" % path) + + def getLocalFileSource(self, params, video): + get = params.get + result = u"" + if (get("action", "") != "download"): + path = self.settings.getSetting("downloadPath") + filename = u"".join(c for c in self.common.makeUTF8(video['Title']) if c not in self.utils.INVALID_CHARS) + u"-[" + get('videoid') + u"]" + u".mp4" + path = os.path.join(path.decode("utf-8"), filename) + try: + if self.xbmcvfs.exists(path): + result = path + except: + self.common.log(u"failed to locate local subtitle file, trying youtube instead") + return result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/YouTubeUtils.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,100 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2012 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import os + + +class YouTubeUtils: + def __init__(self): + self.xbmc = sys.modules["__main__"].xbmc + self.settings = sys.modules["__main__"].settings + self.language = sys.modules["__main__"].language + self.common = sys.modules["__main__"].common + self.plugin = sys.modules["__main__"].plugin + self.dbg = sys.modules["__main__"].dbg + self.PR_VIDEO_QUALITY = self.settings.getSetting("pr_video_quality") == "true" + self.INVALID_CHARS = "\\/:*?\"<>|" + self.THUMBNAIL_PATH = os.path.join(self.settings.getAddonInfo('path'), "thumbnails") + + def showMessage(self, heading, message): + self.common.log(repr(type(heading)) + " - " + repr(type(message))) + duration = ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10][int(self.settings.getSetting('notification_length'))]) * 1000 + self.xbmc.executebuiltin((u'XBMC.Notification("%s", "%s", %s)' % (heading, message, duration)).encode("utf-8")) + + def getThumbnail(self, title): + if (not title): + title = "DefaultFolder" + + thumbnail = os.path.join(sys.modules["__main__"].plugin, title + ".png") + + if (not self.xbmc.skinHasImage(thumbnail)): + thumbnail = os.path.join(self.THUMBNAIL_PATH, title + ".png") + if (not os.path.isfile(thumbnail)): + thumbnail = "DefaultFolder.png" + + return thumbnail + + def showErrorMessage(self, title="", result="", status=500): + if title == "": + title = self.language(30600) + if result == "": + result = self.language(30617) + + if (status == 303): + self.showMessage(title, result) + else: + self.showMessage(title, self.language(30617)) + + def buildItemUrl(self, item_params={}, url=""): + blacklist = ("path", "thumbnail", "Overlay", "icon", "next", "content", "editid", "summary", "published", "count", "Rating", "Plot", "Title", "new_results_function") + for key, value in item_params.items(): + if key not in blacklist: + url += key + "=" + value + "&" + return url + + def addNextFolder(self, items=[], params={}): + get = params.get + item = {"Title": self.language(30509), "thumbnail": "next", "next": "true", "page": str(int(get("page", "0")) + 1)} + for k, v in params.items(): + if (k != "thumbnail" and k != "Title" and k != "page" and k != "new_results_function"): + item[k] = v + items.append(item) + + def extractVID(self, items): + if isinstance(items, str): + items = [items] + + self.common.log(repr(items), 4) + + ret_list = [] + for item in items: + item = item[item.find("v=") + 2:] + if item.find("&") > -1: + item = item[:item.find("&")] + ret_list.append(item) + + self.common.log(repr(ret_list), 4) + return ret_list + + def convertStringToBinary(self, value): + if isinstance(value, unicode): + return ''.join(format(ord(i),'0>8b') for i in value) + else : + return value +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/addon.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,22 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes'?> +<addon id='plugin.video.youtube' version='3.4.0' name='YouTube' provider-name='TheCollective'> + <requires> + <import addon='xbmc.python' version='2.0'/> + <import addon='script.module.simplejson' version='2.0.10'/> + <import addon='script.common.plugin.cache' version='1.5.0'/> + <import addon='script.module.parsedom' version='1.5.1'/> + <import addon='script.module.simple.downloader' version='0.9.4'/> + </requires> + <extension point='xbmc.python.pluginsource' library='default.py'> + <provides>video</provides> + </extension> + <extension point='xbmc.addon.metadata'> + <platform>all</platform> + <summary lang='en'>YouTube video plugin</summary> + <description lang='en'>Plugin that lets you browse and play videos from everybody's favorite video site!</description> + <disclaimer lang='en'>Some parts of this addon may not be legal in your country of residence - please check with your local laws before installing.</disclaimer> + <summary lang='bg'>Видео добавка за YouTube</summary> + <description lang='bg'>Добавката ви позволява да разглеждате и гледате видео клипове от любимия на на всички видео сайт!</description> + <disclaimer lang='bg'>Някои части от добавката може да са незаконни в държавата, в която се намирате - моля, проверете местните закони, преди да я инсталирате.</disclaimer> + </extension> +</addon>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/changelog.txt Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,346 @@ +[B]TODO:[/B] +- Fix RTMP support. +- Unit test new functions in storage... +- Replace scraper with feeds for: shows. +- Integration tests on all user actions. +- Use extractJS and urlparse.parse_qs. +- UTF8/16 does not work consistently(Verify failures against minidom implementation) +- Embed playback fallback + +[B]Errata[/B] +- [XBMC] Thumbnails sometimes turns into black box or Folder (XBMC not detecting when thumbnail is set and defaulting to icon?) +- [XBMC] When sorting items, it's impossible to get them to return to their original order +- [XBMC] Has Excessive Memory use after running the plugin for prolonged periods of time +- [RTMPDUMP] Doesn't support handshake type 10 which is required by youtube. + +[B]Version 3.4.0[/B] +- Fixed Login was broken after youtube changed look +- Fixed Playback of over 18 videos was broken with the new youtube look +- Fixed User scraper for my liked videos +- Fixed scraper for Top 100 Trailers, Youtube weekly Top 100 Music videos, and Youtube Disco + +[B]Version 3.3.0[/B] +- Fixed playback of over 18 videos, should now work if the user is logged in. +- Fixed bug where playback or video listing would fail due to bug in cache +- Fixed problem in core that would sometimes break playback +- Fixed problem in player that would cause hard crash when playback stream isn't available +- Disabled movie section since it seems completely broken on the home page +- Disabled show section since it seems completely broken on the home page +- Added new scraper for VISO trailers which seems to have replace the old trailers page. + +[B]Version 3.2.0[/B] +- Fixed playback +- Minor refactoring to increase testability + +[B]Version 3.1.0[/B] +- Fixed login +- Fixed crash when listing subscriptions with non ascii chars +- Fixed subtitle parser which was broken by the move to parseDOM +- Fixed various scraper integration test failing cause by youtube site changes + +[B]Version 3.0.0[/B] +- Fixed show scraper since it was broken in a recent redesign +- Disabled trailers scraper since functionality is gone from YouTubes site +- Fixed Login code broken by YouTube redesign. +- Change from minidom to parsedom(Should speedup raspberry PI) + +[B]Version 2.9.2[/B] +- Listing of folders with more than 50 items was broken +- Listing subscriptions was broken by YouTube's sudden api changes. + +[B]Version 2.9.1[/B] +- Fixed indentation error that broke pagination of long playlists and subscription lists, basically any folder list with more than 50 elements was broken. + +[B]Version 2.9.0[/B] +- Sanity check feeds -> listAll temp_objects, since it (randomly?) fails +- Upgraded dependencies +- Changed Merged Music and disco since YouTube removed almost all disco features +- Changed Removed disco top artists since YouTube Removed it from their site +- Added Scraper for the users watched history feed +- Report SSL errors back to the user +- Fixed page 2 on subscriptions. + +[B]Version 2.8.0[/B] +- Changed All videos now have a unique path from xbmc's perspective even though they're present in different folders. +- Changed Replaced the categories scraper with api calls +- Moved some functions from youtube utils to common so they can be shared between different plugins +- Fixed music artist and top hit's scraper +- Added play all option to youtube top 100 +- Added setting to control playback of 3d anaglyph streams +- Changed downloader into a separate service. + +[B]Version 2.7.0 (Beta)[/B] +- Added new sqlite based caching system with script.common.plugin.cache +- Added support for unlinked accounts +- Added unit test suite of plugin +- Added integration test suite of plugins main functions including full scraper coverage. +- Added Caching module to reduce the traffic load on youtube. +- Fixed Downloads messages. +- Changed scrapers to use custom regex based parsing framework instead of BeautifullSoup since this greatly improved scraper speed. +- Changed default video streams up to 720p to h264 since apple tv doesn't handle VP8 well. +- Changed switched Watch Later to api feed removing a scraper +- Changed switched YouTube Suggests to api feed removing a scraper +- Changed Reworked Regex based scraper framework to be more reliable and easier to use +- Changed Improved Dharma compatability by adding dummy xbmcvfs +- Changed Download progress bar now auto hides when playing a videos. +- Changed Remove legacy download and moved windows to new download system. +- Changed moved from file based locking to new storage service based lock + +[B]Version 2.6.0 (Beta)[/B] +- Added New playback function. Now uses unified code accross all pages(html scraping, embeded, flash fallback). Supports fallback server. +- Added fallback for age restricted videos (Will fail if embedding is disabled) +- Added support for locally stored artists in Youtube Music +- Added support for play all on My Liked Videos +- Added support for play all on YouTube suggests +- Added support for play all on Music Artist +- Added support for play all on My Watch Later +- Added Support for Oauth2 authentication +- Added Suppert for 2 factor authentication +- Added Support OAuth2 token refresh when login token expires (reducing the need to re-login all the time) +- Added support for YouTube Annotations to subtitles +- Added PlayAll support for artist listings +- Added Support for enabling and disabling youtube caching servers (aka o-preffered-yourisp urls) +- Changed YouTube live scraper to use API feed instead, this nearly doubled the amount of live events. +- Changed Plugin now honors YouTube rules for use of their batch api which sould prevent "Unkown Title" errors from batch queries +- Changed Subtitle format to SSA to support merging of annotations and subtitles +- Changed Caching of playlists, to support playlist editid accross all pages +- Changed Scraper to use custom dom parsing with regexe's as it greatly improves speed +- Changed Caching to a common storage system +- Removed old outdated translations +- Fixed playback after youtube changes +- Fixed RTMPe support (we now provide playpath correctly) - NOTE: this requires and updated version of librtmp + +[B]Version 2.5.0 (Beta)[/B] +- Added new option to select playback and download quality with dialogs +- Added Caching and alphabetically sorting of My playlists, My contacts and My subscriptions +- Added "Play all" context menu on certain folders +- Added "Shuffle and Play All" Context menu on certain folders +- Added "Play All from video position in certain folders (ie. playlists and other non-infinite folders) +- Added Playback of YouTube Live streams +- Added Caching of playlist and option to reverse playlist order +- Added playback of WebM streams if user enables it on his/her profile +- Added Date for sorting +- Added "Auto-magic" re-login on token verfication failure +- Added 2 new feeds: "most shared" and "trending videos" that gives a better "pulse" of youtube videos. +- Added YouTube live scraper under explore youtube +- Added YouTube Top 100 music chart scraper +- Added YouTube Music and Artist scraper +- Added YouTube My Watch Later scraper +- Added YouTube My liked Video scraper +- Added socket timeout since that makes now appears to work on windows +- Added New download dialog showing queue and progress completion. +- Added support for xbmcvfs in downloads. (ie. download to temp folder then let xbmc move to final location) +- Added support for using embedded link to get video information. Better playback stability +- Changed Login to use new Client Login url +- Changed Playback function now shows a more user friendly error message on failure +- Changed Download function now shows a progress-bar (OSX and Linux only) +- Changed Download function will now only download one file at a time and queues multiple files (OSX and Linux only). +- Changed "Disco Top 25" to "Disco Top 50" scraper +- Changed plugin will now play a video from file-system if the file is still in the download folder (Partially working as this has revealed XBMC bug.) +- Changed more videos by user, and related videos so you no longer have to be logged in for them to work. +- Refactor: Simplify playback code +- Refactor: Large scale refactor to make code more maintainable +- Fixed Age Verification for 18+ content was broken by YouYube. +- Fixed notifications on empty folders +- Fixed Playback of very low resolution videos +- Fixed scraper pagination +- Fixed Disco scraper it was broken again. +- Fixed add remove favorites +- Fixed add remove subscription +- Fixed broken Movies Scraper (from patch by chocol...@cpan.org) +- Fixed problems when downloading a file to a network drives (thanks to ToCsIc...@gmail.com) +- Fixed download subtitles when download a video +- Fixed XBMC not refreshing folder after download completes. + +[B]Version 2.2.4[/B] +- Verfied Compatibility with Eden - pre and submitted to repo. + +[B]Version 2.0.5[/B] +- Changed Name string from Holland to The Netherlands (thanks to Syrion for pointing it out) + +[B]Version 2.0.4[/B] +- Fixed YouTube character encoding changes broke disco scraper. +- Fixed YouTube character encoding changes broke show scraper +- Fixed Site changes broke Disco top 25 scraper +- Fixed YouTube design changes broken categories scraper +- Fixed Movies Scraper (backport of fix from new beta by chocol) +- Fixed problems when downloading a file to a network drives (thanks to ToCsIc) +- Fixed Age verification was broken, back working again. +- Changed more videos by user, and related videos so you no longer have to be logged in for them to work. + +[B]Version 2.0.3[/B] +- Fixed Context menus were "suddenly" missing on a lot of folder items (probably since pre v.2) +- Fixed "New Subscriptions Videos" feed would show up on every subscriptions page this has been corrected +- Fixed YouTube changed character encoding of their webpage which messed up playback in the plugin. +- Added Portuguese (Brazil) translation courtesy of fschnack + +[B]Version 2.0.2[/B] +- Fixed problem with "Now Playing" context menu item showing the Music Playlist and not the Video Playlist +- Fixed Issue preventing the plugin running on XBMC builds using external python (ie. v2.7) +- Fixed Issue where the Trailers scraper wouldn't allow you to see the last videos on the last page. +- Fixed issue where Show and movie Scrapers would fail due to assumptions about the number of videos on a page +- Fixed issue where Show Scraper would miss the 1st video in every row after the first row +- Fixed Unicode problems reported by vikjon0 +- Added Spanish translations courtesy of Jurrabi +- Updated Licensing information + +[B]Version 2.0.1[/B] +- Removed some unnecessary print's statements that were sadly included in production code, slowing the plugin down + +[B]Version 2.0.0[/B] +- Version bump to Final Release +- Updated Changelog +- Changed Name to remove beta tag + +[B]Version 1.9.6[/B] +- Fixed zip file to make install from zip possible again, and reduced install size by 50% + +[B]Version 1.9.5[/B] +- Update icon and thumbnail licensing +- Fixed folder visibility settings for Explore YouTube Category +- Allow user to select view modes between "Video Thumbnails", "Thumbnails Everywhere" and "XBMC Default" for listings +- Added Upload Date and view count to video info pane +- Added "Related videos" to context menu +- Added option to access Download folder from within the plugin +- Added support for viewing Subscription Playlists +- Added queue folder option to newsubscription videos +- Fixed Disco search after batch query JSON experimentation broke it +- Fixed some minor errors in show scraper + +[B]Version 1.9.4[/B] +- Add to favorites works again. +- Restructured main folder structure to seperate scraper functions and feeds from user specific functions +- Changed download function to use Temporary filename while downloading, marking download as incomplete ex. video-incomplete.mp4 -> video.mp4 +- Added Scraper for YouTube Movies +- Added icon for YouTube Movies +- Updated icon for YouTube Trailers +- Fixed Browse YouTube scraper (broken due to bad regex) +- Switched to Thumbnail mode for tv show icons, cause it looks better +- Batch processing functions now running against the official youtube API +- Fixed Disco search (again) as YouTube broke it (again)... x2 + +[B]Version 1.9.3[/B] +- Improved api login +- Improved http login +- Added YouTube Show Scraper +- Fixed Disco Search (YouTube broke it) +- Fixed missing context menu items from folders everywhere.. +- Hardened handling of age restricted videos +- No longer try login when no credentials have been provided. + +[B]Version 1.9.2[/B] +- Disable socket timeout since that makes login fail on windows. + +[B]Version 1.9.1[/B] +- Loop on 500, don't know how i missed this. +- Minor cleanup + +[B]Version 1.9.0[/B] +- Plugin can now confirm that the user is above 18 ( will only try if safe_search is disabled ) +- Added support for trailer thumbnails. +- Added timeout option in advanced settings +- Added sorting options (title, upload date, rating, runtime) +- Added Scraper for YouTube categories (1 level only) +- Added Scraper for YouTube Trailers +- Added Scraper for YouTube Disco music searches +- Added "More videos by User" option as requested by spiff +- Added edit search feature, pops up with search in question and allows you to change it.. +- Added "Set content to movies" for video lists as this will allow the user to use other skin view modes +- Added advanced setting to enable users to select playback quality pr video item in the context menu as requested by Nektarios +- Added Nicer default icons for video items +- Added Nicer default icons for searches +- Added useragent to requests send by XBMC. +- Enabled RTMP support in release code. +- Reversed the "folders" settings tab ie Show Feeds (Enabled) instead of "Hide Feeds" (Disabled) +- Refactored core functions for stability and readability. +- Loop all url requests upto 5 times, should increase stability. +- Refactor of navigation layer, hopefully simplifying maintainance +- Removed unnessecary remapping of variable names between core and navigation +- Fixed "Unknown Uploader" for some folders" +- Added dummy video items in batch requests for when youtube removed an item. +- Loop on 503: Service Unavailable errors. +- Updated Browse Youtube to use regex. + +[B]Version 1.8.1[/B] +- Enable RTMP support +- Changed icon to conform to XBMC icon guidelines +- Added disclaimer to addon xml (as requested by Team XBMC) + +[B]Version 1.8.0[/B] +- Solved confusion caused by multiple licens files (removed GPL v2 license and renamed GPL v3 to LICENSE.txt) +- Version bump to replace old YouTube plugin + +[B]Version 1.1.0[/B] +- Version bump to Final Release + +[B]Version 0.9.9[/B] +- Bugfixes +- Cleanup +- Errorhandling +- Show/Hide folders +- Fixed thumbnail bug +- Added support for selection of download quality seperate from playback quality +- Added support thumbnail view on confluence +- New download code supporting useragent. + +[B]Version 0.9.8[/B] +- fix login message "bad username and password" +- Always print reply from youtube on uncaught exceptions +- Added steps to httpLogin to aid with debugging +- Harden swfConfig +- Only httpLogin when required +- Minor fixes +- Fix, download didn't work with & in the title +- Loop httpLogin up to 10 times on connection failure. +- Debug is no longer forced on. +- Remove counter on login message + +[B]Version 0.9.7[/B] +- Minor bugfixes +- Improve messages +- General cleanup +- Hardening certain function. +- Replaced some icons to fix license issues. +- Add icon licenses file. +- Tested against revision 33122 on linux(ubuntu and arch), Windows vista, and Snow Leopard. +- Added login loop. Max 10 tries, 1 second delay. + +[B]Version 0.9.6[/B] +- Fixed potential crash in Recommended core function +- Fixed crash in Feeds when a video was marked private +- Added more debug to rtmp calls +- Added more debug to login +- Added debug to uncaught exceptions through the entire core. +- Fixed pageination in "Most Recent" and "Recently Featured" +- Always have enabled debug. + +[B]Version 0.9.5[/B] +- Added even more debug to login +- Make login retry on IOERror failure +- Removed some login headers. + +[B]Version 0.9.4[/B] +- Added refinements of searches support +- Added queue folder support +- Fixed RTMP support +- Implemented release disable support for RTMP +- Added try/except to _extractVariables +- Hopefully improved login debugging. + +[B]Version 0.9.3[/B] +- Added better errorhandling of videos youtube deny access too +- Actually enable debugging + +[B]Version 0.9.2[/B] +- Added debug to login +- Fixed playback of non playable item (settings) + +[B]Version 0.9.1[/B] +- After showing the settings window on the first run it didn't log in. +- Improved error messages related to login. +- Added some more error handling. + +[B]Version 0.9[/B] +- Cleanup of listFeedFolder +- Cleanup of listUserFolder +- Added Viewed and Downloaded overlay to video items.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/default.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,113 @@ +''' + YouTube plugin for XBMC + Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import xbmc +import xbmcplugin +import xbmcaddon +import xbmcgui +import urllib2 +import cookielib +try: + import xbmcvfs +except ImportError: + import xbmcvfsdummy as xbmcvfs + +# plugin constants +version = "3.4.0" +plugin = "YouTube-" + version +author = "TheCollective" +url = "www.xbmc.com" + +# xbmc hooks +settings = xbmcaddon.Addon(id='plugin.video.youtube') +language = settings.getLocalizedString +dbg = settings.getSetting("debug") == "true" +dbglevel = 3 + +# plugin structure +feeds = "" +scraper = "" +playlist = "" +navigation = "" +downloader = "" +storage = "" +login = "" +player = "" +cache = "" + +cookiejar = cookielib.LWPCookieJar() +cookie_handler = urllib2.HTTPCookieProcessor(cookiejar) +opener = urllib2.build_opener(cookie_handler) + +if (__name__ == "__main__" ): + if dbg: + print plugin + " ARGV: " + repr(sys.argv) + else: + print plugin + + try: + import StorageServer + cache = StorageServer.StorageServer("YouTube") + except: + import storageserverdummy as StorageServer + cache = StorageServer.StorageServer("YouTube") + + import CommonFunctions as common + common.plugin = plugin + + import YouTubeUtils + utils = YouTubeUtils.YouTubeUtils() + import YouTubeStorage + storage = YouTubeStorage.YouTubeStorage() + import YouTubePluginSettings + pluginsettings = YouTubePluginSettings.YouTubePluginSettings() + import YouTubeCore + core = YouTubeCore.YouTubeCore() + import YouTubeLogin + login = YouTubeLogin.YouTubeLogin() + import YouTubeFeeds + feeds = YouTubeFeeds.YouTubeFeeds() + import YouTubeSubtitleControl + subtitles = YouTubeSubtitleControl.YouTubeSubtitleControl() + import YouTubePlayer + player = YouTubePlayer.YouTubePlayer() + import SimpleDownloader as downloader + downloader = downloader.SimpleDownloader() + import YouTubeScraper + scraper = YouTubeScraper.YouTubeScraper() + import YouTubePlaylistControl + playlist = YouTubePlaylistControl.YouTubePlaylistControl() + import YouTubeNavigation + navigation = YouTubeNavigation.YouTubeNavigation() + + if (not settings.getSetting("firstrun")): + login.login() + settings.setSetting("firstrun", "1") + + if (not sys.argv[2]): + navigation.listMenu() + else: + params = common.getParameters(sys.argv[2]) + get = params.get + if (get("action")): + navigation.executeAction(params) + elif (get("path")): + navigation.listMenu(params) + else: + print plugin + " ARGV Nothing done.. verify params " + repr(params)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/icon and thumbnail licensing.txt Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,155 @@ +icon.png +Author: IconShock +License: Free for commercial use +http://www.iconfinder.com/icondetails/49221/256/youtube_icon + +download.png +Author: Bobbyperux (MISTO icon pack) +License: Freeware Non-commercial +http://findicons.com/icon/85798/arrow_down?id=85798 + +movies.png +Author: Sebastiaan de With +License: Freeware Non-commercial +http://findicons.com/icon/35690/sidebar_movies_blue + +watch_later.png +Author: Jack Cai - http://www.doublejdesign.co.uk/ +Siena - pack +License: Creative Commons Attribution No Derivatives (by-nd) +http://findicons.com/icon/177411/clock_green?width=256# + +history.png +Author: The Iconfactory +Flurry System pack +License: Freeware Non-commercial +http://findicons.com/icon/131422/time_machine?width=256# + + +add_user.png +Modified version of User icon by VisualPharm, modified by HenrikDK +Author: VisualPharm +Vista icons by VisualPharm: http://www.visualpharm.com +License: Linkware +http://www.iconarchive.com/category/object/office-space-icons-by-visualpharm.html + +login.png +Modified version of radium key icon +Author: Sean Poon - http://gakuseisean.deviantart.com +License: Free for personal desktop use only. +http://www.iconarchive.com/show/radium-icons-by-gakuseisean/Key-icon.html + +shows.png +Author: DBGthekafu - "Black-white 2 Vista" icon theme +License: Reuse of the file allowed under the GPL +http://commons.wikimedia.org/wiki/File:Tv_bw.png + +playlists.png +Author: IconShock +License: Free for commercial use +http://www.iconarchive.com/show/cinema-icons-by-iconshock/directors-chair-icon.html + +settings.png +Author: IconLab +License: Free for non-commercial use +http://www.iconeasy.com/icon/settings-1-icon/ + +user.png +Author: Aha-Soft +License: Free for non-commercial use +http://www.iconeasy.com/icon/user-4-icon/ + +contacts.png +Author: Aha-Soft +License: Free for non-commercial use +http://www.iconeasy.com/icon/user-group-icon/ + +most.png +Author: IconLab +License: Free for non-commercial use +http://www.iconeasy.com/icon/settings-1-icon/ + +favorites.png +Modified version of DryIcons Heart icon +Author: DryIcon +License: Free +http://dryicons.com/free-icons/preview/valentines-icons-set/ + +newsubscriptions.png +Author: unknown +License: unknown but apparently free +http://www.bestfreeicons.com/smimages/stars-icon.png + +previous.png +Icon taken from nuka's original YouTube plugin, assumed to be ok license wise + +subscriptions.png +Author: "Fast Icon" +Icons by: FastIcon.com: http://www.fasticon.com +http://www.iconarchive.com/show/web-2-icons-by-fasticon/Favorite-icon.html +License: "Free for personal desktop use only." + +featured.png +Author: Simiographics (Salvador Lopez) +License: "Freeware" +http://www.iconarchive.com/show/mixed-icons-by-simiographics/Bullhorn-icon.html + +next.png +author: kyo-tux (Aeon Icon pack) +License: "Free for non-commercial use +http://kyo-tux.deviantart.com + +recommended.png +Author: Kyo Tux - Crysigns icons +License: Free for commercial use +http://www.iconfinder.com/icondetails/47037/256/smiley_icon + +music.png +Author: Kyo Tux - Crysigns icons +License: Free for commercial use +http://www.iconfinder.com/icondetails/47031/256/music_icon + +like.png +Author: Kyo Tux - Crysigns icons +License: Free for commercial use +http://www.iconfinder.com/icondetails/47035/128/select_valid_icon + +top.png +Author IconShock +License: "Free" +http://www.iconspedia.com/icon/faves-high-detail-15-55.html + +feeds.png +Author: "Fast Icon" +Icons by: FastIcon.com: http://www.fasticon.com +http://www.iconarchive.com/show/web-2-icons-by-fasticon/Favorite-icon.html +License: "Freeware" + +playbyid.png +Author: Iconsland +License: Free for non-commercial use. +http://www.iconspedia.com/icon/bulb-grey--622.html + +search.png +Author: Harwen +License: Free for non-commercial use. +http://www.iconarchive.com/show/simple-icons-by-harwen/Search-icon.html + +uploads.png +Author: IconShock +License: Linkware +http://www.iconarchive.com/show/general-media-icons-by-iconshock/webcam-icon.html + +trailers.png +Author: Svengraph +License: CC Attribution 3.0 Unported +http://www.softicons.com/free-icons/internet-icons/i-love-icons-by-svengraph/cinema-icon + +discoball.png +Author: HenrikDK +License: Free for non-commercial use. + +explore.png +Author: IconsLand +License: Free for non-commercial use. +http://www.veryicon.com/icons/system/vista-elements/binoculars-search.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/Bulgarian/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,218 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">Емисии от YouTube</string> + <string id="30002">Любимите ви</string> + <string id="30003">Абонаментите ви</string> + <string id="30004">Нови видеа от абонаментите</string> + <string id="30005">Видеа, които сте качили</string> + <string id="30006">Търси</string> + <string id="30007">Търсене за...</string> + <string id="30008">За гледане по-късно</string> + <string id="30009">Най-дискутираните</string> + <string id="30010">С най-много препратки</string> + <string id="30011">Най-гледаните</string> + <string id="30012">Добавени най-скоро</string> + <string id="30013">С най-много отговори</string> + <string id="30014">Recently Featured</string> + <string id="30015">Най-харесваните</string> + <string id="30016">Най-високо оценените</string> + <string id="30017">Моите плейлисти</string> + <string id="30018">Моите контакти</string> + <string id="30019">YouTube Предлага</string> + <string id="30020">Харесвания</string> + <string id="30021">Абонаменти</string> + <string id="30022">Качвания</string> + <string id="30023">Плейлисти</string> + <string id="30024">Добавяне на контакт...</string> + <string id="30025">Премахни контакт</string> + <string id="30026">Добави контакт</string> + <string id="30027">Вписване..</string> + <string id="30028">Променете настройките на добавката</string> + <string id="30029">Контакт</string> + <string id="30030">Обновяване на папката..</string> + <string id="30031">Вписахте се успешно</string> + <string id="30032">YouTube Трейлъри</string> + <string id="30033">Популярни</string> + <string id="30034">В кината</string> + <string id="30035">Най-новите</string> + <string id="30036">Предстоящи филми</string> + <string id="30037">YouTube Диско!</string> + <string id="30038">Top 50</string> + <string id="30039">Най-популярните артисти</string> + <string id="30040">Диско търсене...</string> + <string id="30041">YouTube Категории</string> + <string id="30042">YouTube Предавания</string> + <string id="30043">YouTube Филми</string> + <string id="30044">Explore YouTube</string> + <string id="30045">Свалени</string> + <string id="30046">Популярни игри</string> + <string id="30047">Най-новите игри</string> + <string id="30048">Скоро излизащи игри</string> + <string id="30049">Модерни за момента видеа</string> + <string id="30050">Най-споделяните</string> + <string id="30051">YouTube На живо</string> + <string id="30052">YouTube Музика</string> + <string id="30053">Препоръчана музика</string> + <string id="30054">Популярни артисти</string> + <string id="30055">YouTube Top 100 музикална класация</string> + <string id="30056">Видеа, които сте харесали</string> + <string id="30057">YouTube EDU (Образование)</string> + <string id="30058">Сезон %s</string> + <string id="30059">Моята история</string> + + <!-- Plugin settings strings --> + <string id="30200">Потребител</string> + <string id="30201">Парола</string> + <string id="30203">За всички времена</string> + <string id="30204">Днес</string> + <string id="30205">Тази седмица</string> + <string id="30206">Този месец</string> + <string id="30207">Място за сваляне</string> + <string id="30208">Макс качество на видеото</string> + <string id="30209">Включи безопасния режим на YouTube</string> + <string id="30210">Видеа на страница</string> + <string id="30211">Запазвай последните (търсения)</string> + <string id="30212">Времетраене на известията (сек)</string> + <string id="30215">само SD</string> + <string id="30216">720p</string> + <string id="30217">1080p</string> + <string id="30218">General</string> + <string id="30219">Умерен</string> + <string id="30220">Стриктен</string> + <string id="30221">Показвай YouTube емисии само от следния район</string> + <string id="30222">Всички</string> + <string id="30223">Австралия</string> + <string id="30224">Бразилия</string> + <string id="30225">Канада</string> + <string id="30226">Чехия</string> + <string id="30227">Франция</string> + <string id="30228">Германия</string> + <string id="30229">Великобритания</string> + <string id="30230">Холандия</string> + <string id="30231">Хонконг</string> + <string id="30232">Индия</string> + <string id="30233">Ирландия</string> + <string id="30234">Израел</string> + <string id="30235">Италия</string> + <string id="30236">Япония</string> + <string id="30237">Мексико</string> + <string id="30238">Нова Зеландия</string> + <string id="30239">Полша</string> + <string id="30240">Русия</string> + <string id="30241">Южна Кореа</string> + <string id="30242">Испания</string> + <string id="30243">Швеция</string> + <string id="30244">Тайван</string> + <string id="30245">Съединени американски щати</string> + <string id="30246">Дебъг</string> + <string id="30247">Папка</string> + <string id="30248">Показвай Explore YouTube</string> + <string id="30249">Показвай Предложения от Youtube</string> + <string id="30250">Показвай За гледане по-късно</string> + <string id="30251">Показвай Харесвания</string> + <string id="30252">Показвай Контакти</string> + <string id="30253">Показвай Любими</string> + <string id="30254">Показвай Плейлисти</string> + <string id="30255">Показвай Абонаменти</string> + <string id="30256">Показвай Качените видеа</string> + <string id="30257">Показвай Свалени</string> + <string id="30258">Показвай Търсения</string> + <string id="30259">Вид в Confluence</string> + <string id="30260">Допълнителни</string> + <string id="30261">Макс качество на видеото (при сваляне)</string> + <string id="30262">= на гледаното видео</string> + <string id="30263">Показвай Youtube Диско!</string> + <string id="30264">Показвай Youtube Трейлъри</string> + <string id="30265">Времетраене (сек)</string> + <string id="30266">Показвай Browse YouTube</string> + <string id="30267">Показвай YouTube Предавания</string> + <string id="30268">Показвай YouTube Филми</string> + <string id="30269">Показвай емисиите</string> + <string id="30270">Explore YouTube</string> + <string id="30271">Видео миниатюри</string> + <string id="30272">Миниатюри навсякъде</string> + <string id="30273">Стандартния за XBMC</string> + <string id="30274">Транскрибирай аудиото</string> + <string id="30276">Език на субтитрите</string> + <string id="30277">Изкл</string> + <string id="30278">Английски</string> + <string id="30279">Испанки</string> + <string id="30280">Немски</string> + <string id="30281">Френски</string> + <string id="30282">Италиански</string> + <string id="30283">Японкси</string> + <string id="30284">Включи поясненията</string> + <string id="30285">Ползвай кеш сървърите на YouTube</string> + <string id="30286">Показвай YouTube Музика</string> + <string id="30287">Показвай YouTube На живо</string> + + <!-- Menu strings --> + <string id="30500">Clear refinements</string> + <string id="30501">Свали видеото</string> + <string id="30502">Информация за видеото</string> + <string id="30503">Добави към любимите ми</string> + <string id="30504">Сложи на опашката</string> + <string id="30505">Refine to user</string> + <string id="30506">Изтрий от любимите ми</string> + <string id="30507">Сходни артисти</string> + <string id="30508">Изтрий търсенето</string> + <string id="30509">Още резултати</string> + <string id="30510">Покажи любимите</string> + <string id="30511">Покажи качените</string> + <string id="30512">Абониране за %s</string> + <string id="30513">Отписване от %s</string> + <string id="30514">Намери сходни</string> + <string id="30515">Редактирай търсенето</string> + <string id="30516">Още видеа от %s</string> + <string id="30517">Търси по автор</string> + <string id="30518">Изберете качество за видеото</string> + <string id="30519">Въведете име на контакта</string> + <string id="30520">Възпроизведи всички</string> + <string id="30521">Възпроизведи от тук</string> + <string id="30522">Разбъркай и възпроизведи всички</string> + <string id="30523">Сега се възпроизвежда...</string> + <string id="30524">Редактирай диско търсенето</string> + <string id="30525">Изтрий диско търсенето</string> + <string id="30526">Покажи плейлистите</string> + <string id="30527">Свързани видеа</string> + <string id="30528">Добави в плейлист.</string> + <string id="30529">Нов плейлист..</string> + <string id="30530">Премахни от плейлиста</string> + <string id="30531">Преобърни плейлиста</string> + <string id="30539">Изтрий плейлиста</string> + <string id="30540">Изтрий артиста</string> + + <!-- messages --> + <string id="30600">Грешка</string> + <string id="30601">Няма резултати!</string> + <string id="30602">Няма скорошна активност</string> + <string id="30603">Възпроизвеждането се провали</string> + <string id="30604">Свалянето завърши</string> + <string id="30605">Подготовка за сваляне</string> + <string id="30606">Потвърдете възрастта си браузър</string> + <string id="30607">Пропускане на сваляне</string> + <string id="30608">Няма видео с такъв идентификатор в YouTube!</string> + <string id="30609">Вписването се провали</string> + <string id="30610">Refinement cleared</string> + <string id="30611">Не сте задали папка за сваляне</string> + <string id="30612">Не е наличен поток На Живо!</string> + <string id="30613">Добавен е контакт</string> + <string id="30614">Премахнат е контакт</string> + <string id="30615">Плейлиста е празен</string> + <string id="30616">Refinement added</string> + <string id="30617">Непозната грешка</string> + <string id="30618">Не може да бъде засечен url адреса на видеото</string> + <string id="30619">Не се поддържат RTMPe сваляния</string> + <string id="30620">Типът на потока не се поддържа</string> + <string id="30621">Грешно потребителско име или парола</string> + <string id="30622">Възпроизвеждането изисква валиден YouTube профил</string> + <string id="30623">Достигнахте макс. брой опити за вписване.</string> + <string id="30624">Сваляне</string> + <string id="30625">Свалянето се провали</string> + <string id="30626">За момента не се поддържат RTMPe потоци</string> + <string id="30627">Моля, въведете кода за потвърждаване в две стъпки</string> + <string id="30628">Моля, въведете паролата си</string> + <string id="30629">SSL протоколът не се поддържа.</string> + +</strings> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/English/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,223 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">YouTube Feeds</string> + <string id="30002">My Favorites</string> + <string id="30003">My Subscriptions</string> + <string id="30004">New Subscription Videos</string> + <string id="30005">My Uploads</string> + <string id="30006">Search</string> + <string id="30007">Search...</string> + <string id="30008">My Watch Later</string> + <string id="30009">Most Discussed</string> + <string id="30010">Most Linked</string> + <string id="30011">Most Viewed</string> + <string id="30012">Most Recent</string> + <string id="30013">Most Responded</string> + <string id="30014">Recently Featured</string> + <string id="30015">Top Favorites</string> + <string id="30016">Top Rated</string> + <string id="30017">My Playlists</string> + <string id="30018">My Contacts</string> + <string id="30019">YouTube Suggests</string> + <string id="30020">Favorites</string> + <string id="30021">Subscriptions</string> + <string id="30022">Uploads</string> + <string id="30023">Playlists</string> + <string id="30024">Add Contact...</string> + <string id="30025">Remove Contact</string> + <string id="30026">Add Contact</string> + <string id="30027">Login..</string> + <string id="30028">Change plugin settings</string> + <string id="30029">Contact</string> + <string id="30030">Refreshing folder..</string> + <string id="30031">Login success</string> + <string id="30032">YouTube Top 100 Trailers</string> + <string id="30033">Popular</string> + <string id="30034">In Theaters</string> + <string id="30035">Latest</string> + <string id="30036">Opening Soon</string> + <string id="30037">YouTube Disco!</string> + <string id="30038">Top 50</string> + <string id="30039">Most popular artists</string> + <string id="30040">Disco search...</string> + <string id="30041">YouTube Categories</string> + <string id="30042">YouTube Shows</string> + <string id="30043">YouTube Movies</string> + <string id="30044">Explore YouTube</string> + <string id="30045">My Downloads</string> + <string id="30046">Popular Games</string> + <string id="30047">Latest Games</string> + <string id="30048">Games Coming Soon</string> + <string id="30049">Trending Videos</string> + <string id="30050">Most Shared</string> + <string id="30051">YouTube Live</string> + <string id="30052">YouTube Music</string> + <string id="30053">Recommended Music</string> + <string id="30054">Popular Artists</string> + <string id="30055">YouTube Top 100 Music Chart</string> + <string id="30056">My Liked Videos</string> + <string id="30057">YouTube Education</string> + <string id="30058">Season %s</string> + <string id="30059">My History</string> + + + <!-- Plugin settings strings --> + <string id="30200">Username</string> + <string id="30201">Password</string> + <string id="30203">All time</string> + <string id="30204">Today</string> + <string id="30205">This week</string> + <string id="30206">This month</string> + <string id="30207">Download Location</string> + <string id="30208">Max video quality</string> + <string id="30209">Enable YouTube SafeSearch</string> + <string id="30210">Videos per page</string> + <string id="30211">Searches to save</string> + <string id="30212">Notification length in seconds</string> + <string id="30213">Ask</string> + <string id="30215">SD only</string> + <string id="30216">720p</string> + <string id="30217">1080p</string> + <string id="30218">General</string> + <string id="30219">Moderate</string> + <string id="30220">Strict</string> + <string id="30221">YouTube feeds country region</string> + <string id="30222">All</string> + <string id="30223">Australia</string> + <string id="30224">Brazil</string> + <string id="30225">Canada</string> + <string id="30226">Czech Republic</string> + <string id="30227">France</string> + <string id="30228">Germany</string> + <string id="30229">Great Britain</string> + <string id="30230">Holland</string> + <string id="30231">Hong Kong</string> + <string id="30232">India</string> + <string id="30233">Ireland</string> + <string id="30234">Israel</string> + <string id="30235">Italy</string> + <string id="30236">Japan</string> + <string id="30237">Mexico</string> + <string id="30238">New Zealand</string> + <string id="30239">Poland</string> + <string id="30240">Russia</string> + <string id="30241">South Korea</string> + <string id="30242">Spain</string> + <string id="30243">Sweden</string> + <string id="30244">Taiwan</string> + <string id="30245">United States</string> + <string id="30246">Debug</string> + <string id="30247">Folder</string> + <string id="30248">Show Explore YouTube</string> + <string id="30249">Show Youtube Suggests</string> + <string id="30250">Show Watch Later</string> + <string id="30251">Show Liked Videos</string> + <string id="30252">Show Contacts</string> + <string id="30253">Show Favorites</string> + <string id="30254">Show Playlists</string> + <string id="30255">Show Subscriptions</string> + <string id="30256">Show Uploads</string> + <string id="30257">Show Downloads</string> + <string id="30258">Show Search</string> + <string id="30259">View mode in Confluence</string> + <string id="30260">Advanced</string> + <string id="30261">Max video quality (Downloading)</string> + <string id="30262">Same as playback</string> + <string id="30263">Show Youtube Disco!</string> + <string id="30264">Show Youtube Trailers</string> + <string id="30265">Timeout (in seconds)</string> + <string id="30266">Show Browse YouTube</string> + <string id="30267">Show YouTube Shows</string> + <string id="30268">Show YouTube Movies</string> + <string id="30269">Show Feeds</string> + <string id="30270">Explore YouTube</string> + <string id="30271">Video Thumbnails</string> + <string id="30272">Thumbnails Everywhere</string> + <string id="30273">XBMC Default</string> + <string id="30274">Transcribe Audio</string> + <string id="30276">Subtitle Language</string> + <string id="30277">Off</string> + <string id="30278">English</string> + <string id="30279">Spanish</string> + <string id="30280">German</string> + <string id="30281">French</string> + <string id="30282">Italian</string> + <string id="30283">Japanese</string> + <string id="30284">Enable annotations</string> + <string id="30285">Use YouTube cache servers</string> + <string id="30286">Show YouTube Music</string> + <string id="30287">Show YouTube Live</string> + <string id="30288">Show YouTube History</string> + + <!-- Menu strings --> + <string id="30500">Clear refinements</string> + <string id="30501">Download video</string> + <string id="30502">Video information</string> + <string id="30503">Add to My Favorites</string> + <string id="30504">Queue video</string> + <string id="30505">Refine to user</string> + <string id="30506">Delete from My Favorites</string> + <string id="30507">Similar artists</string> + <string id="30508">Delete search</string> + <string id="30509">More results</string> + <string id="30510">View favorites</string> + <string id="30511">View uploads</string> + <string id="30512">Subscribe to %s</string> + <string id="30513">Unsubscribe from %s</string> + <string id="30514">Find similar</string> + <string id="30515">Edit search</string> + <string id="30516">More videos by %s</string> + <string id="30517">Search on author</string> + <string id="30518">Select video quality</string> + <string id="30519">Enter contact name</string> + <string id="30520">Play All</string> + <string id="30521">Play from here</string> + <string id="30522">Shuffle and Play All</string> + <string id="30523">Now Playing...</string> + <string id="30524">Edit disco search</string> + <string id="30525">Delete disco search</string> + <string id="30526">View playlists</string> + <string id="30527">Related videos</string> + <string id="30528">Add to Playlist..</string> + <string id="30529">New Playlist..</string> + <string id="30530">Remove from Playlist</string> + <string id="30531">Reverse Playlist</string> + <string id="30539">Delete Playlist</string> + <string id="30540">Delete artist</string> + + <!-- messages --> + <string id="30600">Error</string> + <string id="30601">No results!</string> + <string id="30602">No recent activity</string> + <string id="30603">Playback failed</string> + <string id="30604">Download complete</string> + <string id="30605">Preparing download</string> + <string id="30606">Verify your age in a browser</string> + <string id="30607">Skipping download</string> + <string id="30608">YouTube doesn't seem to know this video id!</string> + <string id="30609">Login Failed</string> + <string id="30610">Refinement cleared</string> + <string id="30611">Download folder not set</string> + <string id="30612">Live steam not available!</string> + <string id="30613">Added contact</string> + <string id="30614">Removed contact</string> + <string id="30615">Playlist empty</string> + <string id="30616">Refinement added</string> + <string id="30617">Unknown error</string> + <string id="30618">Couldn't locate video url</string> + <string id="30619">RTMPe download not supported</string> + <string id="30620">Stream type not supported</string> + <string id="30621">Bad username or password</string> + <string id="30622">Playback requires valid YouTube account</string> + <string id="30623">Maximum login attempts tried.</string> + <string id="30624">Downloading</string> + <string id="30625">Download Failed</string> + <string id="30626">RTMPe playback currently not supported</string> + <string id="30627">Please provide 2-factor authentication PIN</string> + <string id="30628">Please provide your password</string> + <string id="30629">SSL protocol not supported.</string> + + <!-- Dorfelite --> + <string id="31337">Dorfelite</string> +</strings>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/Russian/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">Каналы YouTube</string> + <string id="30002">Избранные</string> + <string id="30003">Подписки</string> + <string id="30004">Новые в подписках</string> + <string id="30005">Опубликованные</string> + <string id="30006">Поиск</string> + <string id="30007">Поиск...</string> + <string id="30008">Видео по ID</string> + <string id="30009">Макс. комментариев</string> + <string id="30010">Макс. ссылок</string> + <string id="30011">Макс. просмотров</string> + <string id="30012">Самое свежее</string> + <string id="30013">Макс. ответов</string> + <string id="30014">Рекомендуемое</string> + <string id="30015">Макс. в избранное</string> + <string id="30016">Макс. оценки</string> + <string id="30017">Плейлисты</string> + <string id="30018">Контакты</string> + <string id="30019">YouTube предлагает</string> + <string id="30020">Избранные</string> + <string id="30021">Подписки</string> + <string id="30022">Опубликованные</string> + <string id="30023">Плейлисты</string> + <string id="30024">Добавить контакт...</string> + <string id="30025">Удалить контакт</string> + <string id="30026">Добавить контакт</string> + <string id="30027">Вход...</string> + <string id="30028">Настройки</string> + <string id="30029">Контакт</string> + <string id="30030">Загрузка...</string> + <string id="30031">Вход ОК</string> + <string id="30032">Трейлеры</string> + <string id="30033">Популярные</string> + <string id="30034">В кинотеатрах</string> + <string id="30035">Последние</string> + <string id="30036">Скоро</string> + <string id="30037">YouTube Disco!</string> + <string id="30038">Лучшие 25</string> + <string id="30039">Популярные артисты</string> + <string id="30040">Поиск Disco...</string> + <string id="30041">Категории YouTube</string> + <string id="30042">Показывает YouTube</string> + <string id="30043">Фильмы YouTube</string> + <string id="30044">YouTube представляет</string> + <string id="30045">Закачки</string> + + <!-- Plugin settings strings --> + <string id="30200">Имя</string> + <string id="30201">Пароль</string> + <string id="30202">Период</string> + <string id="30203">Все</string> + <string id="30204">Сегодня</string> + <string id="30205">Неделя</string> + <string id="30206">Месяц</string> + <string id="30207">Папка закачек</string> + <string id="30208">Макс. качество видео</string> + <string id="30209">Безопасный поиск</string> + <string id="30210">Видео на странице</string> + <string id="30211">Сохраненных поисков</string> + <string id="30212">Вид папки</string> + <string id="30213">Вид видео</string> + <string id="30214">Уведомления (сек.)</string> + <string id="30215">1080p</string> + <string id="30216">720p</string> + <string id="30217">SD only</string> + <string id="30218">Выкл.</string> + <string id="30219">Умеренно</string> + <string id="30220">Строго</string> + <string id="30221">Регион</string> + <string id="30222">Все</string> + <string id="30223">Australia</string> + <string id="30224">Brazil</string> + <string id="30225">Canada</string> + <string id="30226">Czech Republic</string> + <string id="30227">France</string> + <string id="30228">Germany</string> + <string id="30229">Great Britain</string> + <string id="30230">The Netherlands</string> + <string id="30231">Hong Kong</string> + <string id="30232">India</string> + <string id="30233">Ireland</string> + <string id="30234">Israel</string> + <string id="30235">Italy</string> + <string id="30236">Japan</string> + <string id="30237">Mexico</string> + <string id="30238">New Zealand</string> + <string id="30239">Poland</string> + <string id="30240">Россия</string> + <string id="30241">South Korea</string> + <string id="30242">Spain</string> + <string id="30243">Sweden</string> + <string id="30244">Taiwan</string> + <string id="30245">United States</string> + <string id="30246">Debug</string> + <string id="30247">Основные</string> + <string id="30248">Папка</string> + <string id="30249">Показывать Каналы</string> + <string id="30250">Показывать YT предлагает</string> + <string id="30251">Показывать Контакты</string> + <string id="30252">Показывать Избранное</string> + <string id="30253">Показывать Плейлисты</string> + <string id="30254">Показывать Подписки</string> + <string id="30255">Показывать Опубликованное</string> + <string id="30256">Показывать Поиск</string> + <string id="30257">Показывать Видео по ID</string> + <string id="30258">Вид в Confluence</string> + <string id="30259">Еще</string> + <string id="30260">Качество загрузки</string> + <string id="30261">как и просмотр</string> + <string id="30262">Выбор качества</string> + <string id="30263">Показывать Youtube Disco!</string> + <string id="30264">Показывать Трейлеры</string> + <string id="30265">Задержка (сек.)</string> + <string id="30266">Показывать Browse YouTube</string> + <string id="30267">Показывать Показывает YT</string> + <string id="30268">Показывать Фильмы</string> + <string id="30269">Показывать YT представляет</string> + <string id="30270">YouTube представляет</string> + <string id="30271">Эскизы видео</string> + <string id="30272">Эскизы везде</string> + <string id="30273">По умолчанию XBMC</string> + <string id="30274">Показывать Загрузки</string> + + <!-- Menu strings --> + <string id="30500">Очистить уточнения</string> + <string id="30501">Скачать видео</string> + <string id="30502">Свойства видео</string> + <string id="30503">В Избранное</string> + <string id="30504">В Плейлист</string> + <string id="30505">Refine to user</string> + <string id="30506">Убрать из Избранного</string> + <string id="30507">Папку в плейлист</string> + <string id="30508">Удалить поиск</string> + <string id="30509">Еще</string> + <string id="30510">Изменение подписки в избранное</string> + <string id="30511">Изменение подписки в опубликованное</string> + <string id="30512">Подписаться на %s</string> + <string id="30513">Отменить подписку на %s</string> + <string id="30514">Найти похожие</string> + <string id="30515">Изменить поиск</string> + <string id="30516">Еще видео от %s</string> + <string id="30517">Искать по автору</string> + <string id="30518">Видео ID</string> + <string id="30519">Ввведите имя контакта</string> + <string id="30520">Воспроизвести в 1080p</string> + <string id="30521">Воспроизвести в 720p</string> + <string id="30522">Воспроизвести в SD</string> + <string id="30523">%s Disco!</string> + <string id="30524">Изменить поиск Disco</string> + <string id="30525">Удалить поиск Disco</string> + <string id="30526">Воспроизвести папку</string> + <string id="30527">Сейчас играет...</string> + <string id="30528">Изменение подписки на плейлисты</string> + <string id="30529">Похожие видео</string> + + <!-- messages --> + <string id="30600">Ошибка</string> + <string id="30601">Нет результатов!</string> + <string id="30602">Нет новых</string> + <string id="30603">Воспроизведение не удалось</string> + <string id="30604">Загрузка завершена</string> + <string id="30605">YouTube не отвечает</string> + <string id="30606">Неизвестная ошибка:</string> + <string id="30607">Нет подходящего разрешения</string> + <string id="30608">Только для совершеннолетних!</string> + <string id="30609">Ошибка входа</string> + <string id="30610">Уточнение очищено</string> + <string id="30611">Папка для загрузок не задана</string> + <string id="30612">Начинаю закачку</string> + <string id="30613">Контакт добавлен</string> + <string id="30614">Контакт удален</string> + <string id="30615">Не найдено</string> + <string id="30616">Уточнение </string> + <string id="30617">Неизвестная ошибка</string> + <string id="30618">Не удается извлечь адрес видео</string> + <string id="30619">Не удалось очистить YouTube</string> + <string id="30620">Формат не поддерживается</string> + <string id="30621">Неправильное имя или пароль</string> + <string id="30622">Воспроизведение не удалось, попробуйте войти в аккаунт</string> + <string id="30623">Достигнуто максимальное колличество попыток входа.</string> +</strings>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/language/Bulgarian/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,218 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">Емисии от YouTube</string> + <string id="30002">Любимите ви</string> + <string id="30003">Абонаментите ви</string> + <string id="30004">Нови видеа от абонаментите</string> + <string id="30005">Видеа, които сте качили</string> + <string id="30006">Търси</string> + <string id="30007">Търсене за...</string> + <string id="30008">За гледане по-късно</string> + <string id="30009">Най-дискутираните</string> + <string id="30010">С най-много препратки</string> + <string id="30011">Най-гледаните</string> + <string id="30012">Добавени най-скоро</string> + <string id="30013">С най-много отговори</string> + <string id="30014">Recently Featured</string> + <string id="30015">Най-харесваните</string> + <string id="30016">Най-високо оценените</string> + <string id="30017">Моите плейлисти</string> + <string id="30018">Моите контакти</string> + <string id="30019">YouTube Предлага</string> + <string id="30020">Харесвания</string> + <string id="30021">Абонаменти</string> + <string id="30022">Качвания</string> + <string id="30023">Плейлисти</string> + <string id="30024">Добавяне на контакт...</string> + <string id="30025">Премахни контакт</string> + <string id="30026">Добави контакт</string> + <string id="30027">Вписване..</string> + <string id="30028">Променете настройките на добавката</string> + <string id="30029">Контакт</string> + <string id="30030">Обновяване на папката..</string> + <string id="30031">Вписахте се успешно</string> + <string id="30032">YouTube Трейлъри</string> + <string id="30033">Популярни</string> + <string id="30034">В кината</string> + <string id="30035">Най-новите</string> + <string id="30036">Предстоящи филми</string> + <string id="30037">YouTube Диско!</string> + <string id="30038">Top 50</string> + <string id="30039">Най-популярните артисти</string> + <string id="30040">Диско търсене...</string> + <string id="30041">YouTube Категории</string> + <string id="30042">YouTube Предавания</string> + <string id="30043">YouTube Филми</string> + <string id="30044">Explore YouTube</string> + <string id="30045">Свалени</string> + <string id="30046">Популярни игри</string> + <string id="30047">Най-новите игри</string> + <string id="30048">Скоро излизащи игри</string> + <string id="30049">Модерни за момента видеа</string> + <string id="30050">Най-споделяните</string> + <string id="30051">YouTube На живо</string> + <string id="30052">YouTube Музика</string> + <string id="30053">Препоръчана музика</string> + <string id="30054">Популярни артисти</string> + <string id="30055">YouTube Top 100 музикална класация</string> + <string id="30056">Видеа, които сте харесали</string> + <string id="30057">YouTube EDU (Образование)</string> + <string id="30058">Сезон %s</string> + <string id="30059">Моята история</string> + + <!-- Plugin settings strings --> + <string id="30200">Потребител</string> + <string id="30201">Парола</string> + <string id="30203">За всички времена</string> + <string id="30204">Днес</string> + <string id="30205">Тази седмица</string> + <string id="30206">Този месец</string> + <string id="30207">Място за сваляне</string> + <string id="30208">Макс качество на видеото</string> + <string id="30209">Включи безопасния режим на YouTube</string> + <string id="30210">Видеа на страница</string> + <string id="30211">Запазвай последните (търсения)</string> + <string id="30212">Времетраене на известията (сек)</string> + <string id="30215">само SD</string> + <string id="30216">720p</string> + <string id="30217">1080p</string> + <string id="30218">General</string> + <string id="30219">Умерен</string> + <string id="30220">Стриктен</string> + <string id="30221">Показвай YouTube емисии само от следния район</string> + <string id="30222">Всички</string> + <string id="30223">Австралия</string> + <string id="30224">Бразилия</string> + <string id="30225">Канада</string> + <string id="30226">Чехия</string> + <string id="30227">Франция</string> + <string id="30228">Германия</string> + <string id="30229">Великобритания</string> + <string id="30230">Холандия</string> + <string id="30231">Хонконг</string> + <string id="30232">Индия</string> + <string id="30233">Ирландия</string> + <string id="30234">Израел</string> + <string id="30235">Италия</string> + <string id="30236">Япония</string> + <string id="30237">Мексико</string> + <string id="30238">Нова Зеландия</string> + <string id="30239">Полша</string> + <string id="30240">Русия</string> + <string id="30241">Южна Кореа</string> + <string id="30242">Испания</string> + <string id="30243">Швеция</string> + <string id="30244">Тайван</string> + <string id="30245">Съединени американски щати</string> + <string id="30246">Дебъг</string> + <string id="30247">Папка</string> + <string id="30248">Показвай Explore YouTube</string> + <string id="30249">Показвай Предложения от Youtube</string> + <string id="30250">Показвай За гледане по-късно</string> + <string id="30251">Показвай Харесвания</string> + <string id="30252">Показвай Контакти</string> + <string id="30253">Показвай Любими</string> + <string id="30254">Показвай Плейлисти</string> + <string id="30255">Показвай Абонаменти</string> + <string id="30256">Показвай Качените видеа</string> + <string id="30257">Показвай Свалени</string> + <string id="30258">Показвай Търсения</string> + <string id="30259">Вид в Confluence</string> + <string id="30260">Допълнителни</string> + <string id="30261">Макс качество на видеото (при сваляне)</string> + <string id="30262">= на гледаното видео</string> + <string id="30263">Показвай Youtube Диско!</string> + <string id="30264">Показвай Youtube Трейлъри</string> + <string id="30265">Времетраене (сек)</string> + <string id="30266">Показвай Browse YouTube</string> + <string id="30267">Показвай YouTube Предавания</string> + <string id="30268">Показвай YouTube Филми</string> + <string id="30269">Показвай емисиите</string> + <string id="30270">Explore YouTube</string> + <string id="30271">Видео миниатюри</string> + <string id="30272">Миниатюри навсякъде</string> + <string id="30273">Стандартния за XBMC</string> + <string id="30274">Транскрибирай аудиото</string> + <string id="30276">Език на субтитрите</string> + <string id="30277">Изкл</string> + <string id="30278">Английски</string> + <string id="30279">Испанки</string> + <string id="30280">Немски</string> + <string id="30281">Френски</string> + <string id="30282">Италиански</string> + <string id="30283">Японкси</string> + <string id="30284">Включи поясненията</string> + <string id="30285">Ползвай кеш сървърите на YouTube</string> + <string id="30286">Показвай YouTube Музика</string> + <string id="30287">Показвай YouTube На живо</string> + + <!-- Menu strings --> + <string id="30500">Clear refinements</string> + <string id="30501">Свали видеото</string> + <string id="30502">Информация за видеото</string> + <string id="30503">Добави към любимите ми</string> + <string id="30504">Сложи на опашката</string> + <string id="30505">Refine to user</string> + <string id="30506">Изтрий от любимите ми</string> + <string id="30507">Сходни артисти</string> + <string id="30508">Изтрий търсенето</string> + <string id="30509">Още резултати</string> + <string id="30510">Покажи любимите</string> + <string id="30511">Покажи качените</string> + <string id="30512">Абониране за %s</string> + <string id="30513">Отписване от %s</string> + <string id="30514">Намери сходни</string> + <string id="30515">Редактирай търсенето</string> + <string id="30516">Още видеа от %s</string> + <string id="30517">Търси по автор</string> + <string id="30518">Изберете качество за видеото</string> + <string id="30519">Въведете име на контакта</string> + <string id="30520">Възпроизведи всички</string> + <string id="30521">Възпроизведи от тук</string> + <string id="30522">Разбъркай и възпроизведи всички</string> + <string id="30523">Сега се възпроизвежда...</string> + <string id="30524">Редактирай диско търсенето</string> + <string id="30525">Изтрий диско търсенето</string> + <string id="30526">Покажи плейлистите</string> + <string id="30527">Свързани видеа</string> + <string id="30528">Добави в плейлист.</string> + <string id="30529">Нов плейлист..</string> + <string id="30530">Премахни от плейлиста</string> + <string id="30531">Преобърни плейлиста</string> + <string id="30539">Изтрий плейлиста</string> + <string id="30540">Изтрий артиста</string> + + <!-- messages --> + <string id="30600">Грешка</string> + <string id="30601">Няма резултати!</string> + <string id="30602">Няма скорошна активност</string> + <string id="30603">Възпроизвеждането се провали</string> + <string id="30604">Свалянето завърши</string> + <string id="30605">Подготовка за сваляне</string> + <string id="30606">Потвърдете възрастта си браузър</string> + <string id="30607">Пропускане на сваляне</string> + <string id="30608">Няма видео с такъв идентификатор в YouTube!</string> + <string id="30609">Вписването се провали</string> + <string id="30610">Refinement cleared</string> + <string id="30611">Не сте задали папка за сваляне</string> + <string id="30612">Не е наличен поток На Живо!</string> + <string id="30613">Добавен е контакт</string> + <string id="30614">Премахнат е контакт</string> + <string id="30615">Плейлиста е празен</string> + <string id="30616">Refinement added</string> + <string id="30617">Непозната грешка</string> + <string id="30618">Не може да бъде засечен url адреса на видеото</string> + <string id="30619">Не се поддържат RTMPe сваляния</string> + <string id="30620">Типът на потока не се поддържа</string> + <string id="30621">Грешно потребителско име или парола</string> + <string id="30622">Възпроизвеждането изисква валиден YouTube профил</string> + <string id="30623">Достигнахте макс. брой опити за вписване.</string> + <string id="30624">Сваляне</string> + <string id="30625">Свалянето се провали</string> + <string id="30626">За момента не се поддържат RTMPe потоци</string> + <string id="30627">Моля, въведете кода за потвърждаване в две стъпки</string> + <string id="30628">Моля, въведете паролата си</string> + <string id="30629">SSL протоколът не се поддържа.</string> + +</strings> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/language/English/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">YouTube Feeds</string> + <string id="30002">My Favorites</string> + <string id="30003">My Subscriptions</string> + <string id="30004">New Subscription Videos</string> + <string id="30005">My Uploads</string> + <string id="30006">Search</string> + <string id="30007">Search...</string> + <string id="30008">My Watch Later</string> + <string id="30009">Most Discussed</string> + <string id="30010">Most Linked</string> + <string id="30011">Most Viewed</string> + <string id="30012">Most Recent</string> + <string id="30013">Most Responded</string> + <string id="30014">Recently Featured</string> + <string id="30015">Top Favorites</string> + <string id="30016">Top Rated</string> + <string id="30017">My Playlists</string> + <string id="30018">My Contacts</string> + <string id="30019">YouTube Suggests</string> + <string id="30020">Favorites</string> + <string id="30021">Subscriptions</string> + <string id="30022">Uploads</string> + <string id="30023">Playlists</string> + <string id="30024">Add Contact...</string> + <string id="30025">Remove Contact</string> + <string id="30026">Add Contact</string> + <string id="30027">Login..</string> + <string id="30028">Change plugin settings</string> + <string id="30029">Contact</string> + <string id="30030">Refreshing folder..</string> + <string id="30031">Login success</string> + <string id="30032">YouTube Trailers</string> + <string id="30033">Popular</string> + <string id="30034">In Theaters</string> + <string id="30035">Latest</string> + <string id="30036">Opening Soon</string> + <string id="30037">YouTube Disco!</string> + <string id="30038">Top 50</string> + <string id="30039">Most popular artists</string> + <string id="30040">Disco search...</string> + <string id="30041">YouTube Categories</string> + <string id="30042">YouTube Shows</string> + <string id="30043">YouTube Movies</string> + <string id="30044">Explore YouTube</string> + <string id="30045">My Downloads</string> + <string id="30046">Popular Games</string> + <string id="30047">Latest Games</string> + <string id="30048">Games Coming Soon</string> + <string id="30049">Trending Videos</string> + <string id="30050">Most Shared</string> + <string id="30051">YouTube Live</string> + <string id="30052">YouTube Music</string> + <string id="30053">Recommended Music</string> + <string id="30054">Popular Artists</string> + <string id="30055">YouTube Top 100 Music Chart</string> + <string id="30056">My Liked Videos</string> + <string id="30057">YouTube Education</string> + <string id="30058">Season %s</string> + <string id="30059">My History</string> + + + <!-- Plugin settings strings --> + <string id="30200">Username</string> + <string id="30201">Password</string> + <string id="30203">All time</string> + <string id="30204">Today</string> + <string id="30205">This week</string> + <string id="30206">This month</string> + <string id="30207">Download Location</string> + <string id="30208">Max video quality</string> + <string id="30209">Enable YouTube SafeSearch</string> + <string id="30210">Videos per page</string> + <string id="30211">Searches to save</string> + <string id="30212">Notification length in seconds</string> + <string id="30213">Ask</string> + <string id="30215">SD only</string> + <string id="30216">720p</string> + <string id="30217">1080p</string> + <string id="30218">General</string> + <string id="30219">Moderate</string> + <string id="30220">Strict</string> + <string id="30221">YouTube feeds country region</string> + <string id="30222">All</string> + <string id="30223">Australia</string> + <string id="30224">Brazil</string> + <string id="30225">Canada</string> + <string id="30226">Czech Republic</string> + <string id="30227">France</string> + <string id="30228">Germany</string> + <string id="30229">Great Britain</string> + <string id="30230">Holland</string> + <string id="30231">Hong Kong</string> + <string id="30232">India</string> + <string id="30233">Ireland</string> + <string id="30234">Israel</string> + <string id="30235">Italy</string> + <string id="30236">Japan</string> + <string id="30237">Mexico</string> + <string id="30238">New Zealand</string> + <string id="30239">Poland</string> + <string id="30240">Russia</string> + <string id="30241">South Korea</string> + <string id="30242">Spain</string> + <string id="30243">Sweden</string> + <string id="30244">Taiwan</string> + <string id="30245">United States</string> + <string id="30246">Debug</string> + <string id="30247">Folder</string> + <string id="30248">Show Explore YouTube</string> + <string id="30249">Show Youtube Suggests</string> + <string id="30250">Show Watch Later</string> + <string id="30251">Show Liked Videos</string> + <string id="30252">Show Contacts</string> + <string id="30253">Show Favorites</string> + <string id="30254">Show Playlists</string> + <string id="30255">Show Subscriptions</string> + <string id="30256">Show Uploads</string> + <string id="30257">Show Downloads</string> + <string id="30258">Show Search</string> + <string id="30259">View mode in Confluence</string> + <string id="30260">Advanced</string> + <string id="30261">Max video quality (Downloading)</string> + <string id="30262">Same as playback</string> + <string id="30263">Show Youtube Disco!</string> + <string id="30264">Show Youtube Trailers</string> + <string id="30265">Timeout (in seconds)</string> + <string id="30266">Show Browse YouTube</string> + <string id="30267">Show YouTube Shows</string> + <string id="30268">Show YouTube Movies</string> + <string id="30269">Show Feeds</string> + <string id="30270">Explore YouTube</string> + <string id="30271">Video Thumbnails</string> + <string id="30272">Thumbnails Everywhere</string> + <string id="30273">XBMC Default</string> + <string id="30274">Transcribe Audio</string> + <string id="30276">Subtitle Language</string> + <string id="30277">Off</string> + <string id="30278">English</string> + <string id="30279">Spanish</string> + <string id="30280">German</string> + <string id="30281">French</string> + <string id="30282">Italian</string> + <string id="30283">Japanese</string> + <string id="30284">Enable annotations</string> + <string id="30285">Use YouTube cache servers</string> + <string id="30286">Show YouTube Music</string> + <string id="30287">Show YouTube Live</string> + + <!-- Menu strings --> + <string id="30500">Clear refinements</string> + <string id="30501">Download video</string> + <string id="30502">Video information</string> + <string id="30503">Add to My Favorites</string> + <string id="30504">Queue video</string> + <string id="30505">Refine to user</string> + <string id="30506">Delete from My Favorites</string> + <string id="30507">Similar artists</string> + <string id="30508">Delete search</string> + <string id="30509">More results</string> + <string id="30510">View favorites</string> + <string id="30511">View uploads</string> + <string id="30512">Subscribe to %s</string> + <string id="30513">Unsubscribe from %s</string> + <string id="30514">Find similar</string> + <string id="30515">Edit search</string> + <string id="30516">More videos by %s</string> + <string id="30517">Search on author</string> + <string id="30518">Select video quality</string> + <string id="30519">Enter contact name</string> + <string id="30520">Play All</string> + <string id="30521">Play from here</string> + <string id="30522">Shuffle and Play All</string> + <string id="30523">Now Playing...</string> + <string id="30524">Edit disco search</string> + <string id="30525">Delete disco search</string> + <string id="30526">View playlists</string> + <string id="30527">Related videos</string> + <string id="30528">Add to Playlist..</string> + <string id="30529">New Playlist..</string> + <string id="30530">Remove from Playlist</string> + <string id="30531">Reverse Playlist</string> + <string id="30539">Delete Playlist</string> + <string id="30540">Delete artist</string> + + <!-- messages --> + <string id="30600">Error</string> + <string id="30601">No results!</string> + <string id="30602">No recent activity</string> + <string id="30603">Playback failed</string> + <string id="30604">Download complete</string> + <string id="30605">Preparing download</string> + <string id="30606">Verify your age in a browser</string> + <string id="30607">Skipping download</string> + <string id="30608">YouTube doesn't seem to know this video id!</string> + <string id="30609">Login Failed</string> + <string id="30610">Refinement cleared</string> + <string id="30611">Download folder not set</string> + <string id="30612">Live steam not available!</string> + <string id="30613">Added contact</string> + <string id="30614">Removed contact</string> + <string id="30615">Playlist empty</string> + <string id="30616">Refinement added</string> + <string id="30617">Unknown error</string> + <string id="30618">Couldn't locate video url</string> + <string id="30619">RTMPe download not supported</string> + <string id="30620">Stream type not supported</string> + <string id="30621">Bad username or password</string> + <string id="30622">Playback requires valid YouTube account</string> + <string id="30623">Maximum login attempts tried.</string> + <string id="30624">Downloading</string> + <string id="30625">Download Failed</string> + <string id="30626">RTMPe playback currently not supported</string> + <string id="30627">Please provide 2-factor authentication PIN</string> + <string id="30628">Please provide your password</string> + <string id="30629">SSL protocol not supported.</string> + + <!-- Dorfelite --> + <string id="31337">Dorfelite</string> +</strings>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/language/language/Russian/strings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<strings> + <!-- Category strings --> + <string id="30001">Каналы YouTube</string> + <string id="30002">Избранные</string> + <string id="30003">Подписки</string> + <string id="30004">Новые в подписках</string> + <string id="30005">Опубликованные</string> + <string id="30006">Поиск</string> + <string id="30007">Поиск...</string> + <string id="30008">Видео по ID</string> + <string id="30009">Макс. комментариев</string> + <string id="30010">Макс. ссылок</string> + <string id="30011">Макс. просмотров</string> + <string id="30012">Самое свежее</string> + <string id="30013">Макс. ответов</string> + <string id="30014">Рекомендуемое</string> + <string id="30015">Макс. в избранное</string> + <string id="30016">Макс. оценки</string> + <string id="30017">Плейлисты</string> + <string id="30018">Контакты</string> + <string id="30019">YouTube предлагает</string> + <string id="30020">Избранные</string> + <string id="30021">Подписки</string> + <string id="30022">Опубликованные</string> + <string id="30023">Плейлисты</string> + <string id="30024">Добавить контакт...</string> + <string id="30025">Удалить контакт</string> + <string id="30026">Добавить контакт</string> + <string id="30027">Вход...</string> + <string id="30028">Настройки</string> + <string id="30029">Контакт</string> + <string id="30030">Загрузка...</string> + <string id="30031">Вход ОК</string> + <string id="30032">Трейлеры</string> + <string id="30033">Популярные</string> + <string id="30034">В кинотеатрах</string> + <string id="30035">Последние</string> + <string id="30036">Скоро</string> + <string id="30037">YouTube Disco!</string> + <string id="30038">Лучшие 25</string> + <string id="30039">Популярные артисты</string> + <string id="30040">Поиск Disco...</string> + <string id="30041">Категории YouTube</string> + <string id="30042">Показывает YouTube</string> + <string id="30043">Фильмы YouTube</string> + <string id="30044">YouTube представляет</string> + <string id="30045">Закачки</string> + + <!-- Plugin settings strings --> + <string id="30200">Имя</string> + <string id="30201">Пароль</string> + <string id="30202">Период</string> + <string id="30203">Все</string> + <string id="30204">Сегодня</string> + <string id="30205">Неделя</string> + <string id="30206">Месяц</string> + <string id="30207">Папка закачек</string> + <string id="30208">Макс. качество видео</string> + <string id="30209">Безопасный поиск</string> + <string id="30210">Видео на странице</string> + <string id="30211">Сохраненных поисков</string> + <string id="30212">Вид папки</string> + <string id="30213">Вид видео</string> + <string id="30214">Уведомления (сек.)</string> + <string id="30215">1080p</string> + <string id="30216">720p</string> + <string id="30217">SD only</string> + <string id="30218">Выкл.</string> + <string id="30219">Умеренно</string> + <string id="30220">Строго</string> + <string id="30221">Регион</string> + <string id="30222">Все</string> + <string id="30223">Australia</string> + <string id="30224">Brazil</string> + <string id="30225">Canada</string> + <string id="30226">Czech Republic</string> + <string id="30227">France</string> + <string id="30228">Germany</string> + <string id="30229">Great Britain</string> + <string id="30230">The Netherlands</string> + <string id="30231">Hong Kong</string> + <string id="30232">India</string> + <string id="30233">Ireland</string> + <string id="30234">Israel</string> + <string id="30235">Italy</string> + <string id="30236">Japan</string> + <string id="30237">Mexico</string> + <string id="30238">New Zealand</string> + <string id="30239">Poland</string> + <string id="30240">Россия</string> + <string id="30241">South Korea</string> + <string id="30242">Spain</string> + <string id="30243">Sweden</string> + <string id="30244">Taiwan</string> + <string id="30245">United States</string> + <string id="30246">Debug</string> + <string id="30247">Основные</string> + <string id="30248">Папка</string> + <string id="30249">Показывать Каналы</string> + <string id="30250">Показывать YT предлагает</string> + <string id="30251">Показывать Контакты</string> + <string id="30252">Показывать Избранное</string> + <string id="30253">Показывать Плейлисты</string> + <string id="30254">Показывать Подписки</string> + <string id="30255">Показывать Опубликованное</string> + <string id="30256">Показывать Поиск</string> + <string id="30257">Показывать Видео по ID</string> + <string id="30258">Вид в Confluence</string> + <string id="30259">Еще</string> + <string id="30260">Качество загрузки</string> + <string id="30261">как и просмотр</string> + <string id="30262">Выбор качества</string> + <string id="30263">Показывать Youtube Disco!</string> + <string id="30264">Показывать Трейлеры</string> + <string id="30265">Задержка (сек.)</string> + <string id="30266">Показывать Browse YouTube</string> + <string id="30267">Показывать Показывает YT</string> + <string id="30268">Показывать Фильмы</string> + <string id="30269">Показывать YT представляет</string> + <string id="30270">YouTube представляет</string> + <string id="30271">Эскизы видео</string> + <string id="30272">Эскизы везде</string> + <string id="30273">По умолчанию XBMC</string> + <string id="30274">Показывать Загрузки</string> + + <!-- Menu strings --> + <string id="30500">Очистить уточнения</string> + <string id="30501">Скачать видео</string> + <string id="30502">Свойства видео</string> + <string id="30503">В Избранное</string> + <string id="30504">В Плейлист</string> + <string id="30505">Refine to user</string> + <string id="30506">Убрать из Избранного</string> + <string id="30507">Папку в плейлист</string> + <string id="30508">Удалить поиск</string> + <string id="30509">Еще</string> + <string id="30510">Изменение подписки в избранное</string> + <string id="30511">Изменение подписки в опубликованное</string> + <string id="30512">Подписаться на %s</string> + <string id="30513">Отменить подписку на %s</string> + <string id="30514">Найти похожие</string> + <string id="30515">Изменить поиск</string> + <string id="30516">Еще видео от %s</string> + <string id="30517">Искать по автору</string> + <string id="30518">Видео ID</string> + <string id="30519">Ввведите имя контакта</string> + <string id="30520">Воспроизвести в 1080p</string> + <string id="30521">Воспроизвести в 720p</string> + <string id="30522">Воспроизвести в SD</string> + <string id="30523">%s Disco!</string> + <string id="30524">Изменить поиск Disco</string> + <string id="30525">Удалить поиск Disco</string> + <string id="30526">Воспроизвести папку</string> + <string id="30527">Сейчас играет...</string> + <string id="30528">Изменение подписки на плейлисты</string> + <string id="30529">Похожие видео</string> + + <!-- messages --> + <string id="30600">Ошибка</string> + <string id="30601">Нет результатов!</string> + <string id="30602">Нет новых</string> + <string id="30603">Воспроизведение не удалось</string> + <string id="30604">Загрузка завершена</string> + <string id="30605">YouTube не отвечает</string> + <string id="30606">Неизвестная ошибка:</string> + <string id="30607">Нет подходящего разрешения</string> + <string id="30608">Только для совершеннолетних!</string> + <string id="30609">Ошибка входа</string> + <string id="30610">Уточнение очищено</string> + <string id="30611">Папка для загрузок не задана</string> + <string id="30612">Начинаю закачку</string> + <string id="30613">Контакт добавлен</string> + <string id="30614">Контакт удален</string> + <string id="30615">Не найдено</string> + <string id="30616">Уточнение </string> + <string id="30617">Неизвестная ошибка</string> + <string id="30618">Не удается извлечь адрес видео</string> + <string id="30619">Не удалось очистить YouTube</string> + <string id="30620">Формат не поддерживается</string> + <string id="30621">Неправильное имя или пароль</string> + <string id="30622">Воспроизведение не удалось, попробуйте войти в аккаунт</string> + <string id="30623">Достигнуто максимальное колличество попыток входа.</string> +</strings>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/resources/settings.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<settings> + + <!-- General --> + <category label="30218"> + <setting id="username" type="text" label="30200" default="" /> + <setting id="user_password" type="text" option="hidden" label="30201" enable="!eq(-1,)" default="" /> + <setting type="sep" /> + <setting id="safe_search" type="enum" label="30209" lvalues="30277|30219|30220" default="1" /> + <setting id="hd_videos" type="enum" label="30208" lvalues="30213|30215|30216|30217" default="2" /> + <setting type="sep" /> + <setting id="download_path" type="folder" label="30207" default="" /> + <setting type="sep" /> + <setting id="lang_code" type="enum" label="30276" lvalues="30277|30278|30279|30280|30281|30282|30283" default="0" /> + <setting id="transcode" type="bool" label="30274" enable="!eq(-1,0)" default="false" /> + <setting id="annotations" type="bool" label="30284" enable="!eq(-2,0)" default="false" /> + </category> + + <!-- Advanced --> + <category label="30260"> + <setting id="list_view" type="enum" label="30259" lvalues="30271|30272|30273" default="0" /> + <setting id="perpage" type="enum" label="30210" values="10|15|20|25|30|40|50" default="6" /> + <setting id="saved_searches" type="enum" label="30211" values="10|20|30|40" default="3" /> + <setting type="sep" /> + <setting id="region_id" type="enum" label="30221" lvalues="30222|30223|30224|30225|30226|30227|30228|30229|30230|30231|30232|30233|30234|30235|30236|30237|30238|30239|30240|30241|30242|30243|30244|30245" default="0" /> + <setting id="hd_videos_download" type="enum" label="30261" lvalues="30262|30215|30216|30217" default="0" /> + <setting type="sep" /> + <setting id="preferred" type="bool" label="30285" default="true" /> + <setting id="notification_length" type="enum" label="30212" values="1|2|3|4|5|6|7|8|9|10" default="2" /> + <setting id="timeout" type="enum" label="30265" values="5|10|15|20|25" default="3" /> + <setting id="debug" type="bool" label="30246" default="false" /> + <setting type="sep" /> + </category> + + <!-- Folders --> + <category label="30247"> + <setting id="explore" type="bool" label="30248" default="true" /> + <setting id="recommended" type="bool" label="30249" default="true" /> + <setting id="watch_later" type="bool" label="30250" default="true" /> + <setting id="liked" type="bool" label="30251" default= "true" /> + <setting id="contacts" type="bool" label="30252" default="true" /> + <setting id="favorites" type="bool" label="30253" default="true" /> + <setting id="playlists" type="bool" label="30254" default="true" /> + <setting id="subscriptions" type="bool" label="30255" default="true" /> + <setting id="uploads" type="bool" label="30256" default="true" /> + <setting id="downloads" type="bool" label="30257" default="true" /> + <setting id="search" type="bool" label="30258" default="true" /> + <setting id="history" type="bool" label="30288" default="true" /> + </category> + + <!-- Explore YouTube --> + <category label="30270"> + <setting id="categories" type="bool" label="30266" default="true" /> + <setting id="disco" type="bool" label="30263" default="true" /> + <setting id="feeds" type="bool" label="30249" default="true" /> + <setting id="movies" type="bool" label="30268" default="true" /> + <setting id="music" type="bool" label="30286" default="true" /> + <setting id="shows" type="bool" label="30267" default="true" /> + <setting id="trailers" type="bool" label="30264" default="true" /> + <setting id="live" type="bool" label="30287" default="true" /> + </category> +</settings> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/storageserverdummy.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,96 @@ +''' + StorageServer override. + Version: 1.0 +''' +import xbmc +try: + import hashlib +except: + import md5 + +import xbmcvfs +import os +import time +import sys + +if hasattr(sys.modules["__main__"], "settings"): + settings = sys.modules["__main__"].settings +else: + settings = False + + +class StorageServer: + def __init__(self, table=False): + self.table = table + if settings: + temporary_path = xbmc.translatePath(settings.getAddonInfo("profile")) + if not xbmcvfs.exists(temporary_path): + os.makedirs(temporary_path) + + return None + + def cacheFunction(self, funct=False, *args): + result = "" + if not settings: + return funct(*args) + elif funct and self.table: + name = repr(funct) + if name.find(" of ") > -1: + name = name[name.find("method") + 7:name.find(" of ")] + elif name.find(" at ") > -1: + name = name[name.find("function") + 9:name.find(" at ")] + + # Build unique name + if "hashlib" in globals(): + keyhash = hashlib.md5() + else: + keyhash = md5.new() + + for params in args: + if isinstance(params, dict): + for key in sorted(params.iterkeys()): + if key not in ["new_results_function"]: + keyhash.update("'%s'='%s'" % (key, params[key])) + elif isinstance(params, list): + keyhash.update(",".join(["%s" % el for el in params])) + else: + try: + keyhash.update(params) + except: + keyhash.update(str(params)) + + name += "-" + keyhash.hexdigest() + ".cache" + + path = os.path.join(xbmc.translatePath(settings.getAddonInfo("profile")).decode("utf-8"), name) + if xbmcvfs.exists(path) and os.path.getmtime(path) > time.time() - 3600: + print "Getting cache : " + repr(path) + temp = open(path) + result = eval(temp.read()) + temp.close() + else: + print "Setting cache: " + repr(path) + result = funct(*args) + if len(result) > 0: + temp = open(path, "w") + temp.write(repr(result)) + temp.close() + + return result + + def set(self, name, data): + return "" + + def get(self, name): + return "" + + def setMulti(self, name, data): + return "" + + def getMulti(self, name, items): + return "" + + def lock(self, name): + return False + + def unlock(self, name): + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/plugin.video.youtube/xbmcvfsdummy.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,20 @@ +''' + XBMCVFS override for Dharma. + Version: 1.0 +''' + +import os + + +def exists(target): + return os.path.exists(target) + + +def rename(origin, target): + return os.rename(origin, target) + + +def delete(target): + if os.path.isfile(target) and not os.path.isdir(target): + return os.unlink(target) + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/script.module.parsedom/addon.xml Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,11 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes'?> +<addon id='script.module.parsedom' version='1.5.0' name='Parsedom for xbmc plugins' provider-name='TheCollective'> + <requires> + <import addon='xbmc.python' version='2.0'/> + </requires> + <extension point='xbmc.addon.metadata'> + <platform>all</platform> + <summary lang='en'>Parsedom for xbmc plugins.</summary> + </extension> + <extension point='xbmc.python.module' library='lib' /> +</addon>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/script.module.parsedom/changelog.txt Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,30 @@ +[B]Version 1.5.0[/B] +- Fixed: proper fix for getParameters that only affects Frodo branch +- Added: new function to get the version of xbmc as a float + + +[B]Version 1.4.0[/B] +- Special fix for eden branch to unbreak changes for Frodo + +[B]Version 1.3.0[/B] +- Team xbmc decided to stop unquote-ing their path strings, so getParams now does it for them + +[B]Version 1.2.0[/B] +- fetchPage should default to utf-8 encoding +- parseDOM should handle utf-8 encoding + +[B]Version 1.1.0[/B] +- Handle \t that breaks DOM variable extraction +- Added extractJS function + +[B]Version 1.0.0[/B] +- Minor fixes + +[B]Version 0.9.1[/B] +- Stability and more functions +- Add cookie support to fetchPage. +- Add getCookieInfoAsHTML. +- Add POST and Refering capabilities to fetchPage + +[B]Version 0.9.0[/B] +- Initial public test run.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/script.module.parsedom/lib/CommonFunctions.py Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,537 @@ +''' + Parsedom for XBMC plugins + Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib +import urllib2 +import re +import io +import inspect +import time +import HTMLParser +#import chardet +import json + +version = u"1.4.0" +plugin = u"CommonFunctions-" + version +print plugin + +USERAGENT = u"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" + +if hasattr(sys.modules["__main__"], "xbmc"): + xbmc = sys.modules["__main__"].xbmc +else: + import xbmc + +if hasattr(sys.modules["__main__"], "xbmcgui"): + xbmcgui = sys.modules["__main__"].xbmcgui +else: + import xbmcgui + +if hasattr(sys.modules["__main__"], "dbg"): + dbg = sys.modules["__main__"].dbg +else: + dbg = False + +if hasattr(sys.modules["__main__"], "dbglevel"): + dbglevel = sys.modules["__main__"].dbglevel +else: + dbglevel = 3 + +if hasattr(sys.modules["__main__"], "opener"): + urllib2.install_opener(sys.modules["__main__"].opener) + + +# This function raises a keyboard for user input +def getUserInput(title=u"Input", default=u"", hidden=False): + log("", 5) + result = None + + # Fix for when this functions is called with default=None + if not default: + default = u"" + + keyboard = xbmc.Keyboard(default, title) + keyboard.setHiddenInput(hidden) + keyboard.doModal() + + if keyboard.isConfirmed(): + result = keyboard.getText() + + log(repr(result), 5) + return result + + +# This function raises a keyboard numpad for user input +def getUserInputNumbers(title=u"Input", default=u""): + log("", 5) + result = None + + # Fix for when this functions is called with default=None + if not default: + default = u"" + + keyboard = xbmcgui.Dialog() + result = keyboard.numeric(0, title, default) + + log(repr(result), 5) + return str(result) + + +# Converts the request url passed on by xbmc to the plugin into a dict of key-value pairs +def getParameters(parameterString): + log("", 5) + commands = {} + splitCommands = parameterString[parameterString.find('?') + 1:].split('&') + + for command in splitCommands: + if (len(command) > 0): + splitCommand = command.split('=') + key = splitCommand[0] + value = splitCommand[1] + commands[key] = value + + log(repr(commands), 5) + return commands + + +def replaceHTMLCodes(txt): + log(repr(txt), 5) + + # Fix missing ; in &#<number>; + txt = re.sub("(&#[0-9]+)([^;^0-9]+)", "\\1;\\2", makeUTF8(txt)) + + txt = HTMLParser.HTMLParser().unescape(txt) + txt = txt.replace("&", "&") + log(repr(txt), 5) + return txt + + +def stripTags(html): + log(repr(html), 5) + sub_start = html.find("<") + sub_end = html.find(">") + while sub_start < sub_end and sub_start > -1: + html = html.replace(html[sub_start:sub_end + 1], "").strip() + sub_start = html.find("<") + sub_end = html.find(">") + + log(repr(html), 5) + return html + + +def _getDOMContent(html, name, match, ret): # Cleanup + log("match: " + match, 3) + + endstr = u"</" + name # + ">" + + start = html.find(match) + end = html.find(endstr, start) + pos = html.find("<" + name, start + 1 ) + + log(str(start) + " < " + str(end) + ", pos = " + str(pos) + ", endpos: " + str(end), 8) + + while pos < end and pos != -1: # Ignore too early </endstr> return + tend = html.find(endstr, end + len(endstr)) + if tend != -1: + end = tend + pos = html.find("<" + name, pos + 1) + log("loop: " + str(start) + " < " + str(end) + " pos = " + str(pos), 8) + + log("start: %s, len: %s, end: %s" % (start, len(match), end), 3) + if start == -1 and end == -1: + result = u"" + elif start > -1 and end > -1: + result = html[start + len(match):end] + elif end > -1: + result = html[:end] + elif start > -1: + result = html[start + len(match):] + + if ret: + endstr = html[end:html.find(">", html.find(endstr)) + 1] + result = match + result + endstr + + log("done result length: " + str(len(result)), 3) + return result + +def _getDOMAttributes(match, name, ret): + log("", 3) + lst = re.compile('<' + name + '.*?' + ret + '=(.[^>]*?)>', re.M | re.S).findall(match) + ret = [] + for tmp in lst: + cont_char = tmp[0] + if cont_char in "'\"": + log("Using %s as quotation mark" % cont_char, 3) + + # Limit down to next variable. + if tmp.find('=' + cont_char, tmp.find(cont_char, 1)) > -1: + tmp = tmp[:tmp.find('=' + cont_char, tmp.find(cont_char, 1))] + + # Limit to the last quotation mark + if tmp.rfind(cont_char, 1) > -1: + tmp = tmp[1:tmp.rfind(cont_char)] + else: + log("No quotation mark found", 3) + if tmp.find(" ") > 0: + tmp = tmp[:tmp.find(" ")] + elif tmp.find("/") > 0: + tmp = tmp[:tmp.find("/")] + elif tmp.find(">") > 0: + tmp = tmp[:tmp.find(">")] + + ret.append(tmp.strip()) + + log("Done: " + repr(ret), 3) + return ret + +def _getDOMElements(item, name, attrs): + log("", 3) + lst = [] + for key in attrs: + lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=[\'"]' + attrs[key] + '[\'"].*?>))', re.M | re.S).findall(item) + if len(lst2) == 0 and attrs[key].find(" ") == -1: # Try matching without quotation marks + lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=' + attrs[key] + '.*?>))', re.M | re.S).findall(item) + + if len(lst) == 0: + log("Setting main list " + repr(lst2), 5) + lst = lst2 + lst2 = [] + else: + log("Setting new list " + repr(lst2), 5) + test = range(len(lst)) + test.reverse() + for i in test: # Delete anything missing from the next list. + if not lst[i] in lst2: + log("Purging mismatch " + str(len(lst)) + " - " + repr(lst[i]), 3) + del(lst[i]) + + if len(lst) == 0 and attrs == {}: + log("No list found, trying to match on name only", 3) + lst = re.compile('(<' + name + '>)', re.M | re.S).findall(item) + if len(lst) == 0: + lst = re.compile('(<' + name + ' .*?>)', re.M | re.S).findall(item) + + log("Done: " + str(type(lst)), 3) + return lst + +def parseDOM(html, name=u"", attrs={}, ret=False): + log("Name: " + repr(name) + " - Attrs:" + repr(attrs) + " - Ret: " + repr(ret) + " - HTML: " + str(type(html)), 3) + #log("BLA: " + repr(type(html)) + " - " + repr(type(name))) + + if isinstance(name, str): # Should be handled + try: + name = name #.decode("utf-8") + except: + log("Couldn't decode name binary string: " + repr(name)) + + if isinstance(html, str): + try: + html = [html.decode("utf-8")] # Replace with chardet thingy + except: + log("Couldn't decode html binary string. Data length: " + repr(len(html))) + html = [html] + elif isinstance(html, unicode): + html = [html] + elif not isinstance(html, list): + log("Input isn't list or string/unicode.") + return u"" + + if not name.strip(): + log("Missing tag name") + return u"" + + ret_lst = [] + for item in html: + temp_item = re.compile('(<[^>]*?\n[^>]*?>)').findall(item) + for match in temp_item: + item = item.replace(match, match.replace("\n", " ")) + + lst = _getDOMElements(item, name, attrs) + + if isinstance(ret, str): + log("Getting attribute %s content for %s matches " % (ret, len(lst) ), 3) + lst2 = [] + for match in lst: + lst2 += _getDOMAttributes(match, name, ret) + lst = lst2 + else: + log("Getting element content for %s matches " % len(lst), 3) + lst2 = [] + for match in lst: + log("Getting element content for %s" % match, 4) + temp = _getDOMContent(item, name, match, ret).strip() + item = item[item.find(temp, item.find(match)) + len(temp):] + lst2.append(temp) + lst = lst2 + ret_lst += lst + + log("Done: " + repr(ret_lst), 3) + return ret_lst + + +def extractJS(data, function=False, variable=False, match=False, evaluate=False, values=False): + log("") + scripts = parseDOM(data, "script") + if len(scripts) == 0: + log("Couldn't find any script tags. Assuming javascript file was given.") + scripts = [data] + + lst = [] + log("Extracting", 4) + for script in scripts: + tmp_lst = [] + if function: + tmp_lst = re.compile(function + '\(.*?\).*?;', re.M | re.S).findall(script) + elif variable: + tmp_lst = re.compile(variable + '[ ]+=.*?;', re.M | re.S).findall(script) + else: + tmp_lst = [script] + if len(tmp_lst) > 0: + log("Found: " + repr(tmp_lst), 4) + lst += tmp_lst + else: + log("Found nothing on: " + script, 4) + + test = range(0, len(lst)) + test.reverse() + for i in test: + if match and lst[i].find(match) == -1: + log("Removing item: " + repr(lst[i]), 10) + del lst[i] + else: + log("Cleaning item: " + repr(lst[i]), 4) + if lst[i][0] == u"\n": + lst[i] == lst[i][1:] + if lst[i][len(lst) -1] == u"\n": + lst[i] == lst[i][:len(lst)- 2] + lst[i] = lst[i].strip() + + if values or evaluate: + for i in range(0, len(lst)): + log("Getting values %s" % lst[i]) + if function: + if evaluate: # include the ( ) for evaluation + data = re.compile("(\(.*?\))", re.M | re.S).findall(lst[i]) + else: + data = re.compile("\((.*?)\)", re.M | re.S).findall(lst[i]) + elif variable: + tlst = re.compile(variable +".*?=.*?;", re.M | re.S).findall(lst[i]) + data = [] + for tmp in tlst: # This breaks for some stuff. "ad_tag": "http://ad-emea.doubleclick.net/N4061/pfadx/com.ytpwatch.entertainment/main_563326'' # ends early, must end with } + cont_char = tmp[0] + cont_char = tmp[tmp.find("=") + 1:].strip() + cont_char = cont_char[0] + if cont_char in "'\"": + log("Using %s as quotation mark" % cont_char, 1) + tmp = tmp[tmp.find(cont_char) + 1:tmp.rfind(cont_char)] + else: + log("No quotation mark found", 1) + tmp = tmp[tmp.find("=") + 1: tmp.rfind(";")] + + tmp = tmp.strip() + if len(tmp) > 0: + data.append(tmp) + else: + log("ERROR: Don't know what to extract values from") + + log("Values extracted: %s" % repr(data)) + if len(data) > 0: + lst[i] = data[0] + + if evaluate: + for i in range(0, len(lst)): + log("Evaluating %s" % lst[i]) + data = lst[i].strip() + try: + try: + lst[i] = json.loads(data) + except: + log("Couldn't json.loads, trying eval") + lst[i] = eval(data) + except: + log("Couldn't eval: %s from %s" % (repr(data), repr(lst[i]))) + + log("Done: " + str(len(lst))) + return lst + +def fetchPage(params={}): + get = params.get + link = get("link") + ret_obj = {} + if get("post_data"): + log("called for : " + repr(params['link'])) + else: + log("called for : " + repr(params)) + + if not link or int(get("error", "0")) > 2: + log("giving up") + ret_obj["status"] = 500 + return ret_obj + + if get("post_data"): + if get("hide_post_data"): + log("Posting data", 2) + else: + log("Posting data: " + urllib.urlencode(get("post_data")), 2) + + request = urllib2.Request(link, urllib.urlencode(get("post_data"))) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + else: + log("Got request", 2) + request = urllib2.Request(link) + + if get("headers"): + for head in get("headers"): + request.add_header(head[0], head[1]) + + request.add_header('User-Agent', USERAGENT) + + if get("cookie"): + request.add_header('Cookie', get("cookie")) + + if get("refering"): + request.add_header('Referer', get("refering")) + + try: + log("connecting to server...", 1) + + con = urllib2.urlopen(request) + ret_obj["header"] = con.info() + ret_obj["new_url"] = con.geturl() + if get("no-content", "false") == u"false" or get("no-content", "false") == "false": + inputdata = con.read() + #data_type = chardet.detect(inputdata) + #inputdata = inputdata.decode(data_type["encoding"]) + ret_obj["content"] = inputdata.decode("utf-8") + + con.close() + + log("Done") + ret_obj["status"] = 200 + return ret_obj + + except urllib2.HTTPError, e: + err = str(e) + log("HTTPError : " + err) + log("HTTPError - Headers: " + str(e.headers) + " - Content: " + e.fp.read()) + + params["error"] = str(int(get("error", "0")) + 1) + ret = fetchPage(params) + + if not "content" in ret and e.fp: + ret["content"] = e.fp.read() + return ret + + ret_obj["status"] = 500 + return ret_obj + + except urllib2.URLError, e: + err = str(e) + log("URLError : " + err) + + time.sleep(3) + params["error"] = str(int(get("error", "0")) + 1) + ret_obj = fetchPage(params) + return ret_obj + + +def getCookieInfoAsHTML(): + log("", 5) + if hasattr(sys.modules["__main__"], "cookiejar"): + cookiejar = sys.modules["__main__"].cookiejar + + cookie = repr(cookiejar) + cookie = cookie.replace("<_LWPCookieJar.LWPCookieJar[", "") + cookie = cookie.replace("), Cookie(version=0,", "></cookie><cookie ") + cookie = cookie.replace(")]>", "></cookie>") + cookie = cookie.replace("Cookie(version=0,", "<cookie ") + cookie = cookie.replace(", ", " ") + log(repr(cookie), 5) + return cookie + + log("Found no cookie", 5) + return "" + + +# This function implements a horrible hack related to python 2.4's terrible unicode handling. +def makeAscii(data): + log(repr(data), 5) + #if sys.hexversion >= 0x02050000: + # return data + + try: + return data.encode('ascii', "ignore") + except: + log("Hit except on : " + repr(data)) + s = u"" + for i in data: + try: + i.encode("ascii", "ignore") + except: + log("Can't convert character", 4) + continue + else: + s += i + + log(repr(s), 5) + return s + + +# This function handles stupid utf handling in python. +def makeUTF8(data): + log(repr(data), 5) + return data + try: + return data.decode('utf8', 'xmlcharrefreplace') # was 'ignore' + except: + log("Hit except on : " + repr(data)) + s = u"" + for i in data: + try: + i.decode("utf8", "xmlcharrefreplace") + except: + log("Can't convert character", 4) + continue + else: + s += i + log(repr(s), 5) + return s + + +def openFile(filepath, options=u"r"): + log(repr(filepath) + " - " + repr(options)) + if options.find("b") == -1: # Toggle binary mode on failure + alternate = options + u"b" + else: + alternate = options.replace(u"b", u"") + + try: + log("Trying normal: %s" % options) + return io.open(filepath, options) + except: + log("Fallback to binary: %s" % alternate) + return io.open(filepath, alternate) + + +def log(description, level=0): + if dbg and dbglevel > level: + try: + xbmc.log((u"[%s] %s : '%s'" % (plugin, inspect.stack()[1][3], description)).decode("utf-8"), xbmc.LOGNOTICE) + except: + xbmc.log(u"FALLBACK [%s] %s : '%s'" % (plugin, inspect.stack()[1][3], repr(description)), xbmc.LOGNOTICE)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xbmc/script.module.parsedom/lib/CommonFunctions.py.bak Mon Dec 31 16:45:52 2012 +0100 @@ -0,0 +1,557 @@ +''' + Parsedom for XBMC plugins + Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen + + 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 <http://www.gnu.org/licenses/>. +''' + +import sys +import urllib +import urllib2 +import re +import io +import inspect +import time +import HTMLParser +#import chardet +import json + +version = u"1.5.0" +plugin = u"CommonFunctions Beta-" + version +print plugin + +USERAGENT = u"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" + +if hasattr(sys.modules["__main__"], "xbmc"): + xbmc = sys.modules["__main__"].xbmc +else: + import xbmc + +if hasattr(sys.modules["__main__"], "xbmcgui"): + xbmcgui = sys.modules["__main__"].xbmcgui +else: + import xbmcgui + +if hasattr(sys.modules["__main__"], "dbg"): + dbg = sys.modules["__main__"].dbg +else: + dbg = False + +if hasattr(sys.modules["__main__"], "dbglevel"): + dbglevel = sys.modules["__main__"].dbglevel +else: + dbglevel = 3 + +if hasattr(sys.modules["__main__"], "opener"): + urllib2.install_opener(sys.modules["__main__"].opener) + + +# This function raises a keyboard for user input +def getUserInput(title=u"Input", default=u"", hidden=False): + log("", 5) + result = None + + # Fix for when this functions is called with default=None + if not default: + default = u"" + + keyboard = xbmc.Keyboard(default, title) + keyboard.setHiddenInput(hidden) + keyboard.doModal() + + if keyboard.isConfirmed(): + result = keyboard.getText() + + log(repr(result), 5) + return result + + +# This function raises a keyboard numpad for user input +def getUserInputNumbers(title=u"Input", default=u""): + log("", 5) + result = None + + # Fix for when this functions is called with default=None + if not default: + default = u"" + + keyboard = xbmcgui.Dialog() + result = keyboard.numeric(0, title, default) + + log(repr(result), 5) + return str(result) + + +def getXBMCVersion(): + log("", 3) + version = xbmc.getInfoLabel( "System.BuildVersion" ) + log(version, 3) + if version.find("-") -1: + version = version[:version.find("-")] + version = float(version) + log(repr(version)) + return version + +# Converts the request url passed on by xbmc to the plugin into a dict of key-value pairs +def getParameters(parameterString): + log("", 5) + commands = {} + if getXBMCVersion() >= 12.0: + parameterString = urllib.unquote_plus(parameterString) + splitCommands = parameterString[parameterString.find('?') + 1:].split('&') + + for command in splitCommands: + if (len(command) > 0): + splitCommand = command.split('=') + key = splitCommand[0] + try: + value = splitCommand[1].encode("utf-8") + except: + log("Error utf-8 encoding argument value: " + repr(splitCommand[1])) + value = splitCommand[1] + + commands[key] = value + + log(repr(commands), 5) + return commands + + +def replaceHTMLCodes(txt): + log(repr(txt), 5) + + # Fix missing ; in &#<number>; + txt = re.sub("(&#[0-9]+)([^;^0-9]+)", "\\1;\\2", makeUTF8(txt)) + + txt = HTMLParser.HTMLParser().unescape(txt) + txt = txt.replace("&", "&") + log(repr(txt), 5) + return txt + + +def stripTags(html): + log(repr(html), 5) + sub_start = html.find("<") + sub_end = html.find(">") + while sub_start < sub_end and sub_start > -1: + html = html.replace(html[sub_start:sub_end + 1], "").strip() + sub_start = html.find("<") + sub_end = html.find(">") + + log(repr(html), 5) + return html + + +def _getDOMContent(html, name, match, ret): # Cleanup + log("match: " + match, 3) + + endstr = u"</" + name # + ">" + + start = html.find(match) + end = html.find(endstr, start) + pos = html.find("<" + name, start + 1 ) + + log(str(start) + " < " + str(end) + ", pos = " + str(pos) + ", endpos: " + str(end), 8) + + while pos < end and pos != -1: # Ignore too early </endstr> return + tend = html.find(endstr, end + len(endstr)) + if tend != -1: + end = tend + pos = html.find("<" + name, pos + 1) + log("loop: " + str(start) + " < " + str(end) + " pos = " + str(pos), 8) + + log("start: %s, len: %s, end: %s" % (start, len(match), end), 3) + if start == -1 and end == -1: + result = u"" + elif start > -1 and end > -1: + result = html[start + len(match):end] + elif end > -1: + result = html[:end] + elif start > -1: + result = html[start + len(match):] + + if ret: + endstr = html[end:html.find(">", html.find(endstr)) + 1] + result = match + result + endstr + + log("done result length: " + str(len(result)), 3) + return result + +def _getDOMAttributes(match, name, ret): + log("", 3) + + lst = re.compile('<' + name + '.*?' + ret + '=([\'"].[^>]*?[\'"])>', re.M | re.S).findall(match) + if len(lst) == 0: + lst = re.compile('<' + name + '.*?' + ret + '=(.[^>]*?)>', re.M | re.S).findall(match) + ret = [] + for tmp in lst: + cont_char = tmp[0] + if cont_char in "'\"": + log("Using %s as quotation mark" % cont_char, 3) + + # Limit down to next variable. + if tmp.find('=' + cont_char, tmp.find(cont_char, 1)) > -1: + tmp = tmp[:tmp.find('=' + cont_char, tmp.find(cont_char, 1))] + + # Limit to the last quotation mark + if tmp.rfind(cont_char, 1) > -1: + tmp = tmp[1:tmp.rfind(cont_char)] + else: + log("No quotation mark found", 3) + if tmp.find(" ") > 0: + tmp = tmp[:tmp.find(" ")] + elif tmp.find("/") > 0: + tmp = tmp[:tmp.find("/")] + elif tmp.find(">") > 0: + tmp = tmp[:tmp.find(">")] + + ret.append(tmp.strip()) + + log("Done: " + repr(ret), 3) + return ret + +def _getDOMElements(item, name, attrs): + log("", 3) + + lst = [] + for key in attrs: + lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=[\'"]' + attrs[key] + '[\'"].*?>))', re.M | re.S).findall(item) + if len(lst2) == 0 and attrs[key].find(" ") == -1: # Try matching without quotation marks + lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=' + attrs[key] + '.*?>))', re.M | re.S).findall(item) + + if len(lst) == 0: + log("Setting main list " + repr(lst2), 5) + lst = lst2 + lst2 = [] + else: + log("Setting new list " + repr(lst2), 5) + test = range(len(lst)) + test.reverse() + for i in test: # Delete anything missing from the next list. + if not lst[i] in lst2: + log("Purging mismatch " + str(len(lst)) + " - " + repr(lst[i]), 3) + del(lst[i]) + + if len(lst) == 0 and attrs == {}: + log("No list found, trying to match on name only", 3) + lst = re.compile('(<' + name + '>)', re.M | re.S).findall(item) + if len(lst) == 0: + lst = re.compile('(<' + name + ' .*?>)', re.M | re.S).findall(item) + + log("Done: " + str(type(lst)), 3) + return lst + +def parseDOM(html, name=u"", attrs={}, ret=False): + log("Name: " + repr(name) + " - Attrs:" + repr(attrs) + " - Ret: " + repr(ret) + " - HTML: " + str(type(html)), 3) + + if isinstance(name, str): # Should be handled + try: + name = name #.decode("utf-8") + except: + log("Couldn't decode name binary string: " + repr(name)) + + if isinstance(html, str): + try: + html = [html.decode("utf-8")] # Replace with chardet thingy + except: + log("Couldn't decode html binary string. Data length: " + repr(len(html))) + html = [html] + elif isinstance(html, unicode): + html = [html] + elif not isinstance(html, list): + log("Input isn't list or string/unicode.") + return u"" + + if not name.strip(): + log("Missing tag name") + return u"" + + ret_lst = [] + for item in html: + temp_item = re.compile('(<[^>]*?\n[^>]*?>)').findall(item) + for match in temp_item: + item = item.replace(match, match.replace("\n", " ")) + + lst = _getDOMElements(item, name, attrs) + + if isinstance(ret, str): + log("Getting attribute %s content for %s matches " % (ret, len(lst) ), 3) + lst2 = [] + for match in lst: + lst2 += _getDOMAttributes(match, name, ret) + lst = lst2 + else: + log("Getting element content for %s matches " % len(lst), 3) + lst2 = [] + for match in lst: + log("Getting element content for %s" % match, 4) + temp = _getDOMContent(item, name, match, ret).strip() + item = item[item.find(temp, item.find(match)) + len(temp):] + lst2.append(temp) + lst = lst2 + ret_lst += lst + + log("Done: " + repr(ret_lst), 3) + return ret_lst + + +def extractJS(data, function=False, variable=False, match=False, evaluate=False, values=False): + log("") + scripts = parseDOM(data, "script") + if len(scripts) == 0: + log("Couldn't find any script tags. Assuming javascript file was given.") + scripts = [data] + + lst = [] + log("Extracting", 4) + for script in scripts: + tmp_lst = [] + if function: + tmp_lst = re.compile(function + '\(.*?\).*?;', re.M | re.S).findall(script) + elif variable: + tmp_lst = re.compile(variable + '[ ]+=.*?;', re.M | re.S).findall(script) + else: + tmp_lst = [script] + if len(tmp_lst) > 0: + log("Found: " + repr(tmp_lst), 4) + lst += tmp_lst + else: + log("Found nothing on: " + script, 4) + + test = range(0, len(lst)) + test.reverse() + for i in test: + if match and lst[i].find(match) == -1: + log("Removing item: " + repr(lst[i]), 10) + del lst[i] + else: + log("Cleaning item: " + repr(lst[i]), 4) + if lst[i][0] == u"\n": + lst[i] == lst[i][1:] + if lst[i][len(lst) -1] == u"\n": + lst[i] == lst[i][:len(lst)- 2] + lst[i] = lst[i].strip() + + if values or evaluate: + for i in range(0, len(lst)): + log("Getting values %s" % lst[i]) + if function: + if evaluate: # include the ( ) for evaluation + data = re.compile("(\(.*?\))", re.M | re.S).findall(lst[i]) + else: + data = re.compile("\((.*?)\)", re.M | re.S).findall(lst[i]) + elif variable: + tlst = re.compile(variable +".*?=.*?;", re.M | re.S).findall(lst[i]) + data = [] + for tmp in tlst: # This breaks for some stuff. "ad_tag": "http://ad-emea.doubleclick.net/N4061/pfadx/com.ytpwatch.entertainment/main_563326'' # ends early, must end with } + cont_char = tmp[0] + cont_char = tmp[tmp.find("=") + 1:].strip() + cont_char = cont_char[0] + if cont_char in "'\"": + log("Using %s as quotation mark" % cont_char, 1) + tmp = tmp[tmp.find(cont_char) + 1:tmp.rfind(cont_char)] + else: + log("No quotation mark found", 1) + tmp = tmp[tmp.find("=") + 1: tmp.rfind(";")] + + tmp = tmp.strip() + if len(tmp) > 0: + data.append(tmp) + else: + log("ERROR: Don't know what to extract values from") + + log("Values extracted: %s" % repr(data)) + if len(data) > 0: + lst[i] = data[0] + + if evaluate: + for i in range(0, len(lst)): + log("Evaluating %s" % lst[i]) + data = lst[i].strip() + try: + try: + lst[i] = json.loads(data) + except: + log("Couldn't json.loads, trying eval") + lst[i] = eval(data) + except: + log("Couldn't eval: %s from %s" % (repr(data), repr(lst[i]))) + + log("Done: " + str(len(lst))) + return lst + +def fetchPage(params={}): + get = params.get + link = get("link") + ret_obj = {} + if get("post_data"): + log("called for : " + repr(params['link'])) + else: + log("called for : " + repr(params)) + + if not link or int(get("error", "0")) > 2: + log("giving up") + ret_obj["status"] = 500 + return ret_obj + + if get("post_data"): + if get("hide_post_data"): + log("Posting data", 2) + else: + log("Posting data: " + urllib.urlencode(get("post_data")), 2) + + request = urllib2.Request(link, urllib.urlencode(get("post_data"))) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + else: + log("Got request", 2) + request = urllib2.Request(link) + + if get("headers"): + for head in get("headers"): + request.add_header(head[0], head[1]) + + request.add_header('User-Agent', USERAGENT) + + if get("cookie"): + request.add_header('Cookie', get("cookie")) + + if get("refering"): + request.add_header('Referer', get("refering")) + + try: + log("connecting to server...", 1) + + con = urllib2.urlopen(request) + ret_obj["header"] = con.info() + ret_obj["new_url"] = con.geturl() + if get("no-content", "false") == u"false" or get("no-content", "false") == "false": + inputdata = con.read() + #data_type = chardet.detect(inputdata) + #inputdata = inputdata.decode(data_type["encoding"]) + ret_obj["content"] = inputdata.decode("utf-8") + + con.close() + + log("Done") + ret_obj["status"] = 200 + return ret_obj + + except urllib2.HTTPError, e: + err = str(e) + log("HTTPError : " + err) + log("HTTPError - Headers: " + str(e.headers) + " - Content: " + e.fp.read()) + + params["error"] = str(int(get("error", "0")) + 1) + ret = fetchPage(params) + + if not "content" in ret and e.fp: + ret["content"] = e.fp.read() + return ret + + ret_obj["status"] = 500 + return ret_obj + + except urllib2.URLError, e: + err = str(e) + log("URLError : " + err) + + time.sleep(3) + params["error"] = str(int(get("error", "0")) + 1) + ret_obj = fetchPage(params) + return ret_obj + + +def getCookieInfoAsHTML(): + log("", 5) + if hasattr(sys.modules["__main__"], "cookiejar"): + cookiejar = sys.modules["__main__"].cookiejar + + cookie = repr(cookiejar) + cookie = cookie.replace("<_LWPCookieJar.LWPCookieJar[", "") + cookie = cookie.replace("), Cookie(version=0,", "></cookie><cookie ") + cookie = cookie.replace(")]>", "></cookie>") + cookie = cookie.replace("Cookie(version=0,", "<cookie ") + cookie = cookie.replace(", ", " ") + log(repr(cookie), 5) + return cookie + + log("Found no cookie", 5) + return "" + + +# This function implements a horrible hack related to python 2.4's terrible unicode handling. +def makeAscii(data): + log(repr(data), 5) + #if sys.hexversion >= 0x02050000: + # return data + + try: + return data.encode('ascii', "ignore") + except: + log("Hit except on : " + repr(data)) + s = u"" + for i in data: + try: + i.encode("ascii", "ignore") + except: + log("Can't convert character", 4) + continue + else: + s += i + + log(repr(s), 5) + return s + + +# This function handles stupid utf handling in python. +def makeUTF8(data): + log(repr(data), 5) + return data + try: + return data.decode('utf8', 'xmlcharrefreplace') # was 'ignore' + except: + log("Hit except on : " + repr(data)) + s = u"" + for i in data: + try: + i.decode("utf8", "xmlcharrefreplace") + except: + log("Can't convert character", 4) + continue + else: + s += i + log(repr(s), 5) + return s + + +def openFile(filepath, options=u"r"): + log(repr(filepath) + " - " + repr(options)) + if options.find("b") == -1: # Toggle binary mode on failure + alternate = options + u"b" + else: + alternate = options.replace(u"b", u"") + + try: + log("Trying normal: %s" % options) + return io.open(filepath, options) + except: + log("Fallback to binary: %s" % alternate) + return io.open(filepath, alternate) + + +def log(description, level=0): + if dbg and dbglevel > level: + try: + xbmc.log((u"[%s] %s : '%s'" % (plugin, inspect.stack()[1][3], description)).decode("utf-8"), xbmc.LOGNOTICE) + except: + xbmc.log(u"FALLBACK [%s] %s : '%s'" % (plugin, inspect.stack()[1][3], repr(description)), xbmc.LOGNOTICE)