diff options
author | Antti Ajanki <antti.ajanki@iki.fi> | 2010-07-23 20:55:11 +0300 |
---|---|---|
committer | Antti Ajanki <antti.ajanki@iki.fi> | 2010-07-23 20:55:11 +0300 |
commit | 310743fb9ebbf68b253b923a309cc5f635da89a1 (patch) | |
tree | 59c365db7459649344b4ab6d58fde1ceb362506d | |
download | vdr-plugin-webvideo-310743fb9ebbf68b253b923a309cc5f635da89a1.tar.gz vdr-plugin-webvideo-310743fb9ebbf68b253b923a309cc5f635da89a1.tar.bz2 |
release 0.3.0
153 files changed, 15451 insertions, 0 deletions
@@ -0,0 +1,701 @@ + 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>. + + + + +The project includes some files from iniparse library. They are +covered by the following license: + +Copyright (c) 2000-2007 by Nicolas Devillard. +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. @@ -0,0 +1,185 @@ +VDR Plugin 'webvideo' Revision History +-------------------------------------- + +2008-06-25: Version 0.0.1 + +- Initial revision. + +2008-07-24: Version 0.0.2 + +- Italian translation (thanks to Diego Pierotto) +- Support for mms URLs using libmms +- Guess the file extension from the Content-Type header, not from the + defaultext tag +- New video service: YLE Areena (the web service of the Finland's + national broadcasting company). Only partial support, some of the + URLs do not work with libmms. +- Youtube: Download higher quality MPEG-4 videos + +2008-08-20: Version 0.0.3 + +- Support for video search +- Updated Italian translation (thanks to Diego Pierotto) +- Try mmsh if mms protocol fails (requires libmms 0.4 or later). Most + videos on YLE Areena seem to work after this fix. +- Fix segfault when deleting the plugin at VDR exit +- Youtube: switch back to low quality FLV videos because not all + videos have MP4 version + +2008-08-21: Version 0.0.4 + +- Updated Italian translation (thanks to Diego Pierotto) +- Include a workaround for a bug in the libmms header file mmsx.h + which caused the compilation to fail +- Fix compiler warnings + +2008-09-08: Version 0.0.5 + +- New video service: SVT Play. Contributed by Lars Olsson. +- More robust parsing of .asx files +- Workaround for buggy servers: if the server reports the Content-Type + of a video file as text/plain do not use it for deciding the file + extension. Try to extract the extension from the URL instead. +- Sort service names alphabetically + +2008-12-06: Version 0.0.6 + +- French translation (Thanks to Bruno Roussel) +- Fixed Youtube parsing to accommodate to recent changes + +2009-02-08: Version 0.1.0 + +- The downloader backend is now a separate server process. The user + interface is no longer blocked while the plugin is waiting for a web + server to respond. +- Support for streaming +- A new command line client that has the same capabilities as the plugin + but can be used without VDR. +- Alternative URLs for videos. For example, Youtube module first tries + to download high quality version, and falls back to standard version + if high quality version is not available. +- Cleaning up of the XML menu scheme. New menu items: textfields, item + lists, query buttons. +- Status page that lists uncompleted downloads +- Updated YouTube, Google, and SVTPlay modules to work with the recent + changes on these sites + +2009-02-24: Version 0.1.1 + +- Simplified building: better Makefile, fixed instructions in README +- Updated Italian translations (thanks to Diego Pierotto) +- German translation (contributed by Andre L.) +- Daemon stops downloads gracefully when client disconnects +- Fixed segfault when a menu title is NULL (this happened for + example on YouTube search results page) +- sane filenames: no slashs, no dots in the beginning +- Try to start daemon process automatically if can't open a connection +- Removed busy polling when loading the main menu +- Remove temporary file if the request fails +- Ability to cancel downloads (through the status screen) +- URLencode function in the plugin was bogus: the percent encoded + values should be in hex, not in decimal +- Fixed problem with downloads never finishing if the server sends + shorter file than expected +- History forward skipped over one page +- SVTPlay: various improvements to the parsing of the web pages + +2009-03-07: Version 0.1.2 + +- Unescape the stream URL before passing it to xineliboutput to make + Youtube streaming work. +- Youtube: More robust parsing of search results page. Updated + categories parsing according to recent changes. +- Updated Italian translations (thanks to Diego Pierotto) +- Fixed a typo in German translation (thanks to Halim Sahin) + +2009-04-08: Version 0.1.3 + +Plugin: +- Call libxslt.init() only it exists (old versions of libxslt don't + have init()) +- Update download progress indicators in the status screen at regular + intervals + +webvi, the command line client: +- Show download progress + +Video site modules: +- YLE Areena: show error message if search fails, show categories in + the main menu, various smaller parsing improvements +- Youtube: show error message if no search results, fix parsing of + Movies category + +2009-05-05: Version 0.1.4 + +- Updated Italian translation (thanks to Diego Pierotto) +- Config file for webvi for defining player programs and the address + of the daemon +- Streaming now reverts back to lower quality video if high quality + version is not available (like downloading already did before) + +Video site modules: +- Support for a new video site: Metacafe +- Youtube: adapted parsing to comply with recent changes on Youtube. + Download HD quality video when available. +- YLE Areena: download high quality videos by default +- Google Video: support for videos hosted on Metacafe. Made parsing a + bit more robust. + +2009-05-10: Version 0.1.5 + +- Don't crash VDR if can't connect to the daemon +- Updated to work with Python 2.6 (a parameter name has changed in + asynchat) +- Force the installation prefix for Python scripts to be /usr, not + /usr/local +- Command line argument --daemoncmd specifies the command for starting + the webvid daemon + +2009-08-20: Version 0.1.6 + +- Fixed compilation on gcc4.4. Thanks to Anssi Hannula. +- Fixed Youtube module. +- Removed the outdated YLE Areena support. + +2009-10-27: Version 0.1.7 + +- Compatibility fixes for Youtube and Metacafe modules. + +2010-01-17: Version 0.2.0 + +- The daemon is replaced by Python library with C bindings. This + simplifies the invocation of the VDR plugin and the command line + client. +- New video service: Vimeo +- Re-added support for YLE Areena (requires rtmpdump-yle from + http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html). +- Youtube: using the official API (except for video pages), this + should mean less breakage in the future. Various improvements on + the menus. + +2010-01-23: Version 0.2.1 + +- Support for all Python versions. +- Install the plugin with VDR's "make plugins". (If you use make + plugins, you still need to install the library separately.) + +2010-04-11: Version 0.2.2 + +- Remember query terms and menu positions when moving in history. +- Reduce delays when navigating the menu. +- Install libwebvi.so* links correctly. Run ldconfig. +- Write correct path to /etc/webvi.conf when installing to an + alternative location. +- Show percentage as ??? on status page if the size is unknown. +- Fixed Youtube module. + +2010-07-12: Version 0.3.0 + +- Scheduled downloading +- Show error details on status screen by pressing Info +- Fix a crash when video URL is empty. +- INI file options for controlling the download quality. +- Add support for Finnish TV stations: MTV3 Katsomo, ruutu.fi, Subtv. +- Make all downloads abortable. +- Fixed Vimeo search. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..899da9e --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +# prefix for non-VDR stuff +PREFIX ?= /usr/local +# VDR directory +VDRDIR ?= /usr/src/vdr-1.6.0 +# VDR's library directory +VDRPLUGINDIR ?= $(VDRDIR)/PLUGINS/lib +# VDR's plugin conf directory +VDRPLUGINCONFDIR ?= /video +# VDR's locale directory +VDRLOCALEDIR ?= $(VDRDIR)/locale + +VERSION := $(shell cat src/version) + +TMPDIR = /tmp +ARCHIVE = webvideo-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +APIVERSION := $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) +LIBDIR = $(VDRPLUGINDIR) + +# Default target compiles everything but does not install anything. +all-noinstall: libwebvi vdr-plugin + +# This target is used by VDR's make plugins. It compiles everything +# and installs VDR plugin. +all: libwebvi vdr-plugin $(LIBDIR)/libvdr-webvideo.so.$(APIVERSION) webvi.conf + +vdr-plugin: libwebvi + $(MAKE) -C src/vdr-plugin LOCALEDIR=./locale LIBDIR=. VDRDIR=$(VDRDIR) CXXFLAGS="-fPIC -g -O2 -Wall -Woverloaded-virtual -Wno-parentheses" + +libwebvi: build-python + $(MAKE) -C src/libwebvi all libwebvi.a + +build-python: webvi.conf + python setup.py build + +webvi.conf: + @echo "[webvi]\n\ntemplatepath = $(PREFIX)/share/webvi/templates" > webvi.conf + +$(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION): vdr-plugin + mkdir -p $(VDRPLUGINDIR) + cp -f src/vdr-plugin/libvdr-webvideo.so.$(APIVERSION) $(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION) + +install-vdr-plugin: vdr-plugin $(VDRPLUGINDIR)/libvdr-webvideo.so.$(APIVERSION) + mkdir -p $(VDRLOCALEDIR) + cp -rf src/vdr-plugin/locale/* $(VDRLOCALEDIR) + mkdir -p $(VDRPLUGINCONFDIR)/webvideo + cp -f src/vdr-plugin/mime.types $(VDRPLUGINCONFDIR)/webvideo + +install-libwebvi: libwebvi + $(MAKE) -C src/libwebvi install + +install-python: + python setup.py install --prefix $(PREFIX) + +install-conf: webvi.conf + cp -f webvi.conf /etc/ + +install-webvi: install-libwebvi install-python + +install: install-vdr-plugin install-webvi install-conf + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + $(MAKE) -C src/vdr-plugin clean + $(MAKE) -C src/libwebvi clean + rm -rf src/vdr-plugin/locale webvi.conf + python setup.py clean -a + find . -name "*~" -exec rm {} \; + find . -name "*.pyc" -exec rm {} \; + +.PHONY: vdr-plugin libwebvi build-python install install-vdr-plugin install-webvi dist clean @@ -0,0 +1,48 @@ +Written by: Antti Ajanki <antti.ajanki@iki.fi> +Project's homepage: http://users.tkk.fi/~aajanki/vdr/webvideo + +Webvi is a tool for downloading and playing videos from popular video +sharing webvites such as YouTube. There are two interfaces: a plugin +for Video Disk Recorder (VDR), and a command line client. The two +interfaces are described in README.vdrplugin and README.webvi. + +The common functionality of the VDR plugin and the command line client +is implemented in a Python library (src/libwebvi/webvi). C bindings +for the library are also provided (see src/libwebvi/libwebvi.h). + +Supported video sites: + +* Google Video [1] +* Metacafe +* MTV3 Katsomo +* ruutu.fi [2] +* Subtv +* SVT Play +* Vimeo +* YLE Areena [2] +* YouTube + +[1] Only videos hosted by Google, Youtube, Vimeo and SVT + +[2] Requires rtmpdump-yle + (http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html). Streaming + does not work. + +Known problems: + +SVT Play: not all videos are working +MTV3 Katsomo and Subtv: often the connection is lost before the + video is fully downloaded. + +Because of the modular design it is possible to add support for new +sites quite easily. See doc/developers.txt for more information. + +License: + +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. The project includes files from +iniparse library under MIT license. + +See the file COPYING for more information. diff --git a/README.vdrplugin b/README.vdrplugin new file mode 100644 index 0000000..838a936 --- /dev/null +++ b/README.vdrplugin @@ -0,0 +1,180 @@ +This is a "plugin" for the Video Disk Recorder (VDR). + +Written by: Antti Ajanki <antti.ajanki@iki.fi> + +Project's homepage: http://users.tkk.fi/~aajanki/vdr/webvideo + +Latest version available at: http://users.tkk.fi/~aajanki/vdr/webvideo + +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. The project includes files from +iniparse library under MIT license. + +See the file COPYING for more information. + +Description: + +Webvideo is a VDR plugin for downloading videos from popular video +sharing webvites such as YouTube. With the help of xineliboutput +plugin the videos can be played directly in VDR without downloading +them first. See README for the full list of supported sites. + +Requirements: + +* VDR 1.6.0 or later +* Python 2.5 or later (http://www.python.org/) +* simplejson (on Python 2.5, not needed on later Python versions) +* libcurl (http://curl.haxx.se/) +* pycurl 7.18.2 or newer (http://pycurl.sourceforge.net/) +* libxml and libxslt (http://xmlsoft.org/) +* a video player for viewing the downloaded videos or streaming videos + without downloading, for example xineliboutput plugin + +Suggested: + +* mimms 3.0 or later for downloading mms URLs + (http://savannah.nongnu.org/projects/mimms/) +* rtmpdump-yle (http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html) + +On Debian these dependencies can be satisfied by installing packages +vdr, python-libxml2, python-libxslt1, python-pycurl, +python-simplejson, mimms, either vdr-plugin-xineliboutput or +vdr-plugin-mplayer, and their dependencies. For building the Debian +package vdr-dev, libxml2-dev, python-all-dev, python-central, +debhelper, cdbs, txt2man, gettext, and libglib2.0-dev are needed, as +well. + +Installation and running +------------------------ + +These are the general install instructions. If you are using Debian, +it easier to build and install the Debian package as instructed in the +next section. + +tar -xzf /put/your/path/here/vdr-webvideo-X.Y.Z.tgz +cd webvideo-X.Y.Z +make VDRDIR=/path/to/VDR +make install VDRDIR=/path/to/VDR + +These steps install the library and the VDR plugin. It is not +necessary call VDR's make plugins. + +The installation locations can be further customized by appending the +following variables to make install invocation: + +PREFIX prefix for the non-VDR files (default: /usr/local) +VDRPLUGINDIR VDR's plugin dir (default: VDRDIR/PLUGINS/lib) +VDRPLUGINCONFDIR VDR's plugin conf directory (default: /video) +VDRLOCALEDIR VDR's locale directory (default: VDRDIR/locale) + +To start the VDR with the webvideo plugin run + +vdr -P "webvideo --templatedir=/usr/local/share/webvi/templates" + +The parameter --templatedir can be left out if the default PREFIX was +used in make install. + +Installation on Debian +---------------------- + +tar -xzf /put/your/path/here/vdr-webvideo-X.Y.Z.tgz +cd webvideo-X.Y.Z +dpkg-buildpackage -rfakeroot -us -uc +cd .. +dpkg -i python-webvi_X.Y.Z-W_all.deb libwebvi0_X.Y.Z-W_i386.deb vdr-plugin-webvideo_X.Y.Z-W_i386.deb + +Debian's init scripts automatically load the plugin with proper +parameters when VDR starts. + +VDR plugin command line parameters +---------------------------------- + +-d dir, --downloaddir=DIR Save downloaded files to DIR. The default + path is the VDR video directory. +-t dir, --templatedir=DIR Read video site templates from DIR (default + /usr/local/share/webvi/templates) +-c FILE, --conf=FILE Load settings from FILE + +Config file +----------- + +Config file VDRPLUGINCONFDIR/webvi.plugin.conf (the default path can +be overridden with the --conf argument) controls the quality of the +downloaded and streamed videos. + +Currently only Youtube module supports multiple qualities. The +following options are recognized in section [site-youtube]: + +download-min-quality, download-max-quality + +Minimum and maximum allowed quality when saving the video to disc. The +default is to download the best available version of the video. + +stream-min-quality, stream-max-quality + +Minimum and maximum allowed quality when playing the video. The +default is to download the best available version of the video. + +For Youtube, the available quality scores are (not all videos have the +higher quality versions): + + 50: standard quality (320x240, i.e. what you get in the web browser) + 60: medium quality (480x360 MP4) + 70: HD quality (720p) + +For example, if you don't have enough network bandwidth for playing +the high quality versions smoothly, you may want to limit the maximum +streaming quality score but still get the HD version when downloading. +To do this, add the following snippet to the ini-file: + +[site-youtube] +stream-max-quality = 50 + +Usage +----- + +Navigation links that lead to a new menu pages are marker with +brackets [ ]. They can be followed by selecting them and pushing OK. + +The links without brackets are video or audio streams. They can be +downloaded in the background by pushing OK. Pressing Blue on a media +stream starts playing it immediately in xineliboutput plugin. Pressing +Info shows more information about a media stream. + +Keys: + +OK Follow a link, or start to download a stream +Red Go back to the previous menu / + Show download status screen +Green Go forward in browsing history / + Edit timers +Yellow Create timer +Blue Play media stream without saving +Info Show details of a media stream +0 More options + +In the status screen: + +Red Cancel the selected download +Info Show download error details + +Scheduled downloading +--------------------- + +The plugin can be configured to fetch new videos automatically from +certain pages at regular intervals. + +To setup a timer, navigate to the page that contains the videos you +want to fetch and press Yellow button. The the update interval can be +set in the menu that opens. To save and execute the timer leave the +timer menu with Back button. + +To list, edit or remove existing timers press 0 and Green. + +Hint: The timers work even on search results. To download new VDR +related videos that appear in Youtube navigate to the Youtube search, +enter "VDR Linux" as search term and "Date added" as sorting +criterion, execute the search, and create a timer on the search +results page. diff --git a/README.webvi b/README.webvi new file mode 100644 index 0000000..88f69eb --- /dev/null +++ b/README.webvi @@ -0,0 +1,118 @@ +webvi - command line web video downloader + +Copyright 2009,2010 Antti Ajanki <antti.ajanki@iki.fi> + +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. See the file COPYING for more +information. + +Description +----------- + +Webvi is a tool for downloading and playing videos from popular video +sharing webvites such as YouTube. See README for the full list of +supported sites. + +Installation +------------ + +To compile and install the command line client (without VDR plugin; +see main README if you have VDR installed) run + +make libwebvi +make install-webvi + +By default the program is installed under /usr/local. You can specify +a different installation location by + +make libwebvi PREFIX=/usr +make install-webvi install-conf PREFIX=/usr + +If you use an alternative installation location, you may need to put +PREFIX/lib/pythonX.Y/site-packages/ or +PREFIX/lib/pythonX.Y/dist-packages/ to your PYTHONPATH environment +variable. + +Running +------- + +webvi --templatedir=/usr/local/share/webvi/templates + +The parameter --templatedir can be left out if the default PREFIX was +used in make install-library. + +Command line parameters +----------------------- + +-h, --help show this help message and exit +-t DIR, --templatepath=DIR read video site templates from DIR + +Usage +----- + +The content of video sharing websites is presented as series of menus. +The menus consists of two kinds of links. Navigation links, which are +be identified by [brackets], are used to navigate the site. +Non-bracketed links are media streams that can be downloaded or +played. + +Following commands are recognized: + +help Show help +select x Select a link whose index is x +download x Download a media stream whose index is x +stream x Play a media stream whose index is x +back Go backward in history +forward Go forward in history +display Redisplay the current menu +menu Go back to the main menu +quit Quit the program + +x is an index of a link in the current menu. Entering an index number +x without any command is a shorthand for "select x". + +Config file +----------- + +Config files /etc/webvi.conf and ~/.webvi configure the behavior of +the program. An example configuration file debian/webvi.conf is +included in the sources. + +The config files are in ini format. The following items are recognized +in section [webvi]: + +streamplayer1, ..., streamplayer9 + +streamplayer1 to streamplayer9 are alternative media players to be +used for streaming. The substring %s will be replaced by the stream +URL. The players are tried one by one starting from streamplayer1 +until one of them succeeds playing the stream. If no players are +defined in config files then vlc, totem, mplayer, and xine are tried +(in that order). + +templatepath + +Path to video site templates. + +Quality of the downloaded and streamed videos can be selected in video +site specific sections. Currently only Youtube module (section should +be called [site-youtube]) supports multiple qualities. The following +options are recognized: + +download-min-quality, download-max-quality + +Minimum and maximum allowed quality when saving the video to disc. The +default is to download the best available version of the video. + +stream-min-quality, stream-max-quality + +Minimum and maximum allowed quality when playing the video. The +default is to download the best available version of the video. + +For Youtube, the available quality scores are: + + 50: standard quality (320x240, i.e. what you get in the web browser) + 60: medium quality (480x360 MP4) + 70: HD quality (720p) @@ -0,0 +1,18 @@ +Keep connections alive by reusing the same curl handle + +Translation of strings in XSLT + +Show a poster image next to video's name + +plugin: scrolling does not work on video description pages + +SVTPlay: add search + +metacafe: fails to find video URL for videos with ID sy-*, cb-* +(others?). The videos are probably hosted on some other sites (like +yt-* -> youtube). + +Download percentage on the VDR plugin status page does is not updated +when using external downloader process (YLE Areena) + +Streaming does not work with external downloader process (YLE Areena) diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..8a433db --- /dev/null +++ b/debian/changelog @@ -0,0 +1,150 @@ +vdr-plugin-webvideo (0.3.0-1) unstable; urgency=low + + * Scheduled downloading + * Show error details on status screen by pressing Info. + * Fix a crash when video URL is empty. + * INI file options for controlling the download quality. + * Add support for Finnish TV stations: MTV3 Katsomo, ruutu.fi, Subtv. + * Make all downloads abortable. + * Fixed Vimeo search. + * Get the lower quality video when streaming from Youtube + + -- Antti Ajanki <antti@gaspode> Mon, 12 Jul 2010 11:35:14 +0300 + +vdr-plugin-webvideo (0.2.2-1) unstable; urgency=low + + * New release + * Remember query terms and menu positions when moving in history. + * Reduce delays when navigating the menu. + * Show percentage as ??? on status page if the size is unknown. + * Fixed Youtube module. + + -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 11 Apr 2010 11:46:34 +0300 + +vdr-plugin-webvideo (0.2.1-1) unstable; urgency=low + + * Support for all Python versions. + * Install the plugin with VDR's "make plugins". + + -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 23 Jan 2010 15:03:45 +0200 + +vdr-plugin-webvideo (0.2.0-1) unstable; urgency=low + + * The daemon is replaced by Python library with C bindings. + * New video service: Vimeo + * Re-added support for YLE Areena + * Youtube: using the official API (except for video pages), this + should mean less breakage in the future. Various improvements on + the menus. + * Created new packages for the library: python-webvi, libwebvi0 + + -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 17 Jan 2010 22:01:50 +0200 + +vdr-plugin-webvideo (0.1.7-1) unstable; urgency=low + + * New upstream release + - Moved the default download directory to /var/lib/webvideo + - Standards-Version: 3.8.3 + * Moved webvid daemon to new package webvid + * Removed non-standard shebang line from debian/rules + * Removed DVBDIR from debian/rules + * Added myself to Uploaders and Debian Maintainers + + -- Thomas Günther <tom@toms-cafe.de> Thu, 05 Nov 2009 02:34:31 +0100 + +vdr-plugin-webvideo (0.1.6-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 20 Aug 2009 20:10:32 +0300 + +vdr-plugin-webvideo (0.1.5-1) unstable; urgency=low + + * New release + * Call postinst scripts fragments in the correct order + + -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 10 May 2009 20:24:06 +0300 + +vdr-plugin-webvideo (0.1.4-1) unstable; urgency=low + + * New release + * Moved the default download directory to /var/lib/webvideo. + + -- Antti Ajanki <antti.ajanki@iki.fi> Sun, 03 May 2009 21:06:00 +0300 + +vdr-plugin-webvideo (0.1.3-1) unstable; urgency=low + + * New upstream release + * Bumped standards version to 3.8.1 + + -- Tobias Grimm <etobi@debian.org> Sun, 19 Apr 2009 16:46:26 +0200 + +vdr-plugin-webvideo (0.1.2-1) unstable; urgency=low + + * New upstream release + * Changed XS-Python-Version to current to fix FTBS problem + * Changed maintainer to Debian VDR Team + * Added Python to dependencies + * Removed dh_make boilerplates from debian/copyright + * Fixed homepage field in debian/control + * Bumped standards version to 3.8.1 + * Install init script as /etc/init.d/webvid + * Fixed LOCALEDIR to build locales in correct directory + * Deactivated postinst-script-order.diff - seems not to be required anymore + * Added debian/watch + * Depend on libmms0 + * Dropped patchlevel control field + * Build-Depend on vdr-dev (>=1.6.0-5) + * Added manpages + * Changed section to "video" + + -- Tobias Grimm <etobi@debian.org> Sat, 11 Apr 2009 00:05:37 +0200 + +vdr-plugin-webvideo (0.1.1-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 24 Feb 2009 19:38:28 +0300 + +vdr-plugin-webvideo (0.1.0-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 7 Feb 2009 18:07:37 +0300 + +vdr-plugin-webvideo (0.0.6-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Sat, 6 Dec 2008 11:27:05 +0300 + +vdr-plugin-webvideo (0.0.5-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 2 Sep 2008 21:41:50 +0300 + +vdr-plugin-webvideo (0.0.4-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 21 Aug 2008 12:29:38 +0300 + +vdr-plugin-webvideo (0.0.3-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Tue, 19 Aug 2008 18:19:19 +0300 + +vdr-plugin-webvideo (0.0.2-1) unstable; urgency=low + + * New release + + -- Antti Ajanki <antti.ajanki@iki.fi> Thu, 24 Jul 2008 20:44:15 +0300 + +vdr-plugin-webvideo (0.0.1-1) unstable; urgency=low + + * Initial release + + -- Antti Ajanki <antti.ajanki@iki.fi> Wed, 25 Jun 2008 17:53:54 +0300 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..3deeccb --- /dev/null +++ b/debian/control @@ -0,0 +1,52 @@ +Source: vdr-plugin-webvideo +Section: video +Priority: extra +Maintainer: Antti Ajanki <antti.ajanki@iki.fi> +Uploaders: Tobias Grimm <etobi@debian.org>, Thomas Günther <tom@toms-cafe.de> +Build-Depends: debhelper (>= 5.0.38), cdbs (>= 0.4.49), txt2man, vdr-dev (>= 1.6.0-5), gettext, libxml2-dev, python-all-dev, python-central (>= 0.5.6), libglib2.0-dev +Standards-Version: 3.8.3 +Homepage: http://users.tkk.fi/~aajanki/vdr/webvideo +XS-Python-Version: >= 2.5 + +Package: python-webvi +Architecture: all +Section: python +Depends: ${misc:Depends}, ${python:Depends}, python-libxml2, python-libxslt1, python-pycurl, python-simplejson, mimms +Replaces: vdr-plugin-webvideo (<< 0.2.0), webvid (<< 0.2.0) +Description: Web video downloader library - Python module + This package provides a library for downloading video and audio + streams from media sharing websites, such as YouTube or Google Video. + . + This is the Python module. +XB-Python-Version: ${python:Versions} + +Package: libwebvi0 +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, ${misc:Depends}, python, python-webvi +Description: Web video downloader library - shared library + This package provides a library for downloading video and audio + streams from media sharing websites, such as YouTube or Google Video. + . + This is the shared library. +XB-Python-Version: ${python:Versions} + +Package: libwebvi-dev +Architecture: any +Section: libdevel +Depends: ${misc:Depends}, libc-dev +Description: Web video downloader library - development files + This package provides a library for downloading video and audio + streams from media sharing websites, such as YouTube or Google Video. + . + This package contains the development files. + +Package: vdr-plugin-webvideo +Architecture: any +Section: video +Depends: ${shlibs:Depends}, ${misc:Depends}, ${vdr:Depends}, libwebvi0 +Suggests: vdr-plugin-mplayer | vdr-plugin-xineliboutput +Description: VDR plugin for downloading videos from the Web + This plugin for the Linux Video Disc Recorder (VDR) provides ability + to download video files from popular video sharing websites, such as + YouTube or Google Video. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..b1d4f0e --- /dev/null +++ b/debian/copyright @@ -0,0 +1,61 @@ +Upstream Homepage: + http://users.tkk.fi/~aajanki/vdr/webvideo + +Upstream Author: + Antti Ajanki <antti.ajanki@iki.fi> + +Debian Maintainers: + Antti Ajanki <antti.ajanki@iki.fi> + Tobias Grimm <etobi@debian.org> + Thomas Günther <tom@toms-cafe.de> + +Copyright: + (C) 2008, 2009, 2010 Antti Ajanki + +Copyright (Debian packaging): + (C) 2008, 2009 Antti Ajanki + (C) 2009 Tobias Grimm, Thomas Günther + +License: + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + The complete text of the GNU General Public License can be found + in /usr/share/common-licenses/GPL-3 on most Debian systems. + +License (iniparser library): + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +License (Debian packaging): + The Debian packaging is licensed under the GPL, version 3 or any + later version, see /usr/share/common-licenses/GPL-3. diff --git a/debian/libwebvi-dev.docs b/debian/libwebvi-dev.docs new file mode 100644 index 0000000..523e527 --- /dev/null +++ b/debian/libwebvi-dev.docs @@ -0,0 +1 @@ +doc/developers.txt diff --git a/debian/libwebvi-dev.install b/debian/libwebvi-dev.install new file mode 100644 index 0000000..4946c7c --- /dev/null +++ b/debian/libwebvi-dev.install @@ -0,0 +1,3 @@ +src/libwebvi/libwebvi.so usr/lib/ +src/libwebvi/libwebvi.h usr/include/ +src/libwebvi/libwebvi.a usr/lib/ diff --git a/debian/libwebvi0.docs b/debian/libwebvi0.docs new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/debian/libwebvi0.docs @@ -0,0 +1 @@ +TODO diff --git a/debian/libwebvi0.install b/debian/libwebvi0.install new file mode 100644 index 0000000..6c9de3d --- /dev/null +++ b/debian/libwebvi0.install @@ -0,0 +1 @@ +src/libwebvi/libwebvi.so.0* usr/lib/ diff --git a/debian/plugin.webvideo.conf b/debian/plugin.webvideo.conf new file mode 100644 index 0000000..8941e9b --- /dev/null +++ b/debian/plugin.webvideo.conf @@ -0,0 +1,10 @@ +# Command line parameters for vdr-plugin-webvideo +# +# Recognized parameters: +# +# -d DIR, --downloaddir=DIR Save downloaded files to DIR +# -t DIR, --templatedir=DIR Read video site templates from DIR +# -c FILE, --conf=FILE Read settings from FILE +# +--downloaddir=/var/lib/webvideo +--templatedir=/usr/share/webvi/templates diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..33bb639 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,43 @@ +#!/bin/sh +# postinst script for vdr-plugin-webvideo +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * <postinst> `configure' <most-recently-configured-version> +# * <old-postinst> `abort-upgrade' <new version> +# * <conflictor's-postinst> `abort-remove' `in-favour' <package> +# <new-version> +# * <postinst> `abort-remove' +# * <deconfigured's-postinst> `abort-deconfigure' `in-favour' +# <failed-install-package> <version> `removing' +# <conflicting-package> <version> +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + chown vdr:vdr /var/lib/webvideo + chmod g=rwx /var/lib/webvideo + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 + + diff --git a/debian/pycompat b/debian/pycompat new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/debian/pycompat @@ -0,0 +1 @@ +2 diff --git a/debian/python-webvi.docs b/debian/python-webvi.docs new file mode 100644 index 0000000..803a3ef --- /dev/null +++ b/debian/python-webvi.docs @@ -0,0 +1,2 @@ +README +README.webvi diff --git a/debian/python-webvi.install b/debian/python-webvi.install new file mode 100644 index 0000000..ad5c5ce --- /dev/null +++ b/debian/python-webvi.install @@ -0,0 +1,4 @@ +debian/tmp/usr/lib/python*/site-packages/* +debian/tmp/usr/bin/webvi +debian/tmp/usr/share/webvi/templates/* +debian/webvi.conf etc/ diff --git a/debian/python-webvi.manpages b/debian/python-webvi.manpages new file mode 100644 index 0000000..a09ac56 --- /dev/null +++ b/debian/python-webvi.manpages @@ -0,0 +1 @@ +debian/webvi.1 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..44d5f4a --- /dev/null +++ b/debian/rules @@ -0,0 +1,37 @@ +#!/usr/bin/make -f + +build/libwebvi0:: build/python-webvi +build/vdr-plugin-webvideo:: build/libwebvi0 + +DEB_PYTHON_SYSTEM := pycentral +DEB_PYTHON_MODULE_PACKAGES := python-webvi + +# debhelper must be included before python-distutils +include /usr/share/cdbs/1/rules/debhelper.mk +include /usr/share/cdbs/1/class/makefile.mk +include /usr/share/cdbs/1/class/python-distutils.mk + +DEB_COMPRESS_EXCLUDE := .py + +DEB_MAKE_BUILD_TARGET := all-noinstall VDRDIR=/usr/include/vdr +DEB_MAKE_INSTALL_TARGET := install-vdr-plugin install-python VDRDIR=/usr/include/vdr PREFIX=$(CURDIR)/debian/tmp/usr VDRPLUGINDIR=$(CURDIR)/debian/tmp/usr/lib/vdr/plugins VDRPLUGINCONFDIR=$(CURDIR)/debian/tmp/var/lib/vdr/plugins VDRLOCALEDIR=$(CURDIR)/debian/tmp/usr/share/locale +DEB_MAKE_CHECK_TARGET = + +DEB_INSTALL_CHANGELOGS_ALL = HISTORY + +#DEB_DH_STRIP_ARGS = -Xlibvdr-webvideo -Xlibwebvi + +TXT2MANPAGES = debian/webvi.1 + +$(TXT2MANPAGES): %.1: %.1.txt + cat $< | grep -v "^###" | \ + eval "`cat $< | grep "^### txt2man" | sed "s/### //"`" >$@ + +common-build-indep:: $(TXT2MANPAGES) + +cleanbuilddir:: + $(MAKE) -o .dependencies clean + rm -f $(TXT2MANPAGES) + +common-binary-predeb-arch:: + sh /usr/share/vdr-dev/dependencies.sh diff --git a/debian/vdr-plugin-webvideo.NEWS b/debian/vdr-plugin-webvideo.NEWS new file mode 100644 index 0000000..5499f66 --- /dev/null +++ b/debian/vdr-plugin-webvideo.NEWS @@ -0,0 +1,10 @@ +vdr-plugin-webvideo (0.1.4-1) unstable; urgency=low + + The default download directory is now /var/lib/webvideo instead of + /var/lib/video/webvideo, because the video directory is expected to + hold only VDR's own files. The videos are NOT automatically copied + to the new location. If you want to keep using the old location (it + should not cause problems in practice), leave the path in + /etc/vdr/plugins/plugin.webvideo.conf unchanged. + + -- Antti Ajanki <antti.ajanki@iki.fi> Mon, 04 May 2009 20:42:36 +0300 diff --git a/debian/vdr-plugin-webvideo.dirs b/debian/vdr-plugin-webvideo.dirs new file mode 100644 index 0000000..bab0473 --- /dev/null +++ b/debian/vdr-plugin-webvideo.dirs @@ -0,0 +1 @@ +var/lib/webvideo diff --git a/debian/vdr-plugin-webvideo.docs b/debian/vdr-plugin-webvideo.docs new file mode 100644 index 0000000..2707c31 --- /dev/null +++ b/debian/vdr-plugin-webvideo.docs @@ -0,0 +1,2 @@ +README +README.vdrplugin diff --git a/debian/vdr-plugin-webvideo.install b/debian/vdr-plugin-webvideo.install new file mode 100644 index 0000000..0cf0829 --- /dev/null +++ b/debian/vdr-plugin-webvideo.install @@ -0,0 +1,6 @@ +debian/tmp/usr/lib/vdr/plugins/libvdr-webvideo.so.* +debian/tmp/usr/share/locale +debian/tmp/var/lib/vdr/plugins/webvideo +debian/plugin.webvideo.conf etc/vdr/plugins/ +src/vdr-plugin/mime.types var/lib/vdr/plugins/webvideo/ +debian/webvi.plugin.conf var/lib/vdr/plugins/webvideo/ diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..2cb5267 --- /dev/null +++ b/debian/watch @@ -0,0 +1,2 @@ +version=2 +http://users.tkk.fi/~aajanki/vdr/webvideo/vdr-webvideo-(.*)\.tgz diff --git a/debian/webvi.1.txt b/debian/webvi.1.txt new file mode 100644 index 0000000..89140ed --- /dev/null +++ b/debian/webvi.1.txt @@ -0,0 +1,67 @@ +### txt2man -s 1 -t WEBVI -v "download utility for media sharing websites" + +NAME + webvi - download video and audio from media sharing websites + +SYNOPSIS + webvi [options] + +DESCRIPTION + webvi is a command line tool for downloading video and audio files + from certain media sharing websites, such as YouTube or Google Video. + +OPTIONS + -h, --help show this help message and exit + -t DIR, --templatepath=DIR read video site templates from DIR + +USAGE + The program communicates with webvid daemon, which must be running in + the background. + + The content of video sharing websites is presented as series of menus. + The menus consists of two kinds of links. Navigation links, which can + be identified by [brackets], are used to navigate the site. + Non-bracketed links are media streams that can be downloaded. + + Following commands are recognized: + + help Show help + select x Select a link whose index is x + download x Download a media stream whose index is x + stream x Play a media stream whose index is x + back Go backward in history + forward Go forward in history + display Redisplay the current menu + menu Go back to the main menu + quit Quit the program + + x is an index of a link in the current menu. Entering an index number + x without any command is a shorthand for "select x". + +CONFIG FILE + webvi will read the following config files: /etc/webvi.conf and + ~/.webvi. The files are in INI format. The following options are + recognized in [webvi] section: + + templatepath Path to video site templates + streamplayers1 to streamplayer9 are alternative player commands to be used for streaming videos + + It is possible to set lower and upper bounds for stream quality in + [site-*] sections: + + download-min-quality Minimum accepted quality for downloading + download-max-quality Maximum accepted quality for downloading + stream-min-quality Minimum accepted quality for streaming + stream-max-quality Maximum accepted quality for streaming + + Currently only Youtube module offers multiple versions of the + streams. These are the available quality scores for Youtube + (section [site-youtube]): + + 50 standard quality (320x240, i.e. what you get in the web browser) + 60 medium quality (480x360 MP4) + 70 HD quality (720p) + +AUTHOR + This manual page was written by Tobias Grimm <tg@e-tobi.net> and + Antti Ajanki <antti.ajanki@iki.fi>. diff --git a/debian/webvi.conf b/debian/webvi.conf new file mode 100644 index 0000000..647376b --- /dev/null +++ b/debian/webvi.conf @@ -0,0 +1,20 @@ +[webvi] + +# streamplayer1 to streamplayer9 are alternative media players to be +# used for streaming. The substring %s will be replaced by the stream +# URL. The players are tried one by one starting from streamplayer1 +# until one of them succeeds playing the stream. +# +#streamplayer1 = vlc "%s" +#streamplayer2 = totem "%s" +#streamplayer3 = mplayer "%s" +#streamplayer4 = xine "%s" + +# templatepath is path to the video service template directory +templatepath = /usr/share/webvi/templates + +[site-youtube] + +# Limit the quality when streaming to make the playback smooth even if +# the network connection is slow. +stream-max-quality = 50 diff --git a/debian/webvi.plugin.conf b/debian/webvi.plugin.conf new file mode 100644 index 0000000..675f8d7 --- /dev/null +++ b/debian/webvi.plugin.conf @@ -0,0 +1,3 @@ +[site-youtube] + +stream-max-quality = 50 diff --git a/doc/developers.txt b/doc/developers.txt new file mode 100644 index 0000000..8259e9c --- /dev/null +++ b/doc/developers.txt @@ -0,0 +1,152 @@ +libwebvi interface +================== + +See src/libwebvi/libwebvi.h for C API, src/unittest/testlibwebvi.c for +example code in C, and src/libwebvi/webvi/api.py for Python API. + + +XSLT templates for video sites +============================== + +libwebvi transforms the HTML of the web pages into a simple XML-based +format using the sites specific XSLT stylesheets stored under +templates directory. The XML describes navigation links and video +streams found on the web pages. The VDR plugin or the command line +client interprets the XML and displays a menu to the user. + +The following is an example libwebvi XML response for navigation page +(WEBVIREQ_MENU) query: + +<wvmenu> + <title>Page title</title> + + <link> + <label>Label of the link</label> + <ref>wvt:///youtube/description.xsl?srcurl=...</ref> + <stream>wvt:///youtube/video.xsl?srcurl=...</stream> + </link> + + <textarea> + <label>Text that will be shown to the user</label> + </textarea> + + <textfield name="search_query"> + <label>Search terms</label> + </textfield> + + <itemlist name="search_sort"> + <label>Sort by</label> + <item value="">Relevance</item> + <item value="video_date_uploaded">Date Added</item> + <item value="video_view_count">View Count</item> + <item value="video_avg_rating">Rating</item> + </itemlist> + + <button> + <label>Send</label> + <submission>wvt:///youtube/navigation.xsl?srcurl=...</submission> + </button> +</wvmenu> + +<wvmenu> is the root node of a menu page. Possible children are +<title>, <link>, <textfield>, <itemlist>, <textarea>, and <button> +nodes. + +The content of <title> will be shown as the title of the menu. + +<link> is a navigation link. It must have a child <label>, which +contains the text that will be shown to the user, and at least one of +<ref> (a navigation reference) or <stream> (a media stream reference). +The user interface provides three ways for selecting the link: a +navigation action loads a new menu page by retrieving the reference in +<ref> node from the library (using request type WEBVIREQ_MENU), a file +action downloads the stream by requesting <stream> reference from the +library (WEBVIREQ_FILE), stream URL action retrieves a direct URL to +the stream by requesting reference <stream> (WEBVIREQ_STREAMURL). + +<textarea> defines a text widget. Again, child node <label> contains +the text that will be displayed to the user. + +<textfield> defines a control for entering a string. + +<itemlist> defines a control for selecting one of the specified +<item>s. + +<button> is a special kind of link. It encodes the state of +<textfield>s and <itemlist>s into its reference. When the <button> is +selected the state of other controls is send to the library. The +reference is constructed by concatenating encoded values of each +<textfield> and <itemlist> to the content of <submission> node. Each +control is encoded as string "subst=name,value" where name is +URL encoded presentation of the name nodes name attribute, and value +is URL encoded presentation of the <textfield>'s UTF-8 input or value +attribute of currently selected <item> in an <itemlist>. The encodings +are joined by putting a "&" between them. + + +Format of wvt:// references +=========================== + +In libwebvi, the navigation and menu requests are encoded encoded as +URIs with scheme wvt:// . Typically, these references are extracted +from <ref> or <stream> nodes in the XML menu documents. This section +explains the format of the references. + +The references are of the form + +wvt://templatedir/template.xsl?srcurl=...&other_params=value + +The value of the srcurl parameter is a URL (typically an HTTP URL) of +a web page on a video site. templatedir/template.xsl specifies a name +of the XSLT template that is applied to the srcurl to get an XML menu. +srcurl can be empty. + +templatedir called bin is a special directory. It contains executable +programs or scripts instead of XSLT templates. When a reference to the +bin directory is encountered, the named script is executed. Script's +standard output is returned as the results of the operation. The +script should return 0 on success. + +Parameters that affect the template processing can be appended to the +reference. The parameter name and value are separated by '=', +different parameters are separated by '&'. Following parameters are +understood: + +param=name,value + +name and value are passed to the XSLT processor as an external +parameter. + +subst=name,value + +Replaces string {name} in srcurl with value before retrieving it. This +is used in search pages as discussed in previous section. + +contenttype=value + +value is used as content type if the server does not send a proper +content type header. + +arg=value + +Used to pass command line arguments to the external scripts in the bin +directory. There can be multiple args. + +minquality=value +maxquality=value + +Only the streams whose quality ("priority" attribute of the "url" tag) +is between minquality and maxquality are considered as candidates when +selecting a stream to play. The default limits are 0 and 100. + +postprocess=json2xml + +The source is a JSON document which should be converted to XML before +the XSLT transformation. + +http-header=headername,headervalue + +Append a HTTP header to the request. Ignored on transfers that use +some other protocol besides HTTP. + +The reference to the main menu is wvt:///?srcurl=mainmenu . diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..30f3ee0 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +import re +import os +import os.path +import sys +from distutils.core import setup + +def extract_version(): + sys.path.append('src/libwebvi/webvi') + import version + sys.path.pop() + return version.VERSION + +def install_service_files(): + sourcedir = 'templates' + destdir = 'share/webvi/templates' + + res = [] + for service in os.listdir(sourcedir): + sdir = os.path.join(sourcedir, service) + sfiles = [] + for f in os.listdir(sdir): + sfiles.append(os.path.join(sdir, f)) + res.append((os.path.join(destdir, service), sfiles)) + return res + +setup( + name='libwebvi', + version=extract_version(), + description='webvideo downloader library and command line client', + author='Antti Ajanki', + author_email='antti.ajanki@iki.fi', + license='GPLv3', + url='http://users.tkk.fi/~aajanki/vdr/webvideo', + package_dir = {'webvi': 'src/libwebvi/webvi', 'webvicli': 'src/webvicli/webvicli'}, + packages=['webvi', 'webvicli'], + scripts=['src/webvicli/webvi'], + data_files=install_service_files() + ) diff --git a/src/libwebvi/Makefile b/src/libwebvi/Makefile new file mode 100644 index 0000000..131c4a7 --- /dev/null +++ b/src/libwebvi/Makefile @@ -0,0 +1,34 @@ +PREFIX ?= /usr/local + +LIBNAME=libwebvi.so +LIBSONAME=$(LIBNAME).0 +LIBMINOR=$(LIBSONAME).4 + +VERSION:=$(shell cat ../version) +PYLIB:=$(shell python pythonlibname.py) +DEFINES:=-DPYTHONSHAREDLIB=\"$(PYLIB)\" -DLIBWEBVI_VERSION=\"$(VERSION)\" +# append -DDEBUG to DEFINES to get debug output + +all: $(LIBMINOR) + +libwebvi.o: libwebvi.c libwebvi.h + $(CC) -fPIC -Wall -O2 -g $(CFLAGS) $(DEFINES) `python-config --cflags` -c -o libwebvi.o libwebvi.c + +$(LIBMINOR): libwebvi.o + $(CC) -shared -Wl,-soname,$(LIBSONAME) -Wl,--as-needed libwebvi.o `python-config --ldflags` -o $(LIBMINOR) + ln -sf $(LIBMINOR) $(LIBSONAME) + ln -sf $(LIBSONAME) $(LIBNAME) + +libwebvi.a: libwebvi.o + ar rsc libwebvi.a libwebvi.o + +clean: + rm -f *.o *~ libwebvi.so* libwebvi.a + rm -f webvi/*.pyc webvi/*~ + +install: $(LIBMINOR) + mkdir -p $(PREFIX)/lib + cp --remove-destination -d $(LIBNAME)* $(PREFIX)/lib + /sbin/ldconfig $(PREFIX)/lib + +.PHONY: clean install diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c new file mode 100644 index 0000000..c4d9aed --- /dev/null +++ b/src/libwebvi/libwebvi.c @@ -0,0 +1,814 @@ +/* + * libwebvi.c: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * 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/>. + */ + +#include <Python.h> +#include <stdio.h> +#include <dlfcn.h> + +#include "libwebvi.h" + +static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION; + +static const int MAX_ERROR_MESSAGE_LENGTH = 512; +static const int MAX_MSG_STRING_LENGTH = 512; + +static PyThreadState *main_state = NULL; + +typedef struct per_interpreter_data_t { + PyThreadState *interp; + PyObject *webvi_module; + char *last_error; + WebviMsg latest_message; +} per_interpreter_data; + +#ifdef DEBUG + +#define debug(x...) fprintf(stderr, x) +#define handle_pyerr() { if (PyErr_Occurred()) { PyErr_Print(); } } + +#else + +#define debug(x...) +#define handle_pyerr() PyErr_Clear() + +#endif + + +/********************************************************************** + * + * Internal functions + */ + +static PyObject *call_python(PyObject *webvi_module, + const char *funcname, + PyObject *args) { + PyObject *func, *val = NULL; + +#ifdef DEBUG + debug("call_python %s ", funcname); + if (PyObject_Print(args, stderr, 0) == -1) + debug("<print failed>"); + debug("\n"); +#endif + + func = PyObject_GetAttrString(webvi_module, funcname); + if (func) { + val = PyObject_CallObject(func, args); + + Py_DECREF(func); + } + + return val; +} + +static long set_callback(PyObject *webvi_module, WebviHandle h, + WebviOption callbacktype, + webvi_callback callback, + PyObject *prototype) { + long res = WEBVIERR_UNKNOWN_ERROR; + + if (prototype && PyCallable_Check(prototype)) { + PyObject *args = Py_BuildValue("(l)", (long)callback); + PyObject *val = PyObject_CallObject(prototype, args); + Py_DECREF(args); + + if (val) { + PyObject *webvihandle = PyInt_FromLong(h); + PyObject *option = PyInt_FromLong(callbacktype); + PyObject *args2 = PyTuple_Pack(3, webvihandle, option, val); + PyObject *retval = call_python(webvi_module, "set_opt", args2); + Py_DECREF(args2); + Py_DECREF(option); + Py_DECREF(webvihandle); + Py_DECREF(val); + + if (retval) { + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + Py_DECREF(retval); + } + } + } + + if (res == WEBVIERR_UNKNOWN_ERROR) + handle_pyerr(); + + return res; +} + +/* + * Converts PyInt to WebviResult. + * + * If intobject is NULL, assumes that a Python exception has occurred. + */ +static WebviResult pyint_as_webviresult(PyObject *intobject) { + if (intobject && PyInt_Check(intobject)) + return PyInt_AsLong(intobject); + + handle_pyerr(); + + return WEBVIERR_UNKNOWN_ERROR; +} + +/* + * Duplicate Python string as C string. If the parameter is a unicode + * object, it is encoded to UTF-8. The caller must free the returned + * memory. + */ +static char *PyString_strdupUTF8(PyObject *string) { + char *buffer = NULL; + Py_ssize_t len = -1; + char *ret = NULL; + PyObject *realstring = string; + Py_INCREF(realstring); + + if (PyUnicode_Check(realstring)) { + PyObject *encoded = PyUnicode_AsUTF8String(realstring); + if (encoded) { + Py_DECREF(realstring); + realstring = encoded; + } else { + handle_pyerr(); + } + } + + if (PyString_AsStringAndSize(realstring, &buffer, &len) == -1) { + handle_pyerr(); + buffer = ""; + len = 0; + } + + if (buffer) { + ret = (char *)malloc((len+1)*sizeof(char)); + if (ret) + memcpy(ret, buffer, len+1); + } + + Py_DECREF(realstring); + + return ret; +} + +/********************************************************************** + * + * Public functions + */ + +int webvi_global_init() { + if (main_state) + return 0; + + // Python modules in lib-dynload/*.so do not correctly depend on + // libpython*.so. We need to dlopen the library here, otherwise + // importing webvi dies with "undefined symbol: + // PyExc_ValueError". See http://bugs.python.org/issue4434 + dlopen(PYTHONSHAREDLIB, RTLD_LAZY | RTLD_GLOBAL); + + Py_InitializeEx(0); + PyEval_InitThreads(); + main_state = PyThreadState_Get(); + PyEval_ReleaseLock(); /* release GIL acquired by PyEval_InitThreads */ + + return 0; +} + +void webvi_cleanup(int cleanup_python) { + /* Should we kill active interpreters first? */ + + if (cleanup_python != 0) { + PyEval_AcquireLock(); + PyThreadState_Swap(main_state); + Py_Finalize(); + } +} + +WebviCtx webvi_initialize_context(void) { + per_interpreter_data *ctx = (per_interpreter_data *)malloc(sizeof(per_interpreter_data)); + if (!ctx) + goto err; + + PyEval_AcquireLock(); + + ctx->interp = NULL; + ctx->last_error = NULL; + ctx->latest_message.msg = 0; + ctx->latest_message.handle = -1; + ctx->latest_message.status_code = -1; + ctx->latest_message.data = (char *)malloc(MAX_MSG_STRING_LENGTH*sizeof(char)); + if (!ctx->latest_message.data) + goto err; + + ctx->interp = Py_NewInterpreter(); + if (!ctx->interp) { + debug("Py_NewInterpreter failed\n"); + goto err; + } + + PyThreadState_Swap(ctx->interp); + + ctx->webvi_module = PyImport_ImportModule("webvi.api"); + if (!ctx->webvi_module) { + debug("import webvi.api failed\n"); + handle_pyerr(); + goto err; + } + + /* These are used to wrap C-callbacks into Python callables. + Keep in sync with libwebvi.h. */ + if (PyRun_SimpleString("from ctypes import CFUNCTYPE, c_int, c_size_t, c_char_p, c_void_p\n" + "WriteCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n" + "ReadCallback = CFUNCTYPE(c_size_t, c_char_p, c_size_t, c_void_p)\n") != 0) { + debug("callback definitions failed\n"); + goto err; + } + + PyEval_ReleaseThread(ctx->interp); + + return (WebviCtx)ctx; + +err: + if (ctx) { + if (ctx->interp) { + Py_EndInterpreter(ctx->interp); + PyThreadState_Swap(NULL); + } + + PyEval_ReleaseLock(); + + if (ctx->latest_message.data) + free(ctx->latest_message.data); + free(ctx); + } + + return 0; +} + +void webvi_cleanup_context(WebviCtx ctx) { + if (ctx == 0) + return; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyThreadState_Swap(c->interp); + + /* FIXME: explicitly terminate all active handles? */ + + Py_DECREF(c->webvi_module); + c->webvi_module = NULL; + + Py_EndInterpreter(c->interp); + c->interp = NULL; + + PyThreadState_Swap(NULL); + + free(c); +} + +const char* webvi_version(void) { + return VERSION; +} + +const char* webvi_strerror(WebviCtx ctx, WebviResult res) { + char *errmsg; + + per_interpreter_data *c = (per_interpreter_data *)ctx; + + if (!c->last_error) { + /* We are going to leak c->last_error */ + c->last_error = (char *)malloc(MAX_ERROR_MESSAGE_LENGTH*sizeof(char)); + if (!c->last_error) + return NULL; + } + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", res); + PyObject *msg = call_python(c->webvi_module, "strerror", args); + Py_DECREF(args); + + if (msg) { + errmsg = PyString_AsString(msg); + if (!errmsg) { + handle_pyerr(); + errmsg = "Internal error"; + } + + strncpy(c->last_error, errmsg, MAX_ERROR_MESSAGE_LENGTH-1); + c->last_error[MAX_ERROR_MESSAGE_LENGTH] = '\0'; + + Py_DECREF(msg); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return c->last_error; +} + +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(is)", conf, value); + PyObject *v = call_python(c->webvi_module, "set_config", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviHandle webvi_new_request(WebviCtx ctx, const char *webvireference, WebviRequestType type) { + WebviHandle res = -1; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(si)", webvireference, type); + PyObject *v = call_python(c->webvi_module, "new_request", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "start_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "stop_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h) { + WebviResult res; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(i)", h); + PyObject *v = call_python(c->webvi_module, "delete_handle", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *m = PyImport_AddModule("__main__"); + if (!m) { + handle_pyerr(); + PyEval_ReleaseThread(c->interp); + return res; + } + + PyObject *maindict = PyModule_GetDict(m); + + va_start(argptr, opt); + + switch (opt) { + case WEBVIOPT_WRITEFUNC: + { + webvi_callback writerptr = va_arg(argptr, webvi_callback); + PyObject *write_prototype = PyDict_GetItemString(maindict, "WriteCallback"); + if (write_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_WRITEFUNC, + writerptr, write_prototype); + break; + } + + case WEBVIOPT_WRITEDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_WRITEDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + case WEBVIOPT_READFUNC: + { + webvi_callback readerptr = va_arg(argptr, webvi_callback); + PyObject *read_prototype = PyDict_GetItemString(maindict, "ReadCallback"); + if (read_prototype) + res = set_callback(c->webvi_module, h, WEBVIOPT_READFUNC, + readerptr, read_prototype); + break; + } + + case WEBVIOPT_READDATA: + { + void *data = va_arg(argptr, void *); + PyObject *args = Py_BuildValue("(iil)", h, WEBVIOPT_READDATA, (long)data); + PyObject *v = call_python(c->webvi_module, "set_opt", args); + Py_DECREF(args); + + res = pyint_as_webviresult(v); + Py_XDECREF(v); + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...) { + va_list argptr; + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + va_start(argptr, info); + + switch (info) { + case WEBVIINFO_URL: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_LENGTH: + { + long *dest = va_arg(argptr, long *); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = -1; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && PyInt_Check(val)) { + *dest = PyInt_AsLong(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_CONTENT_TYPE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + case WEBVIINFO_STREAM_TITLE: + { + char **dest = va_arg(argptr, char **); + PyObject *args = Py_BuildValue("(ii)", h, info); + PyObject *v = call_python(c->webvi_module, "get_info", args); + Py_DECREF(args); + + *dest = NULL; + + if (v) { + if (PySequence_Check(v) && (PySequence_Length(v) >= 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *val = PySequence_GetItem(v, 1); + + if (PyInt_Check(retval) && + (PyString_Check(val) || PyUnicode_Check(val))) { + *dest = PyString_strdupUTF8(val); + res = PyInt_AsLong(retval); + } + + Py_DECREF(val); + Py_DECREF(retval); + } + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + break; + } + + va_end(argptr); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_fdset(WebviCtx ctx, + fd_set *read_fd_set, + fd_set *write_fd_set, + fd_set *exc_fd_set, + int *max_fd) +{ + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "fdset", NULL); + + if (v && PySequence_Check(v) && (PySequence_Length(v) == 5)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *readfd = PySequence_GetItem(v, 1); + PyObject *writefd = PySequence_GetItem(v, 2); + PyObject *excfd = PySequence_GetItem(v, 3); + PyObject *maxfd = PySequence_GetItem(v, 4); + PyObject *fd; + int i; + + if (readfd && PySequence_Check(readfd)) { + for (i=0; i<PySequence_Length(readfd); i++) { + fd = PySequence_GetItem(readfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), read_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (writefd && PySequence_Check(writefd)) { + for (i=0; i<PySequence_Length(writefd); i++) { + fd = PySequence_GetItem(writefd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), write_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (excfd && PySequence_Check(excfd)) { + for (i=0; i<PySequence_Length(excfd); i++) { + fd = PySequence_GetItem(excfd, i); + if (fd && PyInt_Check(fd)) + FD_SET(PyInt_AsLong(fd), exc_fd_set); + else + handle_pyerr(); + + Py_XDECREF(fd); + } + } + + if (maxfd && PyInt_Check(maxfd)) + *max_fd = PyInt_AsLong(maxfd); + else + handle_pyerr(); + + if (retval && PyInt_Check(retval)) + res = PyInt_AsLong(retval); + else + handle_pyerr(); + + Py_XDECREF(maxfd); + Py_XDECREF(excfd); + Py_XDECREF(writefd); + Py_XDECREF(readfd); + Py_XDECREF(retval); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviResult webvi_perform(WebviCtx ctx, int fd, int ev_bitmask, long *running_handles) { + WebviResult res = WEBVIERR_UNKNOWN_ERROR; + per_interpreter_data *c = (per_interpreter_data *)ctx; + + PyEval_AcquireThread(c->interp); + + PyObject *args = Py_BuildValue("(ii)", fd, ev_bitmask); + PyObject *v = call_python(c->webvi_module, "perform", args); + Py_DECREF(args); + + if (v && (PySequence_Check(v) == 1) && (PySequence_Size(v) == 2)) { + PyObject *retval = PySequence_GetItem(v, 0); + PyObject *numhandles = PySequence_GetItem(v, 1); + + if (PyInt_Check(numhandles)) + *running_handles = PyInt_AsLong(numhandles); + if (PyInt_Check(retval)) + res = PyInt_AsLong(retval); + + Py_DECREF(numhandles); + Py_DECREF(retval); + } else { + handle_pyerr(); + } + + Py_XDECREF(v); + + PyEval_ReleaseThread(c->interp); + + return res; +} + +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages) { + per_interpreter_data *c = (per_interpreter_data *)ctx; + + WebviMsg *msg = NULL; + + PyEval_AcquireThread(c->interp); + + PyObject *v = call_python(c->webvi_module, "pop_message", NULL); + + if (v) { + if ((PySequence_Check(v) == 1) && (PySequence_Length(v) == 4)) { + msg = &(c->latest_message); + msg->msg = WEBVIMSG_DONE; + msg->handle = -1; + msg->status_code = -1; + msg->data[0] = '\0'; + + PyObject *handle = PySequence_GetItem(v, 0); + if (handle && PyInt_Check(handle)) + msg->handle = (WebviHandle)PyInt_AsLong(handle); + Py_XDECREF(handle); + + PyObject *status = PySequence_GetItem(v, 1); + if (status && PyInt_Check(status)) + msg->status_code = (int)PyInt_AsLong(status); + Py_XDECREF(status); + + PyObject *errmsg = PySequence_GetItem(v, 2); + if (errmsg && + (PyString_Check(errmsg) || PyUnicode_Check(errmsg))) { + char *cstr = PyString_strdupUTF8(errmsg); + if (cstr) { + strncpy(msg->data, cstr, MAX_MSG_STRING_LENGTH); + msg->data[MAX_MSG_STRING_LENGTH-1] = '\0'; + + free(cstr); + } + } + Py_XDECREF(errmsg); + + PyObject *remaining = PySequence_GetItem(v, 3); + if (remaining && PyInt_Check(remaining)) + *remaining_messages = (int)PyInt_AsLong(remaining); + else + *remaining_messages = 0; + Py_XDECREF(remaining); + } + + if (msg->handle == -1) + msg = NULL; + + Py_DECREF(v); + } else { + handle_pyerr(); + } + + PyEval_ReleaseThread(c->interp); + + return msg; +} diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h new file mode 100644 index 0000000..dd7ff39 --- /dev/null +++ b/src/libwebvi/libwebvi.h @@ -0,0 +1,330 @@ +/* + * libwebvi.h: C bindings for webvi Python module + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * 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/>. + */ + +#ifndef __LIBWEBVI_H +#define __LIBWEBVI_H + +#include <sys/select.h> +#include <stdlib.h> + +typedef int WebviHandle; + +typedef ssize_t (*webvi_callback)(const char *, size_t, void *); + +typedef enum { + WEBVIMSG_DONE +} WebviMsgType; + +typedef struct { + WebviMsgType msg; + WebviHandle handle; + int status_code; + char *data; +} WebviMsg; + +typedef enum { + WEBVIREQ_MENU, + WEBVIREQ_FILE, + WEBVIREQ_STREAMURL +} WebviRequestType; + +typedef enum { + WEBVIERR_UNKNOWN_ERROR = -1, + WEBVIERR_OK = 0, + WEBVIERR_INVALID_HANDLE, + WEBVIERR_INVALID_PARAMETER +} WebviResult; + +typedef enum { + WEBVIOPT_WRITEFUNC, + WEBVIOPT_READFUNC, + WEBVIOPT_WRITEDATA, + WEBVIOPT_READDATA, +} WebviOption; + +typedef enum { + WEBVIINFO_URL, + WEBVIINFO_CONTENT_LENGTH, + WEBVIINFO_CONTENT_TYPE, + WEBVIINFO_STREAM_TITLE +} WebviInfo; + +typedef enum { + WEBVI_SELECT_TIMEOUT = 0, + WEBVI_SELECT_READ = 1, + WEBVI_SELECT_WRITE = 2, + WEBVI_SELECT_EXCEPTION = 4 +} WebviSelectBitmask; + +typedef enum { + WEBVI_CONFIG_TEMPLATE_PATH +} WebviConfig; + +typedef long WebviCtx; + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Initialize the library. Must be called before any other functions + * (the only exception is webvi_version() which can be called before + * the library is initialized). + * + * Returns 0, if initialization succeeds. + */ +int webvi_global_init(void); + +/* + * Frees all resources currently used by libwebvi and terminates all + * active connections. Do not call any libwebvi function after this. + * If the cleanup_python equals 0, the Python library is deinitialized + * by calling Py_Finalize(), otherwise the Python library is left + * loaded to be used by other modules of the program. + */ +void webvi_cleanup(int cleanup_python); + +/* + * Create a new context. A valid context is required for calling other + * functions in the library. The created contextes are independent of + * each other. The context must be destroyed by a call to + * webvi_cleanup_context when no longer needed. + * + * Return value 0 indicates an error. + */ +WebviCtx webvi_initialize_context(void); + +/* + * Free resources allocated by context ctx. The context can not be + * used anymore after a call to this function. + */ +void webvi_cleanup_context(WebviCtx ctx); + +/* + * Return the version of libwebvi as a string. The returned value + * points to a status buffer, and the caller should modify or not free() it. + */ +const char* webvi_version(void); + +/* + * Return a string describing an error code. The returned value points + * to a status buffer, and the caller should not modify or free() it. + */ +const char* webvi_strerror(WebviCtx ctx, WebviResult err); + +/* + * Set a new value for a global configuration option conf. + * + * Currently the only legal value for conf is TEMPLATE_PATH, which + * sets the base directory for the XSLT templates. + * + * The string pointed by value is copied to the library. + */ +WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, const char *value); + +/* + * Creates a new download request. + * + * webvireference is a wvt:// URI of the resource that should be + * downloaded. type should be WEBVIREQ_MENU, if the resource should be + * transformed into a XML menu (that is if webvireferece comes from + * <ref> tag), WEBVIREQ_FILE, if the resource points to a media stream + * (from <stream> tag) whose contents should be downloaded, or + * WEBVIREQ_STREAMURL, if the resource is points to a media stream + * whose real URL should be resolved. + * + * Typically, the reference has been acquired from a previously + * downloaded menu. A special constant "wvt:///?srcurl=mainmenu" with + * type WEBVIREQ_MENU can be used to download mainmenu. + * + * The return value is a handle to the newly created request. Value -1 + * indicates an error. + * + * The request is initialized but the actual network transfer is not + * started. You can set up additional configuration options on the + * handle using webvi_set_opt() before starting the handle with + * webvi_start_handle(). + */ +WebviHandle webvi_new_request(WebviCtx ctx, const char *wvtreference, WebviRequestType type); + +/* + * Starts the transfer on handle h. The transfer one or more sockets + * whose file descriptors are returned by webvi_fdset(). The actual + * transfer is done during webvi_perform() calls. + */ +WebviResult webvi_start_handle(WebviCtx ctx, WebviHandle h); + +/* + * Requests that the transfer on handle h shoud be aborted. After the + * library has actually finished aborting the transfer, the handle h + * is returned by webvi_get_message() with non-zero status code. + */ +WebviResult webvi_stop_handle(WebviCtx ctx, WebviHandle h); + +/* + * Frees resources associated with handle h. The handle can not be + * used after this call. If the handle is still in the middle of a + * transfer, the transfer is forcefully aborted. + */ +WebviResult webvi_delete_handle(WebviCtx ctx, WebviHandle h); + +/* + * Sets configuration options that changes behaviour of the handle. + * opt is one of the values of WebviOption enum as indicated below. + * The fourth parameter sets the value of the specified option. Its + * type depends on opt as discussed below. + * + * Possible values for opt: + * + * WEBVIOPT_WRITEFUNC + * + * Set the callback function that shall be called when data is read + * from the network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *). + * + * When the function is called, the first parameter is a pointer to + * the incoming data, the second parameters is the size of the + * incoming data block in bytes, and the third parameter is a pointer + * to user's data structure can be set by WEBVIOPT_WRITEDATA option. + * + * The callback funtion should return the number of bytes is + * processed. If this differs from the size of the incoming data + * block, it indicates that an error occurred and the transfer will be + * aborted. + * + * If write callback has not been set (or if it is set to NULL) the + * incoming data is printed to stdout. + * + * WEBVIOPT_WRITEDATA + * + * Sets the value that will be passed to the write callback. The + * fourth parameter is of type void *. + * + * WEBVIOPT_READFUNC + * + * Set the callback function that shall be called when data is to be + * send to network. The fourth parameter is a pointer to the callback + * funtion + * + * ssize_t (*webvi_callback)(const char *, size_t, void *) + * + * The first parameter is a pointer to a buffer where the data that is + * to be sent should be written. The second parameter is the maximum + * size of the buffer. The thirs parameter is a pointer to user data + * set with WEBVIOPT_READDATA. + * + * The return value should be the number of bytes actually written to + * the buffer. If the return value is -1, the transfer is aborted. + * + * WEBVIOPT_READDATA + * + * Sets the value that will be passed to the read callback. The + * fourth parameter is of type void *. + * + */ +WebviResult webvi_set_opt(WebviCtx ctx, WebviHandle h, WebviOption opt, ...); + +/* + * Get information specific to a WebviHandle. The value will be + * written to the memory location pointed by the third argument. The + * type of the pointer depends in the second parameter as discused + * below. + * + * Available information: + * + * WEBVIINFO_URL + * + * Receive URL. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + * WEBVIINFO_CONTENT_LENGTH + * + * Receive the value of Content-length field, or -1 if the size is + * unknown. The third parameter must be a pointer to long. + * + * WEBVIINFO_CONTENT_TYPE + * + * Receive the Content-type string. The returned value is NULL, if the + * Content-type is unknown. The third parameter must be a pointer to + * char *. The caller must free() the memory. + * + * WEBVIINFO_STREAM_TITLE + * + * Receive stream title. The returned value is NULL, if title is + * unknown. The third parameter must be a pointer to char *. The + * caller must free() the memory. + * + */ +WebviResult webvi_get_info(WebviCtx ctx, WebviHandle h, WebviInfo info, ...); + +/* + * Get active file descriptors in use by the library. The file + * descriptors that should be waited for reading, writing or + * exceptions are returned in read_fd_set, write_fd_set and + * exc_fd_set, respectively. The fd_sets are not cleared, but the new + * file descriptors are added to them. max_fd will contain the highest + * numbered file descriptor that was returned in one of the fd_sets. + * + * One should wait for action in one of the file descriptors returned + * by this function using select(), poll() or similar system call, + * and, after seeing action on a file descriptor, call webvi_perform + * on that descriptor. + */ +WebviResult webvi_fdset(WebviCtx ctx, fd_set *readfd, fd_set *writefd, fd_set *excfd, int *max_fd); + +/* + * Perform input or output action on a file descriptor. + * + * activefd is a file descriptor that was returned by an earlier call + * to webvi_fdset and has been signalled to be ready by select() or + * similar funtion. ev_bitmask should be OR'ed combination of + * WEBVI_SELECT_READ, WEBVI_SELECT_WRITE, WEBVI_SELECT_EXCEPTION to + * indicate that activefd has been signalled to be ready for reading, + * writing or being in exception state, respectively. ev_bitmask can + * also set to WEBVI_SELECT_TIMEOUT which means that the state is + * checked internally. On return, running_handles will contain the + * number of still active file descriptors. + * + * This function should be called with activefd set to 0 and + * ev_bitmask to WEBVI_SELECT_TIMEOUT periodically (every few seconds) + * even if no file descriptors have become ready to allow for timeout + * handling and other internal tasks. + */ +WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles); + +/* + * Return the next message from the message queue. Currently the only + * message, WEBVIMSG_DONE, indicates that a transfer on a handle has + * finished. The number of messages remaining in the queue after this + * call is written to remaining_messages. The pointers in the returned + * WebviMsg point to handle's internal buffers and is valid until the + * next call to webvi_get_message(). The caller should free the + * returned WebviMsg. The return value is NULL if there is no messages + * in the queue. + */ +WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages); + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/src/libwebvi/pythonlibname.py b/src/libwebvi/pythonlibname.py new file mode 100755 index 0000000..48f4b97 --- /dev/null +++ b/src/libwebvi/pythonlibname.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +import distutils.sysconfig +import os +import os.path + +libdir = distutils.sysconfig.get_config_var('LIBDIR') +ldlibrary = distutils.sysconfig.get_config_var('LDLIBRARY') + +libfile = os.readlink(os.path.join(libdir, ldlibrary)) +if not os.path.isabs(libfile): + libfile = os.path.join(libdir, libfile) + +print libfile diff --git a/src/libwebvi/webvi/__init__.py b/src/libwebvi/webvi/__init__.py new file mode 100644 index 0000000..b6d50d5 --- /dev/null +++ b/src/libwebvi/webvi/__init__.py @@ -0,0 +1 @@ +__all__ = ['api', 'asyncurl', 'constants', 'download', 'request', 'utils'] diff --git a/src/libwebvi/webvi/api.py b/src/libwebvi/webvi/api.py new file mode 100644 index 0000000..2fb24ab --- /dev/null +++ b/src/libwebvi/webvi/api.py @@ -0,0 +1,289 @@ +# api.py - webvi API +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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/>. + +"""webvi API + +Example workflow: + +1) Create a new request. ref is a wvt:// URI. + +handle = new_request(ref, WebviRequestType.MENU) + +2) Setup a callback function: + +setopt(handle, WebviOpt.WRITEFUNC, my_callback) + +3) Start the network transfer: + +start_handle(handle) + +4) Get active file descriptors, wait for activity on them, and let +webvi process the file descriptor. + +import select + +... + +readfd, writefd, excfd = fdset()[1:4] +readfd, writefd, excfd = select.select(readfd, writefd, excfd, 5.0) +for fd in readfd: + perform(fd, WebviSelectBitmask.READ) +for fd in writefd: + perform(fd, WebviSelectBitmask.WRITE) + +5) Iterate 4) until pop_message returns handle, which indicates that +the request has been completed. + +finished, status, errmsg, remaining = pop_message() +if finished == handle: + print 'done' +""" + +import request +import asyncore +import asyncurl +from constants import WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig + +# Human readable messages for WebviErr items +error_messages = { + WebviErr.OK: 'Succeeded', + WebviErr.INVALID_HANDLE: 'Invalid handle', + WebviErr.INVALID_PARAMETER: "Invalid parameter", + WebviErr.INTERNAL_ERROR: "Internal error" + } + +# Module-level variables +finished_queue = [] +request_list = request.RequestList() +socket_map = asyncore.socket_map + +# Internal functions + +class MyRequest(request.Request): + def request_done(self, err, errmsg): + """Calls the inherited function and puts the handle of the + finished request to the finished_queue.""" + finished_queue.append(self) + request.Request.request_done(self, err, errmsg) + +# Public functions + +def strerror(err): + """Return human readable error message for conststants.WebviErr""" + try: + return error_messages[err] + except KeyError: + return error_messages[WebviErr.INTERNAL_ERROR] + +def set_config(conf, value): + """Set a new value for a global configuration option conf. + + Currently the only legal value for conf is + constants.WebviConfig.TEMPLATE_PATH, which sets the base directory + for the XSLT templates. + """ + if conf == WebviConfig.TEMPLATE_PATH: + request.set_template_path(value) + return WebviErr.OK + else: + return WebviErr.INVALID_PARAMETER + +def new_request(reference, reqtype): + """Create a new request. + + reference is a wvt:// URI which typically comes from previously + opened menu. reqtype is one of conststants.WebviRequestType and + indicates wheter the reference is a navigation menu, stream that + should be downloaded, or a stream whose URL should be returned. + + Returns a handle (an integer) will be given to following + functions. Return value -1 indicates an error. + """ + req = MyRequest(reference, reqtype) + + if req.srcurl is None: + return -1 + + return request_list.put(req) + +def set_opt(handle, option, value): + """Set configuration options on a handle. + + option specifies option's name (one of constants.WebviOpt values) + and value is the new value for the option. + """ + + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if option == WebviOpt.WRITEFUNC: + req.writefunc = value + elif option == WebviOpt.WRITEDATA: + req.writedata = value + elif option == WebviOpt.READFUNC: + req.readfunc = value + elif option == WebviOpt.READDATA: + req.readdata = value + else: + return WebviErr.INVALID_PARAMETER + + return WebviErr.OK + +def get_info(handle, info): + """Get information about a handle. + + info is the type of data that is to be returned (one of + constants.WebviInfo values). + """ + try: + req = request_list[handle] + except KeyError: + return (WebviErr.INVALID_HANDLE, None) + + val = None + if info == WebviInfo.URL: + if req.dl is not None: + val = req.dl.get_url() + else: + val = req.srcurl + elif info == WebviInfo.CONTENT_LENGTH: + val = req.contentlength + elif info == WebviInfo.CONTENT_TYPE: + val = req.contenttype + elif info == WebviInfo.STREAM_TITLE: + val = req.streamtitle + else: + return (WebviErr.INVALID_PARAMETER, None) + + return (WebviErr.OK, val) + +def start_handle(handle): + """Start the network transfer on a handle.""" + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + req.start() + return WebviErr.OK + +def stop_handle(handle): + """Aborts network transfer on a handle. + + The abort is confirmed by pop_message() returning the handle with + an non-zero error code. + """ + try: + req = request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + if not req.is_finished(): + req.stop() + + return WebviErr.OK + +def delete_handle(handle): + """Frees resources related to handle. + + This should be called when the transfer has been completed and the + user is done with the handle. If the transfer is still in progress + when delete_handle() is called, the transfer is aborted. After + calling delete_handle() the handle value will be invalid, and + should not be feed to other functions anymore. + """ + try: + del request_list[handle] + except KeyError: + return WebviErr.INVALID_HANDLE + + return WebviErr.OK + +def pop_message(): + """Retrieve messages about finished requests. + + If a request has been finished since the last call to this + function, returns a tuple (handle, status, msg, num_messages), + where handle identifies the finished request, status is a numeric + status code (non-zero for an error), msg is a description of an + error as string, and num_messages is the number of messages that + can be retrieved by calling pop_messages() again immediately. If + the finished requests queue is empty, returns (-1, -1, "", 0). + """ + if finished_queue: + req = finished_queue.pop() + return (req.handle, req.status, req.errmsg, len(finished_queue)) + else: + return (-1, -1, "", 0) + +def fdset(): + """Get the list of file descriptors that are currently in use by + the library. + + Returrns a tuple, where the first item is a constants.WebviErr + value indicating the success of the call, the next three values + are lists of descriptors that should be monitored for reading, + writing, and exceptional conditions, respectively. The last item + is the maximum of the file descriptors in the three lists. + """ + readfd = [] + writefd = [] + excfd = [] + maxfd = -1 + + for fd, disp in socket_map.iteritems(): + if disp.readable(): + readfd.append(fd) + if fd > maxfd: + maxfd = fd + if disp.writable(): + writefd.append(fd) + if fd > maxfd: + maxfd = fd + + return (WebviErr.OK, readfd, writefd, excfd, maxfd) + +def perform(fd, ev_bitmask): + """Perform transfer on file descriptor fd. + + fd is a file descriptor that has been signalled to be ready by + select() or similar system call. ev_bitmask specifies what kind of + activity has been detected using values of + constants.WebviSelectBitmask. If ev_bitmask is + constants.WebviSelectBitmask.TIMEOUT the type of activity is check + by the function. + + This function should be called every few seconds with fd=-1, + ev_bitmask=constants.WebviSelectBitmask.TIMEOUT even if no + activity has been signalled on the file descriptors to ensure + correct handling of timeouts and other internal processing. + """ + if fd < 0: + asyncurl.poll() + else: + disp = socket_map.get(fd) + if disp is not None: + if ev_bitmask & WebviSelectBitmask.READ != 0 or \ + (ev_bitmask == 0 and disp.readable()): + disp.handle_read_event() + if ev_bitmask & WebviSelectBitmask.WRITE != 0 or \ + (ev_bitmask == 0 and disp.writable()): + disp.handle_write_event() + + return (WebviErr.OK, len(socket_map)) diff --git a/src/libwebvi/webvi/asyncurl.py b/src/libwebvi/webvi/asyncurl.py new file mode 100644 index 0000000..afc575a --- /dev/null +++ b/src/libwebvi/webvi/asyncurl.py @@ -0,0 +1,389 @@ +# asyncurl.py - Wrapper class for using pycurl objects in asyncore +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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/>. + +"""This is a wrapper for using pycurl objects in asyncore. + +Start a transfer by creating an async_curl_dispatch, and call +asyncurl.loop() instead of asyncore.loop(). +""" + +import asyncore +import pycurl +import traceback +import os +import select +import time +import cStringIO +from errno import EINTR + +SOCKET_TIMEOUT = pycurl.SOCKET_TIMEOUT +CSELECT_IN = pycurl.CSELECT_IN +CSELECT_OUT = pycurl.CSELECT_OUT +CSELECT_ERR = pycurl.CSELECT_ERR + +def poll(timeout=0.0, map=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + if map: + timeout = min(timeout, mdisp.timeout/1000.0) + + r = []; w = []; e = [] + for fd, obj in map.items(): + is_r = obj.readable() + is_w = obj.writable() + if is_r: + r.append(fd) + if is_w: + w.append(fd) + if is_r or is_w: + e.append(fd) + if [] == r == w == e: + time.sleep(timeout) + else: + try: + r, w, e = select.select(r, w, e, timeout) + except select.error, err: + if err[0] != EINTR: + raise + else: + return + + if [] == r == w == e: + mdisp.socket_action(SOCKET_TIMEOUT, 0) + return + + for fd in r: + obj = map.get(fd) + if obj is None: + continue + asyncore.read(obj) + + for fd in w: + obj = map.get(fd) + if obj is None: + continue + asyncore.write(obj) + + for fd in e: + obj = map.get(fd) + if obj is None: + continue + asyncore._exception(obj) + +def loop(timeout=30.0, use_poll=False, map=None, count=None, mdisp=None): + if map is None: + map = asyncore.socket_map + if mdisp is None: + mdisp = multi_dispatcher + + if use_poll and hasattr(select, 'poll'): + print 'poll2 not implemented' + poll_fun = poll + + if count is None: + while map: + poll_fun(timeout, map, mdisp) + + else: + while map and count > 0: + poll_fun(timeout, map, mdisp) + count = count - 1 + +def noop_callback(s): + pass + + +class curl_multi_dispatcher: + """A dispatcher for pycurl.CurlMulti() objects. An instance of + this class is created automatically. There is usually no need to + construct one manually.""" + def __init__(self, socket_map=None): + if socket_map is None: + self._map = asyncore.socket_map + else: + self._map = socket_map + self.dispatchers = {} + self.timeout = 1000 + self._sockets_removed = False + self._curlm = pycurl.CurlMulti() + self._curlm.setopt(pycurl.M_SOCKETFUNCTION, self.socket_callback) + self._curlm.setopt(pycurl.M_TIMERFUNCTION, self.timeout_callback) + + def socket_callback(self, action, socket, user_data, socket_data): +# print 'socket callback: %d, %s' % \ +# (socket, {pycurl.POLL_NONE: "NONE", +# pycurl.POLL_IN: "IN", +# pycurl.POLL_OUT: "OUT", +# pycurl.POLL_INOUT: "INOUT", +# pycurl.POLL_REMOVE: "REMOVE"}[action]) + + if action == pycurl.POLL_NONE: + return + elif action == pycurl.POLL_REMOVE: + if socket in self._map: + del self._map[socket] + self._sockets_removed = True + return + + obj = self._map.get(socket) + if obj is None: + obj = dispatcher_wrapper(socket, self) + self._map[socket] = obj + + if action == pycurl.POLL_IN: + obj.set_readable(True) + obj.set_writable(False) + elif action == pycurl.POLL_OUT: + obj.set_readable(False) + obj.set_writable(True) + elif action == pycurl.POLL_INOUT: + obj.set_readable(True) + obj.set_writable(True) + + def timeout_callback(self, msec): + self.timeout = msec + + def attach(self, curldisp): + """Starts a transfer on curl handle by attaching it to this + multihandle.""" + self.dispatchers[curldisp.curl] = curldisp + try: + self._curlm.add_handle(curldisp.curl) + except pycurl.error: + # the curl object is already on this multi-stack + pass + + while self._curlm.socket_all()[0] == pycurl.E_CALL_MULTI_PERFORM: + pass + + self.check_completed(True) + + def detach(self, curldisp): + """Removes curl handle from this multihandle, and fire its + completion callback function.""" + self.del_curl(curldisp.curl) + + # libcurl does not send POLL_REMOVE when a handle is aborted + for socket, curlobj in self._map.items(): + if curlobj == curldisp: + + print 'handle stopped but socket in map' + + del self._map[socket] + break + + def del_curl(self, curl): + try: + self._curlm.remove_handle(curl) + except pycurl.error: + # the curl object is not on this multi-stack + pass + if curl in self.dispatchers: + del self.dispatchers[curl] + curl.close() + + def socket_action(self, fd, evbitmask): + res = -1 + OK = False + while not OK: + try: + res = self._curlm.socket_action(fd, evbitmask) + OK = True + except pycurl.error: + # Older libcurls may return CURLM_CALL_MULTI_PERFORM, + # which pycurl (at least 7.19.0) converts to an + # exception. If that happens, call socket_action + # again. + pass + return res + + def check_completed(self, force): + if not force and not self._sockets_removed: + return + self._sockets_removed = False + + nmsg, success, failed = self._curlm.info_read() + for handle in success: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(0, None) + except: + self.handle_error() + self.del_curl(handle) + for handle, err, errmsg in failed: + disp = self.dispatchers.get(handle) + if disp is not None: + try: + disp.handle_completed(err, errmsg) + except: + self.handle_error() + self.del_curl(handle) + + def handle_error(self): + print 'Exception occurred in multicurl processing' + print traceback.format_exc() + + +class dispatcher_wrapper: + """An internal helper class that connects a file descriptor in the + asyncore.socket_map to a curl_multi_dispatcher.""" + def __init__(self, fd, multicurl): + self.fd = fd + self.multicurl = multicurl + self.read_flag = False + self.write_flag = False + + def readable(self): + return self.read_flag + + def writable(self): + return self.write_flag + + def set_readable(self, x): + self.read_flag = x + + def set_writable(self, x): + self.write_flag = x + + def handle_read_event(self): + self.multicurl.socket_action(self.fd, CSELECT_IN) + self.multicurl.check_completed(False) + + def handle_write_event(self): + self.multicurl.socket_action(self.fd, CSELECT_OUT) + self.multicurl.check_completed(False) + + def handle_expt_event(self): + self.multicurl.socket_action(self.fd, CSELECT_ERR) + self.multicurl.check_completed(False) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + + +class async_curl_dispatcher: + """A dispatcher class for pycurl transfers.""" + def __init__(self, url, auto_start=True): + """Initializes a pycurl object self.curl. The default is to + download url to an internal buffer whose content can be read + with self.recv(). If auto_start is False, the transfer is not + started before a call to add_channel(). + """ + self.url = url + self.socket = None + self.buffer = cStringIO.StringIO() + self.curl = pycurl.Curl() + self.curl.setopt(pycurl.URL, self.url) + self.curl.setopt(pycurl.FOLLOWLOCATION, 1) + self.curl.setopt(pycurl.AUTOREFERER, 1) + self.curl.setopt(pycurl.MAXREDIRS, 10) + self.curl.setopt(pycurl.FAILONERROR, 1) + self.curl.setopt(pycurl.WRITEFUNCTION, self.write_to_buf) + if auto_start: + self.add_channel() + + def write_to_buf(self, msg): + self.buffer.write(msg) + self.handle_read() + + def send(self, data): + raise NotImplementedError + + def recv(self, buffer_size): + # buffer_size is ignored + ret = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + return ret + + def add_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.attach(self) + + def del_channel(self, multidisp=None): + if multidisp is None: + multidisp = multi_dispatcher + multidisp.detach(self) + + def close(self): + self.del_channel() + + def log_info(self, message, type='info'): + if type != 'info': + print '%s: %s' % (type, message) + + def handle_error(self): + print 'Exception occurred during processing of a curl request' + print traceback.format_exc() + self.close() + + def handle_read(self): + self.log_info('unhandled read event', 'warning') + + def handle_write(self): + self.log_info('unhandled write event', 'warning') + + def handle_completed(self, err, errmsg): + """Called when the download has finished. err is a numeric + error code (or 0 if the download was successfull) and errmsg + is a curl error message as a string.""" + # It seems that a reference to self.write_to_buf forbids + # garbage collection from deleting this object. unsetopt() or + # setting the callback to None are not allowed. Is there a + # better way? + self.curl.setopt(pycurl.WRITEFUNCTION, noop_callback) + self.close() + + +def test(): + + class curl_request(async_curl_dispatcher): + def __init__(self, url, outfile, i): + async_curl_dispatcher.__init__(self, url, False) + self.id = i + self.outfile = outfile + self.add_channel() + + def handle_read(self): + buf = self.recv(4096) + print '%s: writing %d bytes' % (self.id, len(buf)) + self.outfile.write(buf) + + def handle_completed(self, err, errmsg): + if err != 0: + print '%s: error: %d %s' % (self.id, err, errmsg) + else: + print '%s: completed' % self.id + + curl_request('http://www.python.org', open('python.out', 'w'), 1) + curl_request('http://en.wikipedia.org/wiki/Main_Page', open('wikipedia.out', 'w'), 2) + loop(timeout=5.0) + + +pycurl.global_init(pycurl.GLOBAL_DEFAULT) +try: + multi_dispatcher +except NameError: + multi_dispatcher = curl_multi_dispatcher() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/constants.py b/src/libwebvi/webvi/constants.py new file mode 100644 index 0000000..2797178 --- /dev/null +++ b/src/libwebvi/webvi/constants.py @@ -0,0 +1,50 @@ +# constants.py - Python definitions for constants in libwebvi.h +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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/>. + +# Keep these in sync with libwebvi.h + +class WebviRequestType: + MENU = 0 + FILE = 1 + STREAMURL = 2 + +class WebviErr: + OK = 0 + INVALID_HANDLE = 1 + INVALID_PARAMETER = 2 + INTERNAL_ERROR = -1 + +class WebviOpt: + WRITEFUNC = 0 + READFUNC = 1 + WRITEDATA = 2 + READDATA = 3 + +class WebviInfo: + URL = 0 + CONTENT_LENGTH = 1 + CONTENT_TYPE = 2 + STREAM_TITLE = 3 + +class WebviSelectBitmask: + TIMEOUT = 0 + READ = 1 + WRITE = 2 + EXCEPTION = 4 + +class WebviConfig: + TEMPLATE_PATH = 0 diff --git a/src/libwebvi/webvi/download.py b/src/libwebvi/webvi/download.py new file mode 100644 index 0000000..34240ff --- /dev/null +++ b/src/libwebvi/webvi/download.py @@ -0,0 +1,470 @@ +# download.py - webvi downloader backend +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 os +import asyncore +import asynchat +import cStringIO +import urllib +import subprocess +import socket +import signal +import pycurl +import asyncurl +import utils +import version + +WEBVID_USER_AGENT = 'libwebvi/%s %s' % (version.VERSION, pycurl.version) +MOZILLA_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5' + +try: + from libmimms import libmms +except ImportError, e: + pass + +# Mapping from curl error codes to webvi errors. The error constants +# are defined only in pycurl 7.16.1 and newer. +if pycurl.version_info()[2] >= 0x071001: + CURL_ERROR_CODE_MAPPING = \ + {pycurl.E_OK: 0, + pycurl.E_OPERATION_TIMEOUTED: 408, + pycurl.E_OUT_OF_MEMORY: 500, + pycurl.E_PARTIAL_FILE: 504, + pycurl.E_READ_ERROR: 504, + pycurl.E_RECV_ERROR: 504, + pycurl.E_REMOTE_FILE_NOT_FOUND: 404, + pycurl.E_TOO_MANY_REDIRECTS: 404, + pycurl.E_UNSUPPORTED_PROTOCOL: 500, + pycurl.E_URL_MALFORMAT: 400, + pycurl.E_COULDNT_CONNECT: 506, + pycurl.E_COULDNT_RESOLVE_HOST: 506, + pycurl.E_COULDNT_RESOLVE_PROXY: 506, + pycurl.E_FILE_COULDNT_READ_FILE: 404, + pycurl.E_GOT_NOTHING: 504, + pycurl.E_HTTP_RETURNED_ERROR: 404, + pycurl.E_INTERFACE_FAILED: 506, + pycurl.E_LOGIN_DENIED: 403} +else: + CURL_ERROR_CODE_MAPPING = {pycurl.E_OK: 0} + +class DownloaderException(Exception): + def __init__(self, errcode, errmsg): + self.code = errcode + self.msg = errmsg + + def __str__(self): + return '%s %s' % (self.code, self.msg) + +def create_downloader(url, templatedir, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + """Downloader factory. + + Returns a suitable downloader object according to url type. Raises + DownloaderException if creating the downloader fails. + """ + if url == '': + return DummyDownloader('', writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('mms://') or url.startswith('mmsh://'): + try: + libmms + except (NameError, OSError): + raise DownloaderException(501, 'MMS scheme not supported. Install mimms.') + return MMSDownload(url, writefunc, headerfunc, donefunc, + headers_only) + + elif url.startswith('wvt://'): + executable, parameters = parse_external_downloader_wvt_uri(url, templatedir) + if executable is None: + raise DownloaderException(400, 'Invalid wvt:// URL') + try: + return ExternalDownloader(executable, parameters, writefunc, + headerfunc, donefunc, headers_only) + except OSError, (errno, strerror): + raise DownloaderException(500, 'Failed to execute %s: %s' % + (executable, strerror)) + + else: + return CurlDownload(url, writefunc, headerfunc, donefunc, + HTTPheaders, headers_only) + +def convert_curl_error(err, errmsg, aborted): + """Convert a curl error code err to webvi error code.""" + if err == pycurl.E_WRITE_ERROR: + return (402, 'Aborted') + elif err not in CURL_ERROR_CODE_MAPPING: + return (500, errmsg) + else: + return (CURL_ERROR_CODE_MAPPING[err], errmsg) + +def parse_external_downloader_wvt_uri(url, templatedir): + exe = None + params = [] + if not url.startswith('wvt:///bin/'): + return (exe, params) + + split = url[len('wvt:///bin/'):].split('?', 1) + exe = templatedir + '/bin/' + split[0] + + if len(split) > 1: + params = [urllib.unquote(x) for x in split[1].split('&')] + + return (exe, params) + +def _new_process_group(): + os.setpgid(0, 0) + +class DownloaderBase: + """Base class for downloaders.""" + def __init__(self, url): + self.url = url + + def start(self): + """Should start the download process.""" + pass + + def abort(self): + """Signals that the download should be aborted.""" + pass + + def get_url(self): + """Return the URL where the data was downloaded.""" + return self.url + + def get_body(self): + return '' + + def get_encoding(self): + """Return the encoding of the downloaded object, or None if + encoding is not known.""" + return None + + +class DummyDownloader(DownloaderBase, asyncore.file_dispatcher): + """This class doesn't actually download anything, but returns msg + string as if it had been result of a download operation. + """ + def __init__(self, msg, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + self.donefunc = donefunc + self.writefunc = writefunc + self.headers_only = headers_only + + readfd, writefd = os.pipe() + asyncore.file_dispatcher.__init__(self, readfd) + os.write(writefd, msg) + os.close(writefd) + + def set_file(self, fd): + # Like asyncore.file_dispatcher.set_file() but doesn't call + # add_channel(). We'll call add_channel() in start() when the + # download shall begin. + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def start(self): + if self.headers_only: + self.donefunc(0, None) + else: + self.add_channel() + + def readable(self): + return True + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + + def handle_close(self): + self.close() + + if self.donefunc is not None: + self.donefunc(0, '') + + +class CurlDownload(DownloaderBase, asyncurl.async_curl_dispatcher): + """Downloads a large number of different URL schemes using + libcurl.""" + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, HTTPheaders=None, headers_only=False): + DownloaderBase.__init__(self, url) + asyncurl.async_curl_dispatcher.__init__(self, url, False) + self.donefunc = donefunc + self.writefunc = writefunc + self.contenttype = None + self.running = True + self.aborted = False + + self.curl.setopt(pycurl.USERAGENT, WEBVID_USER_AGENT) + if headers_only: + self.curl.setopt(pycurl.NOBODY, 1) + if headerfunc is not None: + self.curl.setopt(pycurl.HEADERFUNCTION, headerfunc) + self.curl.setopt(pycurl.WRITEFUNCTION, self.writewrapper) + + headers = [] + if HTTPheaders is not None: + for headername, headerdata in HTTPheaders.iteritems(): + if headername == 'cookie': + self.curl.setopt(pycurl.COOKIE, headerdata) + else: + headers.append(headername + ': ' + headerdata) + + self.curl.setopt(pycurl.HTTPHEADER, headers) + + def start(self): + self.add_channel() + + def close(self): + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + asyncurl.async_curl_dispatcher.close(self) + self.running = False + + def abort(self): + self.aborted = True + + def writewrapper(self, data): + if self.aborted: + return 0 + + if self.writefunc is None: + return self.write_to_buf(data) + else: + return self.writefunc(data) + + def get_body(self): + return self.buffer.getvalue() + + def get_encoding(self): + if self.running: + self.contenttype = self.curl.getinfo(pycurl.CONTENT_TYPE) + + if self.contenttype is None: + return None + + values = self.contenttype.split(';', 1) + if len(values) > 1: + for par in values[1].split(' '): + if par.startswith('charset='): + return par[len('charset='):].strip('"') + + return None + + def handle_read(self): + # Do nothing to the read data here. Instead, let the base + # class to collect the data to self.buffer. + pass + + def handle_completed(self, err, errmsg): + asyncurl.async_curl_dispatcher.handle_completed(self, err, errmsg) + if self.donefunc is not None: + err, errmsg = convert_curl_error(err, errmsg, self.aborted) + self.donefunc(err, errmsg) + + +class MMSDownload(DownloaderBase, asyncore.file_dispatcher): + def __init__(self, url, writefunc=None, headerfunc=None, + donefunc=None, headers_only=False): + DownloaderBase.__init__(self, url) + self.r, self.w = os.pipe() + asyncore.file_dispatcher.__init__(self, self.r) + + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.relaylen = -1 + self.expectedlen = -1 + self.headers_only = headers_only + self.stream = None + self.errmsg = None + self.aborted = False + + def set_file(self, fd): + self.socket = asyncore.file_wrapper(fd) + self._fileno = self.socket.fileno() + + def recv(self, buffer_size): + data = self.stream.read() + if not data: + self.handle_close() + return '' + else: + return data + + def close(self): + if self.stream is not None: + self.stream.close() + + os.close(self.w) + asyncore.file_dispatcher.close(self) + + def readable(self): + return self.stream is not None + + def writable(self): + return False + + def start(self): + try: + self.stream = libmms.Stream(self.url, 1000000) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + os.write(self.w, '0') # signal that this dispatcher has data available + + if self.headerfunc: + # Output the length in a HTTP-like header field so that we + # can use the same callbacks as with HTTP downloads. + ext = utils.get_url_extension(self.url) + if ext == 'wma': + self.headerfunc('Content-Type: audio/x-ms-wma') + else: # if ext == 'wmv': + self.headerfunc('Content-Type: video/x-ms-wmv') + self.headerfunc('Content-Length: %d' % self.stream.length()) + + if self.headers_only: + self.handle_close() + else: + self.add_channel() + + def abort(self): + self.aborted = True + + def handle_read(self): + if self.aborted: + self.handle_close() + return '' + + try: + data = self.recv(4096) + if data and (self.writefunc is not None): + self.writefunc(data) + except libmms.Error, e: + self.errmsg = e.message + self.handle_close() + return + + def handle_close(self): + self.close() + self.stream = None + + if self.errmsg is not None: + self.donefunc(500, self.errmsg) + elif self.aborted: + self.donefunc(402, 'Aborted') + elif self.relaylen < self.expectedlen: + # We got fewer bytes than expected. Maybe the connection + # was lost? + self.donefunc(504, 'Download may be incomplete (length %d < %d)' % + (self.relaylen, self.expectedlen)) + else: + self.donefunc(0, '') + + +class ExternalDownloader(DownloaderBase, asyncore.file_dispatcher): + """Executes an external process and reads its result on standard + output.""" + def __init__(self, executable, parameters, writefunc=None, + headerfunc=None, donefunc=None, headers_only=False): + DownloaderBase.__init__(self, '') + asyncore.dispatcher.__init__(self, None, None) + self.executable = executable + self.writefunc = writefunc + self.headerfunc = headerfunc + self.donefunc = donefunc + self.headers_only = headers_only + self.contenttype = '' + self.aborted = False + + args = [] + for par in parameters: + try: + key, val = par.split('=', 1) + if key == 'contenttype': + self.contenttype = val + elif key == 'arg': + args.append(val) + except ValueError: + pass + + if args: + self.url = args[0] + else: + self.url = executable + self.cmd = [executable] + args + + self.process = None + + def start(self): + self.headerfunc('Content-Type: ' + self.contenttype) + + if self.headers_only: + self.donefunc(0, None) + return + + self.process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, + close_fds=True, + preexec_fn=_new_process_group) + asyncore.file_dispatcher.__init__(self, os.dup(self.process.stdout.fileno())) + + def abort(self): + if self.process is not None: + self.aborted = True + pg = os.getpgid(self.process.pid) + os.killpg(pg, signal.SIGTERM) + + def readable(self): + # Return True if the subprocess is still alive + return self.process is not None and self.process.returncode is None + + def writable(self): + return False + + def handle_read(self): + try: + data = self.recv(4096) + if data and self.writefunc is not None: + self.writefunc(data) + except socket.error: + self.handle_error() + return + + def handle_close(self): + self.close() + self.process.wait() + + if self.donefunc is not None: + if self.process.returncode == 0: + self.donefunc(0, '') + elif self.aborted and self.process.returncode == -signal.SIGTERM: + self.donefunc(402, 'Aborted') + else: + self.donefunc(500, 'Child process "%s" returned error %s' % \ + (' '.join(self.cmd), str(self.process.returncode))) + + self.process = None diff --git a/src/libwebvi/webvi/json2xml.py b/src/libwebvi/webvi/json2xml.py new file mode 100644 index 0000000..372e6c6 --- /dev/null +++ b/src/libwebvi/webvi/json2xml.py @@ -0,0 +1,69 @@ +import sys +import libxml2 + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + print 'Error: install simplejson' + raise + +def _serialize_to_xml(obj, xmlnode): + """Create XML representation of a Python object (list, tuple, + dist, or basic number and string types).""" + if type(obj) in (list, tuple): + listnode = libxml2.newNode('list') + for li in obj: + itemnode = libxml2.newNode('li') + _serialize_to_xml(li, itemnode) + listnode.addChild(itemnode) + xmlnode.addChild(listnode) + + elif type(obj) == dict: + dictnode = libxml2.newNode('dict') + for key, val in obj.iteritems(): + itemnode = libxml2.newNode(key.encode('utf-8')) + _serialize_to_xml(val, itemnode) + dictnode.addChild(itemnode) + xmlnode.addChild(dictnode) + + elif type(obj) in (str, unicode, int, long, float, complex, bool): + content = libxml2.newText(unicode(obj).encode('utf-8')) + xmlnode.addChild(content) + + elif type(obj) == type(None): + pass + + else: + raise TypeError('Unsupported type %s while serializing to xml' + % type(obj)) + +def json2xml(jsonstr, encoding=None): + """Convert JSON string jsonstr to XML tree.""" + try: + parsed = json.loads(jsonstr, encoding) + except ValueError: + return None + + xmldoc = libxml2.newDoc("1.0") + root = libxml2.newNode("jsondocument") + xmldoc.setRootElement(root) + + _serialize_to_xml(parsed, root) + + return xmldoc + +def test(): + xml = json2xml(open(sys.argv[1]).read()) + + if xml is None: + return + + print xml.serialize('utf-8') + + xml.freeDoc() + +if __name__ == '__main__': + test() diff --git a/src/libwebvi/webvi/request.py b/src/libwebvi/webvi/request.py new file mode 100644 index 0000000..e19eb9c --- /dev/null +++ b/src/libwebvi/webvi/request.py @@ -0,0 +1,617 @@ +# request.py - webvi request class +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 urllib +import libxml2 +import os.path +import cStringIO +import re +import download +import sys +import utils +import json2xml +from constants import WebviRequestType + +DEBUG = False + +DEFAULT_TEMPLATE_PATH = '/usr/local/share/webvi/templates' +template_path = DEFAULT_TEMPLATE_PATH + +def debug(msg): + if DEBUG: + if type(msg) == unicode: + sys.stderr.write(msg.encode('ascii', 'replace')) + else: + sys.stderr.write(msg) + sys.stderr.write('\n') + +def set_template_path(path): + global template_path + + if path is None: + template_path = os.path.realpath(DEFAULT_TEMPLATE_PATH) + else: + template_path = os.path.realpath(path) + + debug("set_template_path " + template_path) + +def parse_reference(reference): + """Parses URLs of the following form: + + wvt:///youtube/video.xsl?srcurl=http%3A%2F%2Fwww.youtube.com%2F¶m=name1,value1¶m=name2,value2 + + reference is assumed to be URL-encoded UTF-8 string. + + Returns (template, srcurl, params, processing_instructions) where + template if the URL path name (the part before ?), srcurl is the + parameter called srcurl, and params is a dictionary of (name, + quoted-value) pairs extracted from param parameters. Parameter + values are quoted so that the xslt parser handles them as string. + processing_instructions is dictionary of options that affect the + further processing of the data. + """ + try: + reference = str(reference) + except UnicodeEncodeError: + return (None, None, None, None) + + if not reference.startswith('wvt:///'): + return (None, None, None, None) + + ref = reference[len('wvt:///'):] + + template = None + srcurl = '' + parameters = {} + substitutions = {} + refsettings = {'HTTP-headers': {}} + + fields = ref.split('?', 1) + template = fields[0] + if len(fields) == 1: + return (template, srcurl, parameters, refsettings) + + for par in fields[1].split('&'): + paramfields = par.split('=', 1) + key = paramfields[0] + + if len(paramfields) == 2: + value = urllib.unquote(paramfields[1]) + else: + value = '' + + if key.lower() == 'srcurl': + srcurl = value + + elif key.lower() == 'param': + fields2 = value.split(',', 1) + pname = fields2[0].lower() + if len(fields2) == 2: + pvalue = "'" + fields2[1] + "'" + else: + pvalue = "''" + parameters[pname] = pvalue + + elif key.lower() == 'subst': + substfields = value.split(',', 1) + if len(substfields) == 2: + substitutions[substfields[0]] = substfields[1] + + elif key.lower() == 'minquality': + try: + refsettings['minquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'maxquality': + try: + refsettings['maxquality'] = int(value) + except ValueError: + pass + + elif key.lower() == 'postprocess': + refsettings.setdefault('postprocess', []).append(value) + + elif key.lower() == 'contenttype': + refsettings['overridecontenttype'] = value + + elif key.lower() == 'http-header': + try: + headername, headerdata = value.split(',', 1) + except ValueError: + continue + refsettings['HTTP-headers'][headername] = headerdata + + if substitutions: + srcurl = brace_substitution(srcurl, substitutions) + + return (template, srcurl, parameters, refsettings) + +def brace_substitution(template, subs): + """Substitute subs[x] for '{x}' in template. Unescape {{ to { and + }} to }. Unescaping is not done in substitution keys, i.e. while + scanning for a closing brace after a single opening brace.""" + strbuf = cStringIO.StringIO() + + last_pos = 0 + for match in re.finditer(r'{{?|}}', template): + next_pos = match.start() + if next_pos < last_pos: + continue + + strbuf.write(template[last_pos:next_pos]) + if match.group(0) == '{{': + strbuf.write('{') + last_pos = next_pos+2 + + elif match.group(0) == '}}': + strbuf.write('}') + last_pos = next_pos+2 + + else: # match.group(0) == '{' + key_end = template.find('}', next_pos+1) + if key_end == -1: + strbuf.write(template[next_pos:]) + last_pos = len(template) + break + + try: + strbuf.write(urllib.quote(subs[template[next_pos+1:key_end]])) + except KeyError: + strbuf.write(template[next_pos:key_end+1]) + last_pos = key_end+1 + + strbuf.write(template[last_pos:]) + return strbuf.getvalue() + + +class Request: + DEFAULT_URL_PRIORITY = 50 + + def __init__(self, reference, reqtype): + self.handle = None + self.dl = None + + # state variables + self.xsltfile, self.srcurl, self.xsltparameters, self.processing = \ + parse_reference(reference) + self.type = reqtype + self.status = -1 + self.errmsg = None + self.mediaurls = [] + + # stream information + self.contenttype = 'text/xml' + self.contentlength = -1 + self.streamtitle = '' + + # callbacks + self.writefunc = None + self.writedata = None + self.readfunc = None + self.readdata = None + + def handle_header(self, buf): + namedata = buf.split(':', 1) + if len(namedata) == 2: + headername, headerdata = namedata + if headername.lower() == 'content-type': + # Strip parameters like charset="utf-8" + self.contenttype = headerdata.split(';', 1)[0].strip() + elif headername.lower() == 'content-length': + try: + self.contentlength = int(headerdata.strip()) + except ValueError: + self.contentlength = -1 + + def setup_downloader(self, url, writefunc, headerfunc, donefunc, + HTTPheaders=None, headers_only=False): + try: + self.dl = download.create_downloader(url, + template_path, + writefunc, + headerfunc, + donefunc, + HTTPheaders, + headers_only) + self.dl.start() + except download.DownloaderException, exc: + self.dl = None + if donefunc is not None: + donefunc(exc.code, exc.msg) + + def start(self): + debug('start %s\ntemplate = %s, type = %s\n' + 'parameters = %s, processing = %s' % + (self.srcurl, self.xsltfile, self.type, str(self.xsltparameters), + str(self.processing))) + + if self.type == WebviRequestType.MENU and self.srcurl == 'mainmenu': + self.send_mainmenu() + else: + self.setup_downloader(self.srcurl, None, + self.handle_header, + self.finished_apply_xslt, + self.processing['HTTP-headers']) + + def stop(self): + if self.dl is not None: + debug("aborting") + self.dl.abort() + + def start_download(self, url=None): + """Initialize a download. + + If url is None, pop the first URL out of self.mediaurls. If + URL is an ASX playlist, read the content URL from it and start + to download the actual content. + """ + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + + debug('Start_download ' + url) + + # reset stream status + self.contenttype = 'text/xml' + self.contentlength = -1 + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, + self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + + else: + self.setup_downloader(url, self.writewrapper, + self.handle_header, + self.finished_download, + self.processing['HTTP-headers']) + + def check_and_send_url(self, url=None): + """Check if the target exists (currently only for HTTP URLs) + before relaying the URL to the client.""" + while url is None or url == '': + try: + url = self.mediaurls.pop(0) + except IndexError: + self.request_done(406, 'No more URLs left') + return + + debug('check_and_send_url ' + str(url)) + + if self.is_asx_playlist(url): + self.setup_downloader(url, None, self.handle_header, + self.finished_playlist_loaded, + self.processing['HTTP-headers']) + elif url.startswith('http://') or url.startswith('https://'): + self.checking_url = url + self.setup_downloader(url, None, None, + self.finished_check_url, + self.processing['HTTP-headers'], True) + else: + self.writewrapper(url) + self.request_done(0, None) + + def send_mainmenu(self): + """Build the XML main menu from the module description files + in the hard drive. + """ + if not os.path.isdir(template_path): + self.request_done(404, "Can't access service directory %s" % + template_path) + return + + debug('Reading XSLT templates from ' + template_path) + + # Find menu items in the service.xml files in the subdirectories + menuitems = {} + for f in os.listdir(template_path): + if f == 'bin': + continue + + filename = os.path.join(template_path, f, 'service.xml') + try: + doc = libxml2.parseFile(filename) + except libxml2.parserError: + debug("Failed to parse " + filename); + continue + + title = '' + url = '' + + root = doc.getRootElement() + if (root is None) or (root.name != 'service'): + debug("Root node is not 'service' in " + filename); + doc.freeDoc() + continue + node = root.children + while node is not None: + if node.name == 'title': + title = utils.get_content_unicode(node) + elif node.name == 'ref': + url = utils.get_content_unicode(node) + node = node.next + doc.freeDoc() + + if (title == '') or (url == ''): + debug("Empty <title> or <ref> in " + filename); + continue + + menuitems[title.lower()] = ('<link>\n' + '<label>%s</label>\n' + '<ref>%s</ref>\n' + '</link>\n' % + (libxml2.newText(title), + libxml2.newText(url))) + # Sort the menu items + titles = menuitems.keys() + titles.sort() + + # Build the menu + mainmenu = ('<?xml version="1.0"?>\n' + '<wvmenu>\n' + '<title>Select video source</title>\n') + for t in titles: + mainmenu += menuitems[t] + mainmenu += '</wvmenu>' + + self.dl = download.DummyDownloader(mainmenu, + writefunc=self.writewrapper, + donefunc=self.request_done) + self.dl.start() + + def writewrapper(self, inp): + """Wraps pycurl write callback (with the data as the only + parameter) into webvi write callback (with signature (data, + length, usertag)). If self.writefunc is not set, write to + stdout.""" + if self.writefunc is not None: + inplen = len(inp) + written = self.writefunc(inp, inplen, self.writedata) + if written != inplen: + self.dl.close() + self.request_done(405, 'Write callback failed') + else: + sys.stdout.write(inp) + + def is_asx_playlist(self, url): + if utils.get_url_extension(url).lower() == 'asx': + return True + else: + return False + + def get_url_from_asx(self, asx, asxurl): + """Simple ASX parser. Return the content of the first <ref> + tag.""" + try: + doc = libxml2.htmlReadDoc(asx, asxurl, None, + libxml2.HTML_PARSE_NOERROR | + libxml2.HTML_PARSE_NOWARNING | + libxml2.HTML_PARSE_NONET) + except libxml2.treeError: + debug('Can\'t parse ASX:\n' + asx) + return None + root = doc.getRootElement() + ret = self._get_ref_recursive(root).strip() + doc.freeDoc() + return ret + + def _get_ref_recursive(self, node): + if node is None: + return None + if node.name.lower() == 'ref': + href = node.prop('href') + if href is not None: + return href + child = node.children + while child: + res = self._get_ref_recursive(child) + if res is not None: + return res + child = child.next + return None + + def parse_mediaurl(self, xml, minpriority, maxpriority): + debug('parse_mediaurl\n' + xml) + + self.streamtitle = '???' + mediaurls = [] + + try: + doc = libxml2.parseDoc(xml) + except libxml2.parserError: + debug('Invalid XML') + return mediaurls + + root = doc.getRootElement() + if root is None: + debug('No root node') + return mediaurls + + urls_and_priorities = [] + node = root.children + while node: + if node.name == 'title': + self.streamtitle = utils.get_content_unicode(node) + elif node.name == 'url': + try: + priority = int(node.prop('priority')) + except (ValueError, TypeError): + priority = self.DEFAULT_URL_PRIORITY + + content = node.getContent() + if priority >= minpriority and priority <= maxpriority and content != '': + urls_and_priorities.append((priority, content)) + node = node.next + doc.freeDoc() + + urls_and_priorities.sort() + urls_and_priorities.reverse() + mediaurls = [b[1] for b in urls_and_priorities] + + return mediaurls + + def finished_download(self, err, errmsg): + if err == 0: + self.request_done(0, None) + elif err != 402 and self.mediaurls: + debug('Download failed (%s %s).\nTrying the next one.' % (err, errmsg)) + self.dl = None + self.start_download() + else: + self.request_done(err, errmsg) + + def finished_playlist_loaded(self, err, errmsg): + if err == 0: + url = self.get_url_from_asx(self.dl.get_body(), + self.dl.get_url()) + if url is None: + err = 404 + errmsg = 'No ref tag in ASX file' + else: + if not self.is_asx_playlist(url) and url.startswith('http:'): + # The protocol is really "Windows Media HTTP + # Streaming Protocol", not plain HTTP, even though + # the scheme in the ASX file says "http://". We + # can't do MS-WMSP but luckily most MS-WMSP + # servers support MMS, too. + url = 'mms:' + url[5:] + + if self.type == WebviRequestType.STREAMURL: + self.check_and_send_url(url) + else: + self.start_download(url) + + if err != 0: + if not self.mediaurls: + self.request_done(err, errmsg) + else: + if self.type == WebviRequestType.STREAMURL: + self.check_and_send_url() + else: + self.start_download() + + def finished_apply_xslt(self, err, errmsg): + if err != 0: + self.request_done(err, errmsg) + return + + url = self.srcurl + + # Add input documentURL to the parameters + params = self.xsltparameters.copy() + params['docurl'] = "'" + url + "'" + + minpriority = self.processing.get('minquality', 0) + maxpriority = self.processing.get('maxquality', 100) + + xsltpath = os.path.join(template_path, self.xsltfile) + + # Check that xsltpath is inside the template directory + if os.path.commonprefix([template_path, os.path.realpath(xsltpath)]) != template_path: + self.request_done(503, 'Insecure template path') + return + + xml = self.dl.get_body() + encoding = self.dl.get_encoding() + + if self.processing.has_key('postprocess') and \ + 'json2xml' in self.processing['postprocess']: + xmldoc = json2xml.json2xml(xml, encoding) + if xmldoc is None: + self.request_done(503, 'Invalid JSON content') + return + xml = xmldoc.serialize('utf-8') + encoding = 'utf-8' + + #debug(xml) + + resulttree = utils.apply_xslt(xml, encoding, url, + xsltpath, params) + if resulttree is None: + self.request_done(503, 'XSLT transformation failed') + return + + if self.type == WebviRequestType.MENU: + debug("result:") + debug(resulttree) + self.writewrapper(resulttree) + self.request_done(0, None) + elif self.type == WebviRequestType.STREAMURL: + self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority) + if self.mediaurls: + self.check_and_send_url() + else: + self.request_done(406, 'No valid URLs found') + elif self.type == WebviRequestType.FILE: + self.mediaurls = self.parse_mediaurl(resulttree, minpriority, maxpriority) + if self.mediaurls: + self.start_download() + else: + self.request_done(406, 'No valid URLs found') + else: + self.request_done(0, None) + + def finished_extract_playlist_url(self, err, errmsg): + if err == 0: + url = self.get_url_from_asx(self.dl.get_body(), + self.dl.get_url()) + if url is not None: + if self.is_asx_playlist(url): + self.setup_downloader(url, None, None, + self.finished_extract_playlist_url, + self.processing['HTTP-headers']) + else: + if url.startswith('http:'): + url = 'mms:' + url[5:] + self.check_and_send_url(url) + else: + self.request_done(503, 'XSLT tranformation failed to produce URL') + else: + self.request_done(err, errmsg) + + + def finished_check_url(self, err, errmsg): + if err == 0: + self.writewrapper(self.checking_url) + self.request_done(0, None) + else: + self.check_and_send_url() + + def request_done(self, err, errmsg): + debug('request_done: %d %s' % (err, errmsg)) + + self.status = err + self.errmsg = errmsg + self.dl = None + + def is_finished(self): + return self.status >= 0 + + +class RequestList(dict): + nextreqnum = 1 + + def put(self, req): + reqnum = RequestList.nextreqnum + RequestList.nextreqnum += 1 + req.handle = reqnum + self[reqnum] = req + return reqnum diff --git a/src/libwebvi/webvi/utils.py b/src/libwebvi/webvi/utils.py new file mode 100644 index 0000000..cefe09a --- /dev/null +++ b/src/libwebvi/webvi/utils.py @@ -0,0 +1,134 @@ +# utils.py - misc. utility functions +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 urlparse +import re +import libxml2 +import libxslt +import urllib + +def get_url_extension(url): + """Extracts and returns the file extension from a URL.""" + # The extension is located right before possible query + # ("?query=foo") or fragment ("#bar"). + try: + i = url.index('?') + url = url[:i] + except ValueError: + pass + # The extension is the part after the last '.' that does not + # contain '/'. + idot = url.rfind('.') + islash = url.rfind('/') + if idot > islash: + return url[idot+1:] + else: + return '' + +def urljoin_query_fix(base, url, allow_fragments=True): + """urlparse.urljoin in Python 2.5 (2.6?) and older is broken in + case url is a pure query. See http://bugs.python.org/issue1432. + This handles correctly the case where base is a full (http) url + and url is a query, and calls urljoin() for other cases.""" + if url.startswith('?'): + bscheme, bnetloc, bpath, bparams, bquery, bfragment = \ + urlparse.urlparse(base, '', allow_fragments) + bquery = url[1:] + return urlparse.urlunparse((bscheme, bnetloc, bpath, + bparams, bquery, bfragment)) + else: + return urlparse.urljoin(base, url, allow_fragments) + +def get_content_unicode(node): + """node.getContent() returns an UTF-8 encoded sequence of bytes (a + string). Convert it to a unicode object.""" + return unicode(node.getContent(), 'UTF-8', 'replace') + +def apply_xslt(buf, encoding, url, xsltfile, params=None): + """Apply xslt transform from file xsltfile to the string buf + with parameters params. url is the location of buf. Returns + the transformed file as a string, or None if the + transformation couldn't be completed.""" + stylesheet = libxslt.parseStylesheetFile(xsltfile) + + if stylesheet is None: + #self.log_info('Can\'t open stylesheet %s' % xsltfile, 'warning') + return None + try: + # htmlReadDoc fails if the buffer is empty but succeeds + # (returning an empty tree) if the buffer is a single + # space. + if buf == '': + buf = ' ' + + # Guess whether this is an XML or HTML document. + if buf.startswith('<?xml'): + doc = libxml2.readDoc(buf, url, None, + libxml2.XML_PARSE_NOERROR | + libxml2.XML_PARSE_NOWARNING | + libxml2.XML_PARSE_NONET) + else: + #self.log_info('Using HTML parser', 'debug') + doc = libxml2.htmlReadDoc(buf, url, encoding, + libxml2.HTML_PARSE_NOERROR | + libxml2.HTML_PARSE_NOWARNING | + libxml2.HTML_PARSE_NONET) + except libxml2.treeError: + stylesheet.freeStylesheet() + #self.log_info('Can\'t parse XML document', 'warning') + return None + resultdoc = stylesheet.applyStylesheet(doc, params) + stylesheet.freeStylesheet() + doc.freeDoc() + if resultdoc is None: + #self.log_info('Can\'t apply stylesheet', 'warning') + return None + + # Postprocess the document: + # Resolve relative URLs in srcurl (TODO: this should be done in XSLT) + root = resultdoc.getRootElement() + if root is None: + resultdoc.freeDoc() + return None + + node2 = root.children + while node2 is not None: + if node2.name not in ['link', 'button']: + node2 = node2.next + continue + + node = node2.children + while node is not None: + if (node.name == 'ref') or (node.name == 'stream') or \ + (node.name == 'submission'): + refurl = node.getContent() + + match = re.search(r'\?.*srcurl=([^&]*)', refurl) + if match is not None: + oldurl = urllib.unquote(match.group(1)) + absurl = urljoin_query_fix(url, oldurl) + newurl = refurl[:match.start(1)] + \ + urllib.quote(absurl) + \ + refurl[match.end(1):] + node.setContent(resultdoc.encodeSpecialChars(newurl)) + + node = node.next + node2 = node2.next + + ret = resultdoc.serialize('UTF-8') + resultdoc.freeDoc() + return ret diff --git a/src/libwebvi/webvi/version.py b/src/libwebvi/webvi/version.py new file mode 100644 index 0000000..26cb817 --- /dev/null +++ b/src/libwebvi/webvi/version.py @@ -0,0 +1,20 @@ +# version.py - webvi version +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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/>. + +MAJOR = '0' +MINOR = '2' +VERSION = MAJOR + '.' + MINOR diff --git a/src/unittest/Makefile b/src/unittest/Makefile new file mode 100644 index 0000000..81b0ea2 --- /dev/null +++ b/src/unittest/Makefile @@ -0,0 +1,11 @@ +CFLAGS=-O2 -g -Wall -I../libwebvi +LDFLAGS=-L../libwebvi -Wl,-rpath=../libwebvi -lwebvi + +all: testlibwebvi testdownload + +testlibwebvi: testlibwebvi.o ../libwebvi/libwebvi.so + +testdownload: testdownload.o ../libwebvi/libwebvi.so + +clean: + rm -f testlibwebvi testlibwebvi.o testdownload testdownload.o diff --git a/src/unittest/runtests.sh b/src/unittest/runtests.sh new file mode 100755 index 0000000..9afc7a5 --- /dev/null +++ b/src/unittest/runtests.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +export PYTHONPATH=../libwebvi + +./testlibwebvi +#./testdownload +#./testwebvi.py diff --git a/src/unittest/testdownload.c b/src/unittest/testdownload.c new file mode 100644 index 0000000..134150a --- /dev/null +++ b/src/unittest/testdownload.c @@ -0,0 +1,195 @@ +/* + * testlibwebvi.c: unittest for webvi C bindings + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * 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/>. + */ + +#include <stdio.h> +#include <sys/select.h> +#include <errno.h> + +#include "libwebvi.h" + +#define WVTREFERENCE "wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv%3Dk5LmKNYTqvk" + +#define CHECK_WEBVI_CALL(err, funcname) \ + if (err != WEBVIERR_OK) { \ + fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \ + returncode = 127; \ + goto cleanup; \ + } + +struct download_data { + long bytes_downloaded; + WebviCtx ctx; + WebviHandle handle; +}; + +ssize_t file_callback(const char *buf, size_t len, void *data) { + struct download_data *dldata = (struct download_data *)data; + + if (dldata->bytes_downloaded == 0) { + char *url, *title, *contentType; + long contentLength; + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_URL, &url) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (url) { + printf("File URL: %s\n", url); + free(url); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_STREAM_TITLE, &title) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (title) { + printf("Title: %s\n", title); + free(title); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_TYPE, &contentType) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + if (contentType) { + printf("Content type: %s\n", contentType); + free(contentType); + } + + if (webvi_get_info(dldata->ctx, dldata->handle, WEBVIINFO_CONTENT_LENGTH, &contentLength) != WEBVIERR_OK) { + fprintf(stderr, "webvi_get_info FAILED\n"); + return -1; + } + + printf("Content length: %ld\n", contentLength); + } + + dldata->bytes_downloaded += len; + + printf("\r%ld", dldata->bytes_downloaded); + + return len; +} + +int main(int argc, const char* argv[]) { + int returncode = 0; + WebviCtx ctx = 0; + WebviHandle handle = -1; + fd_set readfd, writefd, excfd; + int maxfd, fd, s, msg_remaining; + struct timeval timeout; + long running; + WebviMsg *donemsg; + int done; + struct download_data callback_data; + + printf("Testing %s\n", webvi_version()); + + if (webvi_global_init() != 0) { + fprintf(stderr, "webvi_global_init FAILED\n"); + return 127; + } + + ctx = webvi_initialize_context(); + if (ctx == 0) { + fprintf(stderr, "webvi_initialize_context FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"), + "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)"); + + handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_FILE); + if (handle == -1) { + fprintf(stderr, "webvi_new_request FAILED\n"); + returncode = 127; + goto cleanup; + } + + callback_data.bytes_downloaded = 0; + callback_data.ctx = ctx; + callback_data.handle = handle; + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &callback_data), + "webvi_set_opt(WEBVIOPT_WRITEDATA)"); + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, file_callback), + "webvi_set_opt(WEBVIOPT_WRITEFUNC)"); + CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle), + "webvi_start_handle"); + + done = 0; + do { + FD_ZERO(&readfd); + FD_ZERO(&writefd); + FD_ZERO(&excfd); + CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd), + "webvi_fdset"); + + timeout.tv_sec = 1; + timeout.tv_usec = 0; + s = select(maxfd+1, &readfd, &writefd, NULL, &timeout); + + if (s < 0) { + if (errno == EINTR) + continue; + + perror("select FAILED"); + returncode = 127; + goto cleanup; + + } if (s == 0) { + CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running), + "webvi_perform"); + } else { + for (fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running), + "webvi_perform"); + } + if (FD_ISSET(fd, &writefd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running), + "webvi_perform"); + } + } + } + + do { + donemsg = webvi_get_message(ctx, &msg_remaining); + if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) { + done = 1; + } + } while (msg_remaining > 0); + } while (!done); + + printf("\nRead %ld bytes.\n" + "Test successful.\n", callback_data.bytes_downloaded); + +cleanup: + if (ctx != 0) { + if (handle != -1) + webvi_delete_handle(ctx, handle); + webvi_cleanup_context(ctx); + } + webvi_cleanup(1); + + return returncode; +} diff --git a/src/unittest/testlibwebvi.c b/src/unittest/testlibwebvi.c new file mode 100644 index 0000000..0dda58a --- /dev/null +++ b/src/unittest/testlibwebvi.c @@ -0,0 +1,147 @@ +/* + * testlibwebvi.c: unittest for webvi C bindings + * + * Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> + * + * 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/>. + */ + +#include <stdio.h> +#include <sys/select.h> +#include <errno.h> + +#include "libwebvi.h" + +#define WVTREFERENCE "wvt:///?srcurl=mainmenu" +//#define WVTREFERENCE "wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat" +//#define WVTREFERENCE "wvt:///youtube/search.xsl" + +#define CHECK_WEBVI_CALL(err, funcname) \ + if (err != WEBVIERR_OK) { \ + fprintf(stderr, "%s FAILED: %s\n", funcname, webvi_strerror(ctx, err)); \ + returncode = 127; \ + goto cleanup; \ + } + +ssize_t count_bytes_callback(const char *buf, size_t len, void *data) { + long *bytes = (long *)data; + *bytes += len; + return len; +} + +int main(int argc, const char* argv[]) { + int returncode = 0; + WebviCtx ctx = 0; + WebviHandle handle = -1; + long bytes = 0; + fd_set readfd, writefd, excfd; + int maxfd, fd, s, msg_remaining; + struct timeval timeout; + long running; + WebviMsg *donemsg; + int done; + char *contenttype; + + printf("Testing %s\n", webvi_version()); + + if (webvi_global_init() != 0) { + fprintf(stderr, "webvi_global_init FAILED\n"); + return 127; + } + + ctx = webvi_initialize_context(); + if (ctx == 0) { + fprintf(stderr, "webvi_initialize_context FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_config(ctx, WEBVI_CONFIG_TEMPLATE_PATH, "../../templates"), + "webvi_set_config(WEBVI_CONFIG_TEMPLATE_PATH)"); + + handle = webvi_new_request(ctx, WVTREFERENCE, WEBVIREQ_MENU); + if (handle == -1) { + fprintf(stderr, "webvi_new_request FAILED\n"); + returncode = 127; + goto cleanup; + } + + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEDATA, &bytes), + "webvi_set_opt(WEBVIOPT_WRITEDATA)"); + CHECK_WEBVI_CALL(webvi_set_opt(ctx, handle, WEBVIOPT_WRITEFUNC, count_bytes_callback), + "webvi_set_opt(WEBVIOPT_WRITEFUNC)"); + CHECK_WEBVI_CALL(webvi_start_handle(ctx, handle), + "webvi_start_handle"); + + done = 0; + do { + FD_ZERO(&readfd); + FD_ZERO(&writefd); + FD_ZERO(&excfd); + CHECK_WEBVI_CALL(webvi_fdset(ctx, &readfd, &writefd, &excfd, &maxfd), + "webvi_fdset"); + + timeout.tv_sec = 10; + timeout.tv_usec = 0; + s = select(maxfd+1, &readfd, &writefd, NULL, &timeout); + + if (s < 0) { + if (errno == EINTR) + continue; + + perror("select FAILED"); + returncode = 127; + goto cleanup; + + } if (s == 0) { + CHECK_WEBVI_CALL(webvi_perform(ctx, 0, WEBVI_SELECT_TIMEOUT, &running), + "webvi_perform"); + } else { + for (fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running), + "webvi_perform"); + } + if (FD_ISSET(fd, &writefd)) { + CHECK_WEBVI_CALL(webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running), + "webvi_perform"); + } + } + } + + do { + donemsg = webvi_get_message(ctx, &msg_remaining); + if (donemsg && donemsg->msg == WEBVIMSG_DONE && donemsg->handle == handle) { + done = 1; + } + } while (msg_remaining > 0); + } while (!done); + + CHECK_WEBVI_CALL(webvi_get_info(ctx, handle, WEBVIINFO_CONTENT_TYPE, &contenttype), + "webvi_get_info"); + printf("Read %ld bytes. Content type: %s\n", bytes, contenttype); + free(contenttype); + + printf("Test successful.\n"); + +cleanup: + if (ctx != 0) { + if (handle != -1) + webvi_delete_handle(ctx, handle); + webvi_cleanup_context(ctx); + } + webvi_cleanup(1); + + return returncode; +} diff --git a/src/unittest/testwebvi.py b/src/unittest/testwebvi.py new file mode 100644 index 0000000..6017ded --- /dev/null +++ b/src/unittest/testwebvi.py @@ -0,0 +1,407 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of vdr-webvideo-plugin. +# +# Copyright 2009,2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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/>. + +"""Blackbox tests for each of thee supported video sites and webvicli. + +Mainly useful for checking if the web sites have changed so much that +the XSLT templates don't match to them anymore. Requires network +connection because the tests automatically connect and navigate +through links on the video sites. +""" + +import unittest +import sys +import re + +sys.path.append('../webvicli') +sys.path.append('../libwebvi') +from webvicli import client, menu +import webvi.api +from webvi.constants import WebviConfig + +class TestServiceModules(unittest.TestCase): + + # ========== Helper functions ========== + + def setUp(self): + webvi.api.set_config(WebviConfig.TEMPLATE_PATH, '../../templates') + self.client = client.WVClient([], {}, {}) + + def getLinks(self, menuobj): + links = [] + for i in xrange(len(menuobj)): + if isinstance(menuobj[i], menu.MenuItemLink): + links.append(menuobj[i]) + return links + + def downloadMenuPage(self, reference, menuname): + (status, statusmsg, menuobj) = self.client.getmenu(reference) + self.assertEqual(status, 0, 'Unexpected status code %s (%s) in %s menu\nFailed ref was %s' % (status, statusmsg, menuname, reference)) + self.assertNotEqual(menuobj, None, 'Failed to get %s menu' % menuname) + return menuobj + + def downloadAndExtractLinks(self, reference, minlinks, menuname): + menuobj = self.downloadMenuPage(reference, menuname) + links = self.getLinks(menuobj) + self.assertTrue(len(links) >= minlinks, 'Too few links in %s menu' % menuname) + return links + + def checkMediaUrl(self, reference): + streamurl = self.client.get_stream_url(reference) + self.assertNotEqual(streamurl, None, 'get_stream_url returned None') + self.assertNotEqual(streamurl, '', 'get_stream_url returned empty string') + + def getServiceReference(self, templatedir): + service = open(templatedir + '/service.xml').read() + m = re.search(r'<ref>(.*)</ref>', service) + self.assertNotEqual(m, None, 'no <ref> in service.xml') + return m.group(1) + + # ========== Tests for supported websites ========== + + def testMainMenu(self): + self.downloadAndExtractLinks('wvt:///?srcurl=mainmenu', 4, 'main') + + def testYoutube(self): + # Category page + ref = self.getServiceReference('../../templates/youtube') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # Navigation page + # The third one is the first "proper" category. The first and second are "Search" and "All" + navigationref = links[2].ref + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + def testYoutubeSearch(self): + menuobj = self.downloadMenuPage('wvt:///youtube/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 4) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'youtube' + # Sort by: rating + menuobj[1].current = 3 + # Uploaded: This month + menuobj[2].current = 3 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testGoogleSearch(self): + ref = self.getServiceReference('../../templates/google') + menuobj = self.downloadMenuPage(ref, 'search') + self.assertTrue(len(menuobj) == 4, 'Unexpected number of items in Google search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 4) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'google' + # Sort by: date + menuobj[1].current = 3 + # Duration: Short + menuobj[2].current = 1 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testSVTPlay(self): + # Category page + ref = self.getServiceReference('../../templates/svtplay') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # Navigation page + navigationref = links[0].ref + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Single program + programref = links[0].ref + links = self.downloadAndExtractLinks(programref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + def testMetacafe(self): + # Category page + ref = self.getServiceReference('../../templates/metacafe') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # The first is "Search", the second is "Channels" and the + # third is the first "proper" navigation. + channelsref = links[1].ref + navigationref = links[2].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + # User channels + links = self.downloadAndExtractLinks(channelsref, 3, 'channel list') + + def testMetacafeSearch(self): + menuobj = self.downloadMenuPage('wvt:///metacafe/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 4, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) == 3) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) == 4) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'metacafe' + # Sort by: most discussed + menuobj[1].current = 2 + # Published: Anytime + menuobj[2].current = 2 + + resultref = menuobj[3].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testVimeo(self): + # Category page + ref = self.getServiceReference('../../templates/vimeo') + links = self.downloadAndExtractLinks(ref, 3, 'Vimeo main page') + + # The first is "Search", the second is "Channels" and the + # third is "Groups" + channelsref = links[1].ref + groupsref = links[2].ref + + # Channels page + links = self.downloadAndExtractLinks(channelsref, 2, 'channels') + + # Navigation page + links = self.downloadAndExtractLinks(links[0].ref, 2, 'channels navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + self.checkMediaUrl(videolink.stream) + + # User groups + links = self.downloadAndExtractLinks(groupsref, 2, 'channel list') + + # Navigation page + links = self.downloadAndExtractLinks(links[0].ref, 2, 'groups navigation') + + def testVimeoSearch(self): + menuobj = self.downloadMenuPage('wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/', 'search') + self.assertTrue(len(menuobj) >= 3, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 2) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'vimeo' + # Sort by: newest + menuobj[1].current = 1 + + resultref = menuobj[2].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testYLEAreena(self): + # Category page + ref = self.getServiceReference('../../templates/yleareena') + links = self.downloadAndExtractLinks(ref, 3, 'category') + + # The first is "Search", the second is "live", the third is + # "all", the rest are navigation links. + liveref = links[1].ref + navigationref = links[3].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 2, 'navigation') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + # live broadcasts + links = self.downloadAndExtractLinks(liveref, 2, 'live broadcasts') + + def testYLEAreenaSearch(self): + menuobj = self.downloadMenuPage('wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku', 'search') + self.assertTrue(len(menuobj) >= 8, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemList)) + self.assertTrue(len(menuobj[1].items) >= 3) + self.assertTrue(isinstance(menuobj[2], menu.MenuItemList)) + self.assertTrue(len(menuobj[2].items) >= 2) + self.assertTrue(isinstance(menuobj[3], menu.MenuItemList)) + self.assertTrue(len(menuobj[3].items) >= 2) + self.assertTrue(isinstance(menuobj[4], menu.MenuItemList)) + self.assertTrue(len(menuobj[4].items) >= 3) + self.assertTrue(isinstance(menuobj[5], menu.MenuItemList)) + self.assertTrue(len(menuobj[5].items) >= 4) + self.assertTrue(isinstance(menuobj[6], menu.MenuItemList)) + self.assertTrue(len(menuobj[6].items) >= 2) + self.assertTrue(isinstance(menuobj[7], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'yle' + # Media: video + menuobj[1].current = 1 + # Category: all + menuobj[2].current = 0 + # Channel: all + menuobj[3].current = 0 + # Language: Finnish + menuobj[4].current = 1 + # Uploaded: all + menuobj[5].current = 0 + # Only outside Finland: no + menuobj[6].current = 0 + + resultref = menuobj[7].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testKatsomo(self): + # Category page + ref = self.getServiceReference('../../templates/katsomo') + links = self.downloadAndExtractLinks(ref, 2, 'category') + + # The first is "Search", the rest are navigation links. + navigationref = links[1].ref + + # Navigation page + links = self.downloadAndExtractLinks(navigationref, 1, 'navigation') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + # The first few links may be navigation links, but there + # should be video links after them. + foundVideo = False + for link in links: + if link.stream is not None: + foundVideo = True + + self.assertTrue(link, 'No a video links in the program page') + + def testKatsomoSearch(self): + menuobj = self.downloadMenuPage('wvt:///katsomo/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'mtv3' + + resultref = menuobj[1].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testRuutuFi(self): + # Category page + ref = self.getServiceReference('../../templates/ruutufi') + links = self.downloadAndExtractLinks(ref, 4, 'category') + + # The first is "Search", the second is "Series" + seriesref = links[1].ref + + # Series page + links = self.downloadAndExtractLinks(seriesref, 1, 'series') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + def testRuutuFiSearch(self): + menuobj = self.downloadMenuPage('wvt:///ruutufi/search.xsl', 'search') + self.assertTrue(len(menuobj) >= 2, 'Too few items in search menu') + + self.assertTrue(isinstance(menuobj[0], menu.MenuItemTextField)) + self.assertTrue(isinstance(menuobj[1], menu.MenuItemSubmitButton)) + + # Query term + menuobj[0].value = 'nelonen' + + resultref = menuobj[1].activate() + self.assertNotEqual(resultref, None) + self.downloadAndExtractLinks(resultref, 1, 'search result') + + def testSubtv(self): + # Category page + ref = self.getServiceReference('../../templates/subtv') + links = self.downloadAndExtractLinks(ref, 4, 'series') + + # Program page + links = self.downloadAndExtractLinks(links[0].ref, 1, 'program') + + # Video link + videolink = links[0] + self.assertNotEqual(videolink.stream, None, 'No media object in a video link') + self.assertNotEqual(videolink.ref, None, 'No description page in a video link') + + +if __name__ == '__main__': + testnames = sys.argv[1:] + + if testnames == []: + # Run all tests + unittest.main() + else: + # Run test listed on the command line + for test in testnames: + suite = unittest.TestSuite() + suite.addTest(TestServiceModules(test)) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/src/vdr-plugin/Makefile b/src/vdr-plugin/Makefile new file mode 100644 index 0000000..ccc5641 --- /dev/null +++ b/src/vdr-plugin/Makefile @@ -0,0 +1,115 @@ +# +# Makefile for a Video Disk Recorder plugin +# +# $Id$ + +# The official name of this plugin. +# This name will be used in the '-P...' option of VDR to load the plugin. +# By default the main source file also carries this name. +# IMPORTANT: the presence of this macro is important for the Make.config +# file. So it must be defined, even if it is not used here! +# +PLUGIN = webvideo + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$5 }' | sed -e 's/[";]//g') + +### The C++ compiler and options: + +CXX ?= g++ +CXXFLAGS ?= -fPIC -g -O2 -Wall -Woverloaded-virtual -Wno-parentheses + +### The directory environment: + +VDRDIR = ../../../../.. +LIBDIR = ../../../../lib +TMPDIR = /tmp + +### Libraries + +LIBS = `xml2-config --libs` -L../libwebvi -lwebvi + +### Allow user defined options to overwrite defaults: + +-include $(VDRDIR)/Make.config + +### The version number of VDR's plugin API (taken from VDR's "config.h"): + +APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### Includes and Defines (add further entries here): + +LIBWEBVIINCPATH = ../libwebvi +INCLUDES += -I$(VDRDIR)/include $(LIBWEBVIINCLUDES) -I$(LIBWEBVIINCPATH) `xml2-config --cflags` + +DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' + +### The object files (add further files here): + +OBJS = $(PLUGIN).o buffer.o common.o config.o download.o history.o menu.o menudata.o mimetypes.o request.o player.o dictionary.o iniparser.o timer.o menu_timer.o + +### The main target: + +all: libvdr-$(PLUGIN).so i18n + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) $< + +### Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +LOCALEDIR = $(VDRDIR)/locale +I18Npo = $(wildcard $(PODIR)/*.po) +I18Nmsgs = $(addprefix $(LOCALEDIR)/, $(addsuffix /LC_MESSAGES/vdr-$(PLUGIN).mo, $(notdir $(foreach file, $(I18Npo), $(basename $(file)))))) +I18Npot = $(PODIR)/$(PLUGIN).pot + +%.mo: %.po + msgfmt -c -o $@ $< + +$(I18Npot): $(wildcard *.c) + xgettext -C -cTRANSLATORS --no-wrap --no-location -k -ktr -ktrNOOP --msgid-bugs-address='<see README>' -o $@ $^ + +%.po: $(I18Npot) + msgmerge -U --no-wrap --no-location --backup=none -q $@ $< + @touch $@ + +$(I18Nmsgs): $(LOCALEDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo + @mkdir -p $(dir $@) + cp $< $@ + +.PHONY: i18n +i18n: $(I18Nmsgs) $(I18Npot) + +### Targets: + +libvdr-$(PLUGIN).so: $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LIBS) -o $@ + cp --remove-destination $@ $(LIBDIR)/$@.$(APIVERSION) + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(OBJS) $(DEPFILE) *.so *.so.* *.tgz core* *~ $(PODIR)/*.mo $(PODIR)/*.pot diff --git a/src/vdr-plugin/buffer.c b/src/vdr-plugin/buffer.c new file mode 100644 index 0000000..41b2c38 --- /dev/null +++ b/src/vdr-plugin/buffer.c @@ -0,0 +1,84 @@ +/* + * buffer.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <vdr/tools.h> +#include "buffer.h" + +// --- cMemoryBuffer ------------------------------------------------------- + +cMemoryBuffer::cMemoryBuffer(size_t prealloc) { + capacity = prealloc; + buf = (char *)malloc(capacity*sizeof(char)); + offset = 0; + len = 0; +} + +cMemoryBuffer::~cMemoryBuffer() { + if (buf) + free(buf); +} + +void cMemoryBuffer::Realloc(size_t newsize) { + if (newsize > capacity-offset) { + if (newsize <= capacity) { + // The new buffer fits in the memory if we just move the current + // content offset bytes backwards. + buf = (char *)memmove(buf, &buf[offset], len); + offset = 0; + } else { + // We need to realloc. Move the content to the beginning of the + // buffer while we are at it. + capacity += min(capacity, (size_t)10*1024); + capacity = max(capacity, newsize); + char *newbuf = (char *)malloc(capacity*sizeof(char)); + if (newbuf) { + memcpy(newbuf, &buf[offset], len); + offset = 0; + free(buf); + buf = newbuf; + } + } + } +} + +ssize_t cMemoryBuffer::Put(const char *data, size_t bytes) { + if (len+bytes > Free()) { + Realloc(len+bytes); + } + + if (buf) { + memcpy(&buf[offset+len], data, bytes); + len += bytes; + return bytes; + } + return -1; +} + +ssize_t cMemoryBuffer::PutFromFile(int fd, size_t bytes) { + if (len+bytes > Free()) { + Realloc(len+bytes); + } + + if (buf) { + ssize_t r = safe_read(fd, &buf[offset+len], bytes); + if (r > 0) + len += r; + return r; + } else + return -1; +} + +void cMemoryBuffer::Pop(size_t bytes) { + if (bytes <= len) { + offset += bytes; + len -= bytes; + } +} diff --git a/src/vdr-plugin/buffer.h b/src/vdr-plugin/buffer.h new file mode 100644 index 0000000..0a5ee5c --- /dev/null +++ b/src/vdr-plugin/buffer.h @@ -0,0 +1,44 @@ +/* + * buffer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_BUFFER_H +#define __WEBVIDEO_BUFFER_H + +#include <unistd.h> + +// --- cMemoryBuffer ------------------------------------------------------- + +// FIFO character buffer. + +class cMemoryBuffer { +private: + char *buf; + size_t offset; + size_t len; + size_t capacity; +protected: + size_t Free() { return capacity-len-offset; } + virtual void Realloc(size_t newsize); +public: + cMemoryBuffer(size_t prealloc = 10*1024); + virtual ~cMemoryBuffer(); + + // Put data into the end of the buffer + virtual ssize_t Put(const char *data, size_t length); + // Put data from a file descriptor fd to the buffer + virtual ssize_t PutFromFile(int fd, size_t length); + // The pointer to the beginning of the buffer. Only valid until the + // next Put() or PutFromFile(). + virtual char *Get() { return &buf[offset]; } + // Remove first n bytes from the buffer. + void Pop(size_t n); + // Returns the current length of the buffer + virtual size_t Length() { return len; } +}; + +#endif // __WEBVIDEO_BUFFER_H diff --git a/src/vdr-plugin/common.c b/src/vdr-plugin/common.c new file mode 100644 index 0000000..0731da9 --- /dev/null +++ b/src/vdr-plugin/common.c @@ -0,0 +1,182 @@ +/* + * common.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <unistd.h> +#include <ctype.h> +#include <vdr/tools.h> +#include "common.h" + +char *extensionFromUrl(const char *url) { + if (!url) + return NULL; + + // Find the possible query ("?query=foo") or fragment ("#bar"). The + // extension is located right before them. + size_t extendpos = strcspn(url, "?#"); + + size_t extstartpos = extendpos-1; + while ((extstartpos > 0) && (url[extstartpos] != '.') && (url[extstartpos] != '/')) + extstartpos--; + + if ((extstartpos > 0) && (url[extstartpos] == '.')) { + // We found the extension. Copy it to a buffer, and return it. + char *ext = (char *)malloc(sizeof(char)*(extendpos-extstartpos+1)); + memcpy(ext, &url[extstartpos], extendpos-extstartpos); + ext[extendpos-extstartpos] = '\0'; + + return ext; + } + + return NULL; +} + +char *validateFileName(const char *filename) { + if (!filename) + return NULL; + + char *validated = (char *)malloc(strlen(filename)+1); + int j=0; + for (unsigned int i=0; i<strlen(filename); i++) { + if (filename[i] != '/') { + validated[j++] = filename[i]; + } + } + validated[j] = '\0'; + return validated; +} + +int moveFile(const char *oldpath, const char *newpath) { + if (rename(oldpath, newpath) == 0) { + return 0; + } else if (errno == EXDEV) { + // rename can't move a file between file systems. We have to copy + // the file manually. + int fdout = open(newpath, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE); + if (fdout < 0) { + return -1; + } + + int fdin = open(oldpath, O_RDONLY); + if (fdin < 0) { + close(fdout); + return -1; + } + + const int bufsize = 4096; + char buffer[bufsize]; + bool ok = true; + while (true) { + ssize_t len = safe_read(fdin, &buffer, bufsize); + if (len == 0) { + break; + } else if (len < 0) { + ok = false; + break; + } + + if (safe_write(fdout, &buffer, len) != len) { + ok = false; + break; + } + } + + close(fdin); + close(fdout); + + if (ok && (unlink(oldpath) <0)) { + return -1; + } + + return 0; + } else { + return -1; + } +} + +char *URLencode(const char *s) { + char reserved_and_unsafe[] = + { // reserved characters + '$', '&', '+', ',', '/', ':', ';', '=', '?', '@', + // unsafe characters + ' ', '"', '<', '>', '#', '%', '{', '}', + '|', '\\', '^', '~', '[', ']', '`', + '\0' + }; + + char *buf = (char *)malloc((3*strlen(s)+1)*sizeof(char)); + if (!buf) + return NULL; + + unsigned char *out; + const unsigned char *in; + for (out=(unsigned char *)buf, in=(const unsigned char *)s; *in != '\0'; in++) { + if ((*in < 32) // control chracters + || (strchr(reserved_and_unsafe, *in)) // reserved and unsafe + || (*in > 127)) // non-ASCII + { + snprintf((char *)out, 4, "%%%02hhX", *in); + out += 3; + } else { + *out = *in; + out++; + } + } + *out = '\0'; + + return buf; +} + +char *URLdecode(const char *s) { + char *res = (char *)malloc(strlen(s)+1); + const char *in = s; + char *out = res; + const char *hex = "0123456789ABCDEF"; + const char *h1, *h2; + + while (*in) { + if ((*in == '%') && (in[1] != '\0') && (in[2] != '\0')) { + h1 = strchr(hex, toupper(in[1])); + h2 = strchr(hex, toupper(in[2])); + if (h1 && h2) { + *out = ((h1-hex) << 4) + (h2-hex); + in += 3; + } else { + *out = *in; + in++; + } + } else { + *out = *in; + in++; + } + out++; + } + *out = '\0'; + + return res; +} + +char *safeFilename(char *filename) { + if (filename) { + strreplace(filename, '/', '!'); + + char *p = filename; + while ((*p == '.') || isspace(*p)) { + p++; + } + + if (p != filename) { + memmove(filename, p, strlen(p)+1); + } + } + + return filename; +} diff --git a/src/vdr-plugin/common.h b/src/vdr-plugin/common.h new file mode 100644 index 0000000..5b4385f --- /dev/null +++ b/src/vdr-plugin/common.h @@ -0,0 +1,42 @@ +/* + * common.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_COMMON_H +#define __WEBVIDEO_COMMON_H + +#ifdef DEBUG +#define debug(x...) dsyslog("Webvideo: " x); +#define info(x...) isyslog("Webvideo: " x); +#define warning(x...) esyslog("Webvideo: Warning: " x); +#define error(x...) esyslog("Webvideo: " x); +#else +#define debug(x...) ; +#define info(x...) isyslog("Webvideo: " x); +#define warning(x...) esyslog("Webvideo: Warning: " x); +#define error(x...) esyslog("Webvideo: " x); +#endif + +// Return the extension of the url or NULL, if the url has no +// extension. The caller must free the returned string. +char *extensionFromUrl(const char *url); +// Returns a "safe" version of filename. Currently just removes / from +// the name. The caller must free the returned string. +char *validateFileName(const char *filename); +int moveFile(const char *oldpath, const char *newpath); +// Return the URL encoded version of s. The called must free the +// returned memory. +char *URLencode(const char *s); +// Remove URL encoding from s. The called must free the returned +// memory. +char *URLdecode(const char *s); +// Return a "safe" version of filename. Remove path (replace '/' with +// '!') and dots from the beginning. The string is modified in-place, +// i.e. returns the pointer filename that was passed as argument. +char *safeFilename(char *filename); + +#endif // __WEBVIDEO_COMMON_H diff --git a/src/vdr-plugin/config.c b/src/vdr-plugin/config.c new file mode 100644 index 0000000..f294e60 --- /dev/null +++ b/src/vdr-plugin/config.c @@ -0,0 +1,199 @@ +/* + * config.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <string.h> +#include "config.h" +#include "dictionary.h" +#include "iniparser.h" +#include "common.h" + +// --- cDownloadQuality --------------------------------------------------- + +cDownloadQuality::cDownloadQuality(const char *sitename) +: min(NULL), max(NULL) { + site = sitename ? strdup(sitename) : NULL; +} + +cDownloadQuality::~cDownloadQuality() { + if (site) + free(site); + if (min) + free(min); + if (max) + free(max); +} + +void cDownloadQuality::SetMin(const char *val) { + if (min) + free(min); + + min = val ? strdup(val) : NULL; +} + +void cDownloadQuality::SetMax(const char *val) { + if (max) + free(max); + + max = val ? strdup(val) : NULL; +} + +const char *cDownloadQuality::GetSite() { + return site; +} + +const char *cDownloadQuality::GetMin() { + return min; +} + +const char *cDownloadQuality::GetMax() { + return max; +} + +// --- cWebvideoConfig ----------------------------------------------------- + +cWebvideoConfig *webvideoConfig = new cWebvideoConfig(); + +cWebvideoConfig::cWebvideoConfig() { + downloadPath = NULL; + templatePath = NULL; + preferXine = true; +} + +cWebvideoConfig::~cWebvideoConfig() { + if (downloadPath) + free(downloadPath); + if (templatePath) + free(templatePath); +} + +void cWebvideoConfig::SetDownloadPath(const char *path) { + if (downloadPath) + free(downloadPath); + downloadPath = path ? strdup(path) : NULL; +} + +const char *cWebvideoConfig::GetDownloadPath() { + return downloadPath; +} + +void cWebvideoConfig::SetTemplatePath(const char *path) { + if (templatePath) + free(templatePath); + templatePath = path ? strdup(path) : NULL; +} + +const char *cWebvideoConfig::GetTemplatePath() { + return templatePath; +} + +void cWebvideoConfig::SetPreferXineliboutput(bool pref) { + preferXine = pref; +} + +bool cWebvideoConfig::GetPreferXineliboutput() { + return preferXine; +} + +bool cWebvideoConfig::ReadConfigFile(const char *inifile) { + dictionary *conf = iniparser_load(inifile); + + if (!conf) + return false; + + info("loading config file %s", inifile); + + const char *templatepath = iniparser_getstring(conf, "webvi:templatepath", NULL); + if (templatepath) { + debug("templatepath = %s (from %s)", templatepath, inifile); + SetTemplatePath(templatepath); + } + + for (int i=0; i<iniparser_getnsec(conf); i++) { + const char *section = iniparser_getsecname(conf, i); + + if (strncmp(section, "site-", 5) == 0) { + const char *sitename = section+5; + const int maxsectionlen = 40; + char key[64]; + char *keyname; + + strncpy(key, section, maxsectionlen); + key[maxsectionlen] = '\0'; + strcat(key, ":"); + keyname = key+strlen(key); + + strcpy(keyname, "download-min-quality"); + const char *download_min = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "download-max-quality"); + const char *download_max = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "stream-min-quality"); + const char *stream_min = iniparser_getstring(conf, key, NULL); + + strcpy(keyname, "stream-max-quality"); + const char *stream_max = iniparser_getstring(conf, key, NULL); + + if (download_min || download_max) { + cDownloadQuality *limits = new cDownloadQuality(sitename); + limits->SetMin(download_min); + limits->SetMax(download_max); + downloadLimits.Add(limits); + + debug("download priorities for %s (from %s): min = %s, max = %s", + sitename, inifile, download_min, download_max); + } + + if (stream_min || stream_max) { + cDownloadQuality *limits = new cDownloadQuality(sitename); + limits->SetMin(stream_min); + limits->SetMax(stream_max); + streamLimits.Add(limits); + + debug("streaming priorities for %s (from %s): min = %s, max = %s", + sitename, inifile, stream_min, stream_max); + } + } + } + + iniparser_freedict(conf); + + return true; +} + +const char *cWebvideoConfig::GetQuality(const char *site, eRequestType type, int limit) { + if (type != REQT_FILE && type != REQT_STREAM) + return NULL; + + cList<cDownloadQuality>& priorlist = downloadLimits; + if (type == REQT_STREAM) + priorlist = streamLimits; + + cDownloadQuality *node = priorlist.First(); + + while (node && (strcmp(site, node->GetSite()) != 0)) { + node = priorlist.Next(node); + } + + if (!node) + return NULL; + + if (limit == 0) + return node->GetMin(); + else + return node->GetMax(); +} + +const char *cWebvideoConfig::GetMinQuality(const char *site, eRequestType type) { + return GetQuality(site, type, 0); +} + +const char *cWebvideoConfig::GetMaxQuality(const char *site, eRequestType type) { + return GetQuality(site, type, 1); +} diff --git a/src/vdr-plugin/config.h b/src/vdr-plugin/config.h new file mode 100644 index 0000000..29304b4 --- /dev/null +++ b/src/vdr-plugin/config.h @@ -0,0 +1,64 @@ +/* + * config.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_CONFIG_H +#define __WEBVIDEO_CONFIG_H + +#include <vdr/tools.h> +#include "request.h" + +class cDownloadQuality : public cListObject { +private: + char *site; + char *min; + char *max; + +public: + cDownloadQuality(const char *site); + ~cDownloadQuality(); + + void SetMin(const char *val); + void SetMax(const char *val); + + const char *GetSite(); + const char *GetMin(); + const char *GetMax(); +}; + +class cWebvideoConfig { +private: + char *downloadPath; + char *templatePath; + bool preferXine; + cList<cDownloadQuality> downloadLimits; + cList<cDownloadQuality> streamLimits; + + const char *GetQuality(const char *site, eRequestType type, int limit); + +public: + cWebvideoConfig(); + ~cWebvideoConfig(); + + bool ReadConfigFile(const char *inifile); + + void SetDownloadPath(const char *path); + const char *GetDownloadPath(); + + void SetTemplatePath(const char *path); + const char *GetTemplatePath(); + + void SetPreferXineliboutput(bool pref); + bool GetPreferXineliboutput(); + + const char *GetMinQuality(const char *site, eRequestType type); + const char *GetMaxQuality(const char *site, eRequestType type); +}; + +extern cWebvideoConfig *webvideoConfig; + +#endif diff --git a/src/vdr-plugin/dictionary.c b/src/vdr-plugin/dictionary.c new file mode 100644 index 0000000..4c5ae08 --- /dev/null +++ b/src/vdr-plugin/dictionary.c @@ -0,0 +1,410 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file dictionary.c + @author N. Devillard + @date Sep 2007 + @version $Revision: 1.27 $ + @brief Implements a dictionary for string variables. + + This module implements a simple dictionary object, i.e. a list + of string/string associations. This object is useful to store e.g. + informations retrieved from a configuration file (ini files). +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: dictionary.c,v 1.27 2007-11-23 21:39:18 ndevilla Exp $ + $Revision: 1.27 $ +*/ +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ +#include "dictionary.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/** Maximum value size for integers and doubles. */ +#define MAXVALSZ 1024 + +/** Minimal allocated number of entries in a dictionary */ +#define DICTMINSZ 128 + +/** Invalid key token */ +#define DICT_INVALID_KEY ((char*)-1) + +/*--------------------------------------------------------------------------- + Private functions + ---------------------------------------------------------------------------*/ + +/* Doubles the allocated size associated to a pointer */ +/* 'size' is the current allocated size. */ +static void * mem_double(void * ptr, int size) +{ + void * newptr ; + + newptr = calloc(2*size, 1); + if (newptr==NULL) { + return NULL ; + } + memcpy(newptr, ptr, size); + free(ptr); + return newptr ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Duplicate a string + @param s String to duplicate + @return Pointer to a newly allocated string, to be freed with free() + + This is a replacement for strdup(). This implementation is provided + for systems that do not have it. + */ +/*--------------------------------------------------------------------------*/ +static char * xstrdup(char * s) +{ + char * t ; + if (!s) + return NULL ; + t = (char *)malloc(strlen(s)+1) ; + if (t) { + strcpy(t,s); + } + return t ; +} + +/*--------------------------------------------------------------------------- + Function codes + ---------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +/** + @brief Compute the hash key for a string. + @param key Character string to use for key. + @return 1 unsigned int on at least 32 bits. + + This hash function has been taken from an Article in Dr Dobbs Journal. + This is normally a collision-free function, distributing keys evenly. + The key is stored anyway in the struct so that collision can be avoided + by comparing the key itself in last resort. + */ +/*--------------------------------------------------------------------------*/ +unsigned dictionary_hash(char * key) +{ + int len ; + unsigned hash ; + int i ; + + len = strlen(key); + for (hash=0, i=0 ; i<len ; i++) { + hash += (unsigned)key[i] ; + hash += (hash<<10); + hash ^= (hash>>6) ; + } + hash += (hash <<3); + hash ^= (hash >>11); + hash += (hash <<15); + return hash ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Create a new dictionary object. + @param size Optional initial size of the dictionary. + @return 1 newly allocated dictionary objet. + + This function allocates a new dictionary object of given size and returns + it. If you do not know in advance (roughly) the number of entries in the + dictionary, give size=0. + */ +/*--------------------------------------------------------------------------*/ +dictionary * dictionary_new(int size) +{ + dictionary * d ; + + /* If no size was specified, allocate space for DICTMINSZ */ + if (size<DICTMINSZ) size=DICTMINSZ ; + + if (!(d = (dictionary *)calloc(1, sizeof(dictionary)))) { + return NULL; + } + d->size = size ; + d->val = (char **)calloc(size, sizeof(char*)); + d->key = (char **)calloc(size, sizeof(char*)); + d->hash = (unsigned int *)calloc(size, sizeof(unsigned)); + return d ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a dictionary object + @param d dictionary object to deallocate. + @return void + + Deallocate a dictionary object and all memory associated to it. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_del(dictionary * d) +{ + int i ; + + if (d==NULL) return ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]!=NULL) + free(d->key[i]); + if (d->val[i]!=NULL) + free(d->val[i]); + } + free(d->val); + free(d->key); + free(d->hash); + free(d); + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get a value from a dictionary. + @param d dictionary object to search. + @param key Key to look for in the dictionary. + @param def Default value to return if key not found. + @return 1 pointer to internally allocated character string. + + This function locates a key in a dictionary and returns a pointer to its + value, or the passed 'def' pointer if no such key can be found in + dictionary. The returned character pointer points to data internal to the + dictionary object, you should not try to free it or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * dictionary_get(dictionary * d, char * key, char * def) +{ + unsigned hash ; + int i ; + + hash = dictionary_hash(key); + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + /* Compare hash */ + if (hash==d->hash[i]) { + /* Compare string, to avoid hash collisions */ + if (!strcmp(key, d->key[i])) { + return d->val[i] ; + } + } + } + return def ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Set a value in a dictionary. + @param d dictionary object to modify. + @param key Key to modify or add. + @param val Value to add. + @return int 0 if Ok, anything else otherwise + + If the given key is found in the dictionary, the associated value is + replaced by the provided one. If the key cannot be found in the + dictionary, it is added to it. + + It is Ok to provide a NULL value for val, but NULL values for the dictionary + or the key are considered as errors: the function will return immediately + in such a case. + + Notice that if you dictionary_set a variable to NULL, a call to + dictionary_get will return a NULL value: the variable will be found, and + its value (NULL) is returned. In other words, setting the variable + content to NULL is equivalent to deleting the variable from the + dictionary. It is not possible (in this implementation) to have a key in + the dictionary without value. + + This function returns non-zero in case of failure. + */ +/*--------------------------------------------------------------------------*/ +int dictionary_set(dictionary * d, char * key, char * val) +{ + int i ; + unsigned hash ; + + if (d==NULL || key==NULL) return -1 ; + + /* Compute hash for this key */ + hash = dictionary_hash(key) ; + /* Find if value is already in dictionary */ + if (d->n>0) { + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (hash==d->hash[i]) { /* Same hash value */ + if (!strcmp(key, d->key[i])) { /* Same key */ + /* Found a value: modify and return */ + if (d->val[i]!=NULL) + free(d->val[i]); + d->val[i] = val ? xstrdup(val) : NULL ; + /* Value has been modified: return */ + return 0 ; + } + } + } + } + /* Add a new value */ + /* See if dictionary needs to grow */ + if (d->n==d->size) { + + /* Reached maximum size: reallocate dictionary */ + d->val = (char **)mem_double(d->val, d->size * sizeof(char*)) ; + d->key = (char **)mem_double(d->key, d->size * sizeof(char*)) ; + d->hash = (unsigned int *)mem_double(d->hash, d->size * sizeof(unsigned)) ; + if ((d->val==NULL) || (d->key==NULL) || (d->hash==NULL)) { + /* Cannot grow dictionary */ + return -1 ; + } + /* Double size */ + d->size *= 2 ; + } + + /* Insert key in the first empty slot */ + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) { + /* Add key here */ + break ; + } + } + /* Copy key */ + d->key[i] = xstrdup(key); + d->val[i] = val ? xstrdup(val) : NULL ; + d->hash[i] = hash; + d->n ++ ; + return 0 ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a key in a dictionary + @param d dictionary object to modify. + @param key Key to remove. + @return void + + This function deletes a key in a dictionary. Nothing is done if the + key cannot be found. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_unset(dictionary * d, char * key) +{ + unsigned hash ; + int i ; + + if (key == NULL) { + return; + } + + hash = dictionary_hash(key); + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + /* Compare hash */ + if (hash==d->hash[i]) { + /* Compare string, to avoid hash collisions */ + if (!strcmp(key, d->key[i])) { + /* Found key */ + break ; + } + } + } + if (i>=d->size) + /* Key not found */ + return ; + + free(d->key[i]); + d->key[i] = NULL ; + if (d->val[i]!=NULL) { + free(d->val[i]); + d->val[i] = NULL ; + } + d->hash[i] = 0 ; + d->n -- ; + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump + @param f Opened file pointer. + @return void + + Dumps a dictionary onto an opened file pointer. Key pairs are printed out + as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as + output file pointers. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_dump(dictionary * d, FILE * out) +{ + int i ; + + if (d==NULL || out==NULL) return ; + if (d->n<1) { + fprintf(out, "empty dictionary\n"); + return ; + } + for (i=0 ; i<d->size ; i++) { + if (d->key[i]) { + fprintf(out, "%20s\t[%s]\n", + d->key[i], + d->val[i] ? d->val[i] : "UNDEF"); + } + } + return ; +} + + +/* Test code */ +#ifdef TESTDIC +#define NVALS 20000 +int main(int argc, char *argv[]) +{ + dictionary * d ; + char * val ; + int i ; + char cval[90] ; + + /* Allocate dictionary */ + printf("allocating...\n"); + d = dictionary_new(0); + + /* Set values in dictionary */ + printf("setting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + dictionary_set(d, cval, "salut"); + } + printf("getting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + val = dictionary_get(d, cval, DICT_INVALID_KEY); + if (val==DICT_INVALID_KEY) { + printf("cannot get value for key [%s]\n", cval); + } + } + printf("unsetting %d values...\n", NVALS); + for (i=0 ; i<NVALS ; i++) { + sprintf(cval, "%04d", i); + dictionary_unset(d, cval); + } + if (d->n != 0) { + printf("error deleting values\n"); + } + printf("deallocating...\n"); + dictionary_del(d); + return 0 ; +} +#endif +/* vim: set ts=4 et sw=4 tw=75 */ diff --git a/src/vdr-plugin/dictionary.h b/src/vdr-plugin/dictionary.h new file mode 100644 index 0000000..f39493e --- /dev/null +++ b/src/vdr-plugin/dictionary.h @@ -0,0 +1,178 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file dictionary.h + @author N. Devillard + @date Sep 2007 + @version $Revision: 1.12 $ + @brief Implements a dictionary for string variables. + + This module implements a simple dictionary object, i.e. a list + of string/string associations. This object is useful to store e.g. + informations retrieved from a configuration file (ini files). +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: dictionary.h,v 1.12 2007-11-23 21:37:00 ndevilla Exp $ + $Author: ndevilla $ + $Date: 2007-11-23 21:37:00 $ + $Revision: 1.12 $ +*/ + +#ifndef _DICTIONARY_H_ +#define _DICTIONARY_H_ + +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/*--------------------------------------------------------------------------- + New types + ---------------------------------------------------------------------------*/ + + +/*-------------------------------------------------------------------------*/ +/** + @brief Dictionary object + + This object contains a list of string/string associations. Each + association is identified by a unique string key. Looking up values + in the dictionary is speeded up by the use of a (hopefully collision-free) + hash function. + */ +/*-------------------------------------------------------------------------*/ +typedef struct _dictionary_ { + int n ; /** Number of entries in dictionary */ + int size ; /** Storage size */ + char ** val ; /** List of string values */ + char ** key ; /** List of string keys */ + unsigned * hash ; /** List of hash values for keys */ +} dictionary ; + + +/*--------------------------------------------------------------------------- + Function prototypes + ---------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------*/ +/** + @brief Compute the hash key for a string. + @param key Character string to use for key. + @return 1 unsigned int on at least 32 bits. + + This hash function has been taken from an Article in Dr Dobbs Journal. + This is normally a collision-free function, distributing keys evenly. + The key is stored anyway in the struct so that collision can be avoided + by comparing the key itself in last resort. + */ +/*--------------------------------------------------------------------------*/ +unsigned dictionary_hash(char * key); + +/*-------------------------------------------------------------------------*/ +/** + @brief Create a new dictionary object. + @param size Optional initial size of the dictionary. + @return 1 newly allocated dictionary objet. + + This function allocates a new dictionary object of given size and returns + it. If you do not know in advance (roughly) the number of entries in the + dictionary, give size=0. + */ +/*--------------------------------------------------------------------------*/ +dictionary * dictionary_new(int size); + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a dictionary object + @param d dictionary object to deallocate. + @return void + + Deallocate a dictionary object and all memory associated to it. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_del(dictionary * vd); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get a value from a dictionary. + @param d dictionary object to search. + @param key Key to look for in the dictionary. + @param def Default value to return if key not found. + @return 1 pointer to internally allocated character string. + + This function locates a key in a dictionary and returns a pointer to its + value, or the passed 'def' pointer if no such key can be found in + dictionary. The returned character pointer points to data internal to the + dictionary object, you should not try to free it or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * dictionary_get(dictionary * d, char * key, char * def); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Set a value in a dictionary. + @param d dictionary object to modify. + @param key Key to modify or add. + @param val Value to add. + @return int 0 if Ok, anything else otherwise + + If the given key is found in the dictionary, the associated value is + replaced by the provided one. If the key cannot be found in the + dictionary, it is added to it. + + It is Ok to provide a NULL value for val, but NULL values for the dictionary + or the key are considered as errors: the function will return immediately + in such a case. + + Notice that if you dictionary_set a variable to NULL, a call to + dictionary_get will return a NULL value: the variable will be found, and + its value (NULL) is returned. In other words, setting the variable + content to NULL is equivalent to deleting the variable from the + dictionary. It is not possible (in this implementation) to have a key in + the dictionary without value. + + This function returns non-zero in case of failure. + */ +/*--------------------------------------------------------------------------*/ +int dictionary_set(dictionary * vd, char * key, char * val); + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete a key in a dictionary + @param d dictionary object to modify. + @param key Key to remove. + @return void + + This function deletes a key in a dictionary. Nothing is done if the + key cannot be found. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_unset(dictionary * d, char * key); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump + @param f Opened file pointer. + @return void + + Dumps a dictionary onto an opened file pointer. Key pairs are printed out + as @c [Key]=[Value], one per line. It is Ok to provide stdout or stderr as + output file pointers. + */ +/*--------------------------------------------------------------------------*/ +void dictionary_dump(dictionary * d, FILE * out); + +#endif diff --git a/src/vdr-plugin/download.c b/src/vdr-plugin/download.c new file mode 100644 index 0000000..f9d956f --- /dev/null +++ b/src/vdr-plugin/download.c @@ -0,0 +1,222 @@ +/* + * download.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <errno.h> +#include <sys/select.h> +#include <unistd.h> +#include <fcntl.h> +#include <vdr/tools.h> +#include "download.h" +#include "common.h" + +// --- cWebviThread -------------------------------------------------------- + +cWebviThread::cWebviThread() { + int pipefd[2]; + + if (pipe(pipefd) == -1) + LOG_ERROR_STR("new request pipe"); + newreqread = pipefd[0]; + newreqwrite = pipefd[1]; + //fcntl(newreqread, F_SETFL, O_NONBLOCK); + //fcntl(newreqwrite, F_SETFL, O_NONBLOCK); + + webvi = webvi_initialize_context(); +} + +cWebviThread::~cWebviThread() { + int numactive = activeRequestList.Size(); + for (int i=0; i<activeRequestList.Size(); i++) + delete activeRequestList[i]; + activeRequestList.Clear(); + + for (int i=0; i<finishedRequestList.Size(); i++) { + delete finishedRequestList[i]; + } + finishedRequestList.Clear(); + + webvi_cleanup_context(webvi); + + if (numactive > 0) { + esyslog("%d requests failed to complete", numactive); + } +} + +cWebviThread &cWebviThread::Instance() { + static cWebviThread instance; + + return instance; +} + +void cWebviThread::SetTemplatePath(const char *path) { + if (webvi != 0 && path) + webvi_set_config(webvi, WEBVI_CONFIG_TEMPLATE_PATH, path); +} + +void cWebviThread::MoveToFinishedList(cMenuRequest *req) { + // Move the request from the activeList to finishedList. + requestMutex.Lock(); + for (int i=0; i<activeRequestList.Size(); i++) { + if (activeRequestList[i] == req) { + activeRequestList.Remove(i); + break; + } + } + finishedRequestList.Append(req); + + requestMutex.Unlock(); +} + +void cWebviThread::ActivateNewRequest() { + // Move requests from newRequestList to activeRequestList and start + // them. + requestMutex.Lock(); + for (int i=0; i<newRequestList.Size(); i++) { + cMenuRequest *req = newRequestList[i]; + if (req->IsAborted()) { + // The request has been aborted even before we got a chance to + // start it. + MoveToFinishedList(req); + } else { + debug("starting request %d", req->GetID()); + + if (!req->Start(webvi)) { + error("Request failed to start"); + req->RequestDone(-1, "Request failed to start"); + MoveToFinishedList(req); + } else { + activeRequestList.Append(req); + } + } + } + + newRequestList.Clear(); + requestMutex.Unlock(); +} + +void cWebviThread::StopFinishedRequests() { + // Check if some requests have finished, and move them to + // finishedRequestList. + int msg_remaining; + WebviMsg *donemsg; + cMenuRequest *req; + + do { + donemsg = webvi_get_message(webvi, &msg_remaining); + + if (donemsg && donemsg->msg == WEBVIMSG_DONE) { + requestMutex.Lock(); + req = activeRequestList.FindByHandle(donemsg->handle); + if (req) { + debug("Finished request %d", req->GetID()); + req->RequestDone(donemsg->status_code, donemsg->data); + MoveToFinishedList(req); + } + requestMutex.Unlock(); + } + } while (msg_remaining > 0); +} + +void cWebviThread::Stop() { + // The thread may be sleeping, wake it up first. + TEMP_FAILURE_RETRY(write(newreqwrite, "S", 1)); + Cancel(5); +} + +void cWebviThread::Action(void) { + fd_set readfds, writefds, excfds; + int maxfd; + struct timeval timeout; + long running_handles; + bool check_done = false; + + if (webvi == 0) { + error("Failed to get libwebvi context"); + return; + } + + while (Running()) { + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&excfds); + webvi_fdset(webvi, &readfds, &writefds, &excfds, &maxfd); + FD_SET(newreqread, &readfds); + if (newreqread > maxfd) + maxfd = newreqread; + + timeout.tv_sec = 5; + timeout.tv_usec = 0; + + int s = TEMP_FAILURE_RETRY(select(maxfd+1, &readfds, &writefds, NULL, + &timeout)); + if (s == -1) { + // select error + LOG_ERROR_STR("select() error in webvideo downloader thread:"); + Cancel(-1); + + } else if (s == 0) { + // timeout + webvi_perform(webvi, 0, WEBVI_SELECT_TIMEOUT, &running_handles); + check_done = true; + + } else { + for (int fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfds)) { + if (fd == newreqread) { + char tmpbuf[8]; + int n = read(fd, tmpbuf, 8); + if (n > 0 && memchr(tmpbuf, 'S', n)) + Cancel(-1); + ActivateNewRequest(); + } else { + webvi_perform(webvi, fd, WEBVI_SELECT_READ, &running_handles); + check_done = true; + } + } + if (FD_ISSET(fd, &writefds)) + webvi_perform(webvi, fd, WEBVI_SELECT_WRITE, &running_handles); + if (FD_ISSET(fd, &excfds)) + webvi_perform(webvi, fd, WEBVI_SELECT_EXCEPTION, &running_handles); + } + } + + if (check_done) { + StopFinishedRequests(); + check_done = false; + } + } +} + +void cWebviThread::AddRequest(cMenuRequest *req) { + requestMutex.Lock(); + newRequestList.Append(req); + requestMutex.Unlock(); + + int s = TEMP_FAILURE_RETRY(write(newreqwrite, "*", 1)); + if (s == -1) + LOG_ERROR_STR("Failed to signal new webvideo request"); +} + +cMenuRequest *cWebviThread::GetFinishedRequest() { + cMenuRequest *res = NULL; + requestMutex.Lock(); + if (finishedRequestList.Size() > 0) { + res = finishedRequestList[finishedRequestList.Size()-1]; + finishedRequestList.Remove(finishedRequestList.Size()-1); + } + requestMutex.Unlock(); + + return res; +} + +int cWebviThread::GetUnfinishedCount() { + if (!Running()) + return 0; + else + return activeRequestList.Size(); +} diff --git a/src/vdr-plugin/download.h b/src/vdr-plugin/download.h new file mode 100644 index 0000000..5f29150 --- /dev/null +++ b/src/vdr-plugin/download.h @@ -0,0 +1,59 @@ +/* + * download.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_DOWNLOAD_H +#define __WEBVIDEO_DOWNLOAD_H + +#include <vdr/thread.h> +#include <libwebvi.h> +#include "request.h" + +// --- cWebviThread -------------------------------------------------------- + +class cWebviThread : public cThread { +private: + WebviCtx webvi; + cMutex requestMutex; + cRequestVector activeRequestList; + cRequestVector newRequestList; + cRequestVector finishedRequestList; + int newreqread, newreqwrite; + + void MoveToFinishedList(cMenuRequest *req); + void ActivateNewRequest(); + void StopFinishedRequests(); + +protected: + void Action(void); + +public: + cWebviThread(); + ~cWebviThread(); + + static cWebviThread &Instance(); + + // Stop the thread + void Stop(); + // Set path to the site templates. Should be set before + // Start()ing the thread. + void SetTemplatePath(const char *path); + // Start executing req. The control of req is handed over to the + // downloader thread. The main thread should not access req until + // the request is handed back to the main thread by + // GetFinishedRequest(). + void AddRequest(cMenuRequest *req); + // Return a request that has finished or NULL if no requests are + // finished. The ownership of the returned cMenuRequest object + // is again assigned to the main thread. The main thread should poll + // this function periodically. + cMenuRequest *GetFinishedRequest(); + // Returns the number download requests currectly active + int GetUnfinishedCount(); +}; + +#endif diff --git a/src/vdr-plugin/history.c b/src/vdr-plugin/history.c new file mode 100644 index 0000000..a463bac --- /dev/null +++ b/src/vdr-plugin/history.c @@ -0,0 +1,145 @@ +/* + * history.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include "history.h" +#include "menu.h" + +// --- cHistoryObject ----------------------------------------------------- + +cHistoryObject::cHistoryObject(const char *xml, const char *ref, int ID) { + osdxml = strdup(xml); + reference = strdup(ref); + id = ID; + selected = 0; +} + +cHistoryObject::~cHistoryObject() { + if (osdxml) + free(osdxml); + if (reference) + free(reference); + + for (int i=0; i < editData.Size(); i++) + delete editData[i]; +} + +cQueryData *cHistoryObject::GetEditItem(const char *controlName) { + for (int i=0; i < editData.Size(); i++) { + if (strcmp(editData[i]->GetName(), controlName) == 0) { + return editData[i]; + } + } + + return NULL; +} + +int cHistoryObject::QuerySize() const { + return editData.Size(); +} + +char *cHistoryObject::GetQueryFragment(int i) const { + if (i < 0 && i >= editData.Size()) + return NULL; + else + return editData[i]->GetQueryFragment(); +} + +cTextFieldData *cHistoryObject::GetTextFieldData(const char *controlName) { + cQueryData *edititem = GetEditItem(controlName); + cTextFieldData *tfdata = dynamic_cast<cTextFieldData *>(edititem); + + if (!tfdata) { + tfdata = new cTextFieldData(controlName, 256); + editData.Append(tfdata); + } + + return tfdata; +} + +cItemListData *cHistoryObject::GetItemListData(const char *controlName, + cStringList &items, + cStringList &values) { + int n; + char **itemtable, **itemvaluetable; + cQueryData *edititem = GetEditItem(controlName); + cItemListData *ildata = dynamic_cast<cItemListData *>(edititem); + + if (!ildata) { + n = min(items.Size(), values.Size()); + itemtable = (char **)malloc(n*sizeof(char *)); + itemvaluetable = (char **)malloc(n*sizeof(char *)); + + for (int i=0; i<n; i++) { + itemtable[i] = strdup(csc.Convert(items[i])); + itemvaluetable[i] = strdup(values[i]); + } + + ildata = new cItemListData(controlName, + itemtable, + itemvaluetable, + n); + + editData.Append(ildata); + } + + return ildata; +} + +// --- cHistory ------------------------------------------------------------ + +cHistory::cHistory() { + current = NULL; +} + +void cHistory::Clear() { + current = NULL; + cList<cHistoryObject>::Clear(); +} + +void cHistory::TruncateAndAdd(cHistoryObject *page) { + cHistoryObject *last = Last(); + while ((last) && (last != current)) { + Del(last); + last = Last(); + } + + Add(page); + current = Last(); +} + +void cHistory::Reset() { + current = NULL; +} + +cHistoryObject *cHistory::Current() { + return current; +} + +cHistoryObject *cHistory::Home() { + current = First(); + return current; +} + +cHistoryObject *cHistory::Back() { + if (current) + current = Prev(current); + return current; +} + +cHistoryObject *cHistory::Forward() { + cHistoryObject *next; + if (current) { + next = Next(current); + if (next) + current = next; + } else { + current = First(); + } + return current; +} diff --git a/src/vdr-plugin/history.h b/src/vdr-plugin/history.h new file mode 100644 index 0000000..fd5fcf9 --- /dev/null +++ b/src/vdr-plugin/history.h @@ -0,0 +1,62 @@ +/* + * history.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_HISTORY_H +#define __WEBVIDEO_HISTORY_H + +#include <vdr/tools.h> +#include "menudata.h" + +// --- cHistoryObject ----------------------------------------------------- + +class cHistoryObject : public cListObject { +private: + char *osdxml; + int id; + int selected; + cVector<cQueryData *> editData; + char *reference; + + cQueryData *GetEditItem(const char *controlName); + +public: + cHistoryObject(const char *xml, const char *reference, int ID); + ~cHistoryObject(); + + int GetID() const { return id; } + const char *GetOSD() const { return osdxml; } + const char *GetReference() const { return reference; } + void RememberSelected(int sel) { selected = sel; } + int GetSelected() const { return selected; } + + int QuerySize() const; + char *GetQueryFragment(int i) const; + cTextFieldData *GetTextFieldData(const char *controlName); + cItemListData *GetItemListData(const char *controlName, + cStringList &items, + cStringList &itemvalues); +}; + +// --- cHistory ------------------------------------------------------------ + +class cHistory : public cList<cHistoryObject> { +private: + cHistoryObject *current; +public: + cHistory(); + + void Clear(); + void TruncateAndAdd(cHistoryObject *page); + void Reset(); + cHistoryObject *Current(); + cHistoryObject *Home(); + cHistoryObject *Back(); + cHistoryObject *Forward(); +}; + +#endif // __WEBVIDEO_HISTORY_H diff --git a/src/vdr-plugin/iniparser.c b/src/vdr-plugin/iniparser.c new file mode 100644 index 0000000..3990e74 --- /dev/null +++ b/src/vdr-plugin/iniparser.c @@ -0,0 +1,650 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file iniparser.c + @author N. Devillard + @date Sep 2007 + @version 3.0 + @brief Parser for ini files. +*/ +/*--------------------------------------------------------------------------*/ +/* + $Id: iniparser.c,v 2.18 2008-01-03 18:35:39 ndevilla Exp $ + $Revision: 2.18 $ + $Date: 2008-01-03 18:35:39 $ +*/ +/*---------------------------- Includes ------------------------------------*/ +#include <ctype.h> +#include "iniparser.h" + +/*---------------------------- Defines -------------------------------------*/ +#define ASCIILINESZ (1024) +#define INI_INVALID_KEY ((char*)-1) + +/*--------------------------------------------------------------------------- + Private to this module + ---------------------------------------------------------------------------*/ +/** + * This enum stores the status for each parsed line (internal use only). + */ +typedef enum _line_status_ { + LINE_UNPROCESSED, + LINE_ERROR, + LINE_EMPTY, + LINE_COMMENT, + LINE_SECTION, + LINE_VALUE +} line_status ; + +/*-------------------------------------------------------------------------*/ +/** + @brief Convert a string to lowercase. + @param s String to convert. + @return ptr to statically allocated string. + + This function returns a pointer to a statically allocated string + containing a lowercased version of the input string. Do not free + or modify the returned string! Since the returned string is statically + allocated, it will be modified at each function call (not re-entrant). + */ +/*--------------------------------------------------------------------------*/ +static char * strlwc(const char * s) +{ + static char l[ASCIILINESZ+1]; + int i ; + + if (s==NULL) return NULL ; + memset(l, 0, ASCIILINESZ+1); + i=0 ; + while (s[i] && i<ASCIILINESZ) { + l[i] = (char)tolower((int)s[i]); + i++ ; + } + l[ASCIILINESZ]=(char)0; + return l ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Remove blanks at the beginning and the end of a string. + @param s String to parse. + @return ptr to statically allocated string. + + This function returns a pointer to a statically allocated string, + which is identical to the input string, except that all blank + characters at the end and the beg. of the string have been removed. + Do not free or modify the returned string! Since the returned string + is statically allocated, it will be modified at each function call + (not re-entrant). + */ +/*--------------------------------------------------------------------------*/ +static char * strstrip(char * s) +{ + static char l[ASCIILINESZ+1]; + char * last ; + + if (s==NULL) return NULL ; + + while (isspace((int)*s) && *s) s++; + memset(l, 0, ASCIILINESZ+1); + strcpy(l, s); + last = l + strlen(l); + while (last > l) { + if (!isspace((int)*(last-1))) + break ; + last -- ; + } + *last = (char)0; + return (char*)l ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get number of sections in a dictionary + @param d Dictionary to examine + @return int Number of sections found in dictionary + + This function returns the number of sections found in a dictionary. + The test to recognize sections is done on the string stored in the + dictionary: a section name is given as "section" whereas a key is + stored as "section:key", thus the test looks for entries that do not + contain a colon. + + This clearly fails in the case a section name contains a colon, but + this should simply be avoided. + + This function returns -1 in case of error. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getnsec(dictionary * d) +{ + int i ; + int nsec ; + + if (d==NULL) return -1 ; + nsec=0 ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (strchr(d->key[i], ':')==NULL) { + nsec ++ ; + } + } + return nsec ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get name for section n in a dictionary. + @param d Dictionary to examine + @param n Section number (from 0 to nsec-1). + @return Pointer to char string + + This function locates the n-th section in a dictionary and returns + its name as a pointer to a string statically allocated inside the + dictionary. Do not free or modify the returned string! + + This function returns NULL in case of error. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getsecname(dictionary * d, int n) +{ + int i ; + int foundsec ; + + if (d==NULL || n<0) return NULL ; + foundsec=0 ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (strchr(d->key[i], ':')==NULL) { + foundsec++ ; + if (foundsec>n) + break ; + } + } + if (foundsec<=n) { + return NULL ; + } + return d->key[i] ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump. + @param f Opened file pointer to dump to. + @return void + + This function prints out the contents of a dictionary, one element by + line, onto the provided file pointer. It is OK to specify @c stderr + or @c stdout as output files. This function is meant for debugging + purposes mostly. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump(dictionary * d, FILE * f) +{ + int i ; + + if (d==NULL || f==NULL) return ; + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + if (d->val[i]!=NULL) { + fprintf(f, "[%s]=[%s]\n", d->key[i], d->val[i]); + } else { + fprintf(f, "[%s]=UNDEF\n", d->key[i]); + } + } + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Save a dictionary to a loadable ini file + @param d Dictionary to dump + @param f Opened file pointer to dump to + @return void + + This function dumps a given dictionary into a loadable ini file. + It is Ok to specify @c stderr or @c stdout as output files. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump_ini(dictionary * d, FILE * f) +{ + int i, j ; + char keym[ASCIILINESZ+1]; + int nsec ; + char * secname ; + int seclen ; + + if (d==NULL || f==NULL) return ; + + nsec = iniparser_getnsec(d); + if (nsec<1) { + /* No section in file: dump all keys as they are */ + for (i=0 ; i<d->size ; i++) { + if (d->key[i]==NULL) + continue ; + fprintf(f, "%s = %s\n", d->key[i], d->val[i]); + } + return ; + } + for (i=0 ; i<nsec ; i++) { + secname = iniparser_getsecname(d, i) ; + seclen = (int)strlen(secname); + fprintf(f, "\n[%s]\n", secname); + sprintf(keym, "%s:", secname); + for (j=0 ; j<d->size ; j++) { + if (d->key[j]==NULL) + continue ; + if (!strncmp(d->key[j], keym, seclen+1)) { + fprintf(f, + "%-30s = %s\n", + d->key[j]+seclen+1, + d->val[j] ? d->val[j] : ""); + } + } + } + fprintf(f, "\n"); + return ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key + @param d Dictionary to search + @param key Key string to look for + @param def Default value to return if key not found. + @return pointer to statically allocated character string + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the pointer passed as 'def' is returned. + The returned char pointer is pointing to a string allocated in + the dictionary, do not free or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getstring(dictionary * d, const char * key, char * def) +{ + char * lc_key ; + char * sval ; + + if (d==NULL || key==NULL) + return def ; + + lc_key = strlwc(key); + sval = dictionary_get(d, lc_key, def); + return sval ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to an int + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + Supported values for integers include the usual C notation + so decimal, octal (starting with 0) and hexadecimal (starting with 0x) + are supported. Examples: + + "42" -> 42 + "042" -> 34 (octal -> decimal) + "0x42" -> 66 (hexa -> decimal) + + Warning: the conversion may overflow in various ways. Conversion is + totally outsourced to strtol(), see the associated man page for overflow + handling. + + Credits: Thanks to A. Becker for suggesting strtol() + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getint(dictionary * d, const char * key, int notfound) +{ + char * str ; + + str = iniparser_getstring(d, key, INI_INVALID_KEY); + if (str==INI_INVALID_KEY) return notfound ; + return (int)strtol(str, NULL, 0); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a double + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return double + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + */ +/*--------------------------------------------------------------------------*/ +double iniparser_getdouble(dictionary * d, char * key, double notfound) +{ + char * str ; + + str = iniparser_getstring(d, key, INI_INVALID_KEY); + if (str==INI_INVALID_KEY) return notfound ; + return atof(str); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a boolean + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + A true boolean is found if one of the following is matched: + + - A string starting with 'y' + - A string starting with 'Y' + - A string starting with 't' + - A string starting with 'T' + - A string starting with '1' + + A false boolean is found if one of the following is matched: + + - A string starting with 'n' + - A string starting with 'N' + - A string starting with 'f' + - A string starting with 'F' + - A string starting with '0' + + The notfound value returned if no boolean is identified, does not + necessarily have to be 0 or 1. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getboolean(dictionary * d, const char * key, int notfound) +{ + char * c ; + int ret ; + + c = iniparser_getstring(d, key, INI_INVALID_KEY); + if (c==INI_INVALID_KEY) return notfound ; + if (c[0]=='y' || c[0]=='Y' || c[0]=='1' || c[0]=='t' || c[0]=='T') { + ret = 1 ; + } else if (c[0]=='n' || c[0]=='N' || c[0]=='0' || c[0]=='f' || c[0]=='F') { + ret = 0 ; + } else { + ret = notfound ; + } + return ret; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Finds out if a given entry exists in a dictionary + @param ini Dictionary to search + @param entry Name of the entry to look for + @return integer 1 if entry exists, 0 otherwise + + Finds out if a given entry exists in the dictionary. Since sections + are stored as keys with NULL associated values, this is the only way + of querying for the presence of sections in a dictionary. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_find_entry( + dictionary * ini, + char * entry +) +{ + int found=0 ; + if (iniparser_getstring(ini, entry, INI_INVALID_KEY)!=INI_INVALID_KEY) { + found = 1 ; + } + return found ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Set an entry in a dictionary. + @param ini Dictionary to modify. + @param entry Entry to modify (entry name) + @param val New value to associate to the entry. + @return int 0 if Ok, -1 otherwise. + + If the given entry can be found in the dictionary, it is modified to + contain the provided value. If it cannot be found, -1 is returned. + It is Ok to set val to NULL. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_set(dictionary * ini, char * entry, char * val) +{ + return dictionary_set(ini, strlwc(entry), val) ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete an entry in a dictionary + @param ini Dictionary to modify + @param entry Entry to delete (entry name) + @return void + + If the given entry can be found, it is deleted from the dictionary. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_unset(dictionary * ini, char * entry) +{ + dictionary_unset(ini, strlwc(entry)); +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Load a single line from an INI file + @param input_line Input line, may be concatenated multi-line input + @param section Output space to store section + @param key Output space to store key + @param value Output space to store value + @return line_status value + */ +/*--------------------------------------------------------------------------*/ +static line_status iniparser_line( + char * input_line, + char * section, + char * key, + char * value) +{ + line_status sta ; + char line[ASCIILINESZ+1]; + int len ; + + strcpy(line, strstrip(input_line)); + len = (int)strlen(line); + + sta = LINE_UNPROCESSED ; + if (len<1) { + /* Empty line */ + sta = LINE_EMPTY ; + } else if (line[0]=='#') { + /* Comment line */ + sta = LINE_COMMENT ; + } else if (line[0]=='[' && line[len-1]==']') { + /* Section name */ + sscanf(line, "[%[^]]", section); + strcpy(section, strstrip(section)); + strcpy(section, strlwc(section)); + sta = LINE_SECTION ; + } else if (sscanf (line, "%[^=] = \"%[^\"]\"", key, value) == 2 + || sscanf (line, "%[^=] = '%[^\']'", key, value) == 2 + || sscanf (line, "%[^=] = %[^;#]", key, value) == 2) { + /* Usual key=value, with or without comments */ + strcpy(key, strstrip(key)); + strcpy(key, strlwc(key)); + strcpy(value, strstrip(value)); + /* + * sscanf cannot handle '' or "" as empty values + * this is done here + */ + if (!strcmp(value, "\"\"") || (!strcmp(value, "''"))) { + value[0]=0 ; + } + sta = LINE_VALUE ; + } else if (sscanf(line, "%[^=] = %[;#]", key, value)==2 + || sscanf(line, "%[^=] %[=]", key, value) == 2) { + /* + * Special cases: + * key= + * key=; + * key=# + */ + strcpy(key, strstrip(key)); + strcpy(key, strlwc(key)); + value[0]=0 ; + sta = LINE_VALUE ; + } else { + /* Generate syntax error */ + sta = LINE_ERROR ; + } + return sta ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Parse an ini file and return an allocated dictionary object + @param ininame Name of the ini file to read. + @return Pointer to newly allocated dictionary + + This is the parser for ini files. This function is called, providing + the name of the file to be read. It returns a dictionary object that + should not be accessed directly, but through accessor functions + instead. + + The returned dictionary must be freed using iniparser_freedict(). + */ +/*--------------------------------------------------------------------------*/ +dictionary * iniparser_load(const char * ininame) +{ + FILE * in ; + + char line [ASCIILINESZ+1] ; + char section [ASCIILINESZ+1] ; + char key [ASCIILINESZ+1] ; + char tmp [ASCIILINESZ+1] ; + char val [ASCIILINESZ+1] ; + + int last=0 ; + int len ; + int lineno=0 ; + int errs=0; + + dictionary * dict ; + + if ((in=fopen(ininame, "r"))==NULL) { + fprintf(stderr, "iniparser: cannot open %s\n", ininame); + return NULL ; + } + + dict = dictionary_new(0) ; + if (!dict) { + fclose(in); + return NULL ; + } + + memset(line, 0, ASCIILINESZ); + memset(section, 0, ASCIILINESZ); + memset(key, 0, ASCIILINESZ); + memset(val, 0, ASCIILINESZ); + last=0 ; + + while (fgets(line+last, ASCIILINESZ-last, in)!=NULL) { + lineno++ ; + len = (int)strlen(line)-1; + /* Safety check against buffer overflows */ + if (line[len]!='\n') { + fprintf(stderr, + "iniparser: input line too long in %s (%d)\n", + ininame, + lineno); + dictionary_del(dict); + fclose(in); + return NULL ; + } + /* Get rid of \n and spaces at end of line */ + while ((len>=0) && + ((line[len]=='\n') || (isspace(line[len])))) { + line[len]=0 ; + len-- ; + } + /* Detect multi-line */ + if (line[len]=='\\') { + /* Multi-line value */ + last=len ; + continue ; + } else { + last=0 ; + } + switch (iniparser_line(line, section, key, val)) { + case LINE_EMPTY: + case LINE_COMMENT: + break ; + + case LINE_SECTION: + errs = dictionary_set(dict, section, NULL); + break ; + + case LINE_VALUE: + sprintf(tmp, "%s:%s", section, key); + errs = dictionary_set(dict, tmp, val) ; + break ; + + case LINE_ERROR: + fprintf(stderr, "iniparser: syntax error in %s (%d):\n", + ininame, + lineno); + fprintf(stderr, "-> %s\n", line); + errs++ ; + break; + + default: + break ; + } + memset(line, 0, ASCIILINESZ); + last=0; + if (errs<0) { + fprintf(stderr, "iniparser: memory allocation failure\n"); + break ; + } + } + if (errs) { + dictionary_del(dict); + dict = NULL ; + } + fclose(in); + return dict ; +} + +/*-------------------------------------------------------------------------*/ +/** + @brief Free all memory associated to an ini dictionary + @param d Dictionary to free + @return void + + Free all memory associated to an ini dictionary. + It is mandatory to call this function before the dictionary object + gets out of the current context. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_freedict(dictionary * d) +{ + dictionary_del(d); +} + +/* vim: set ts=4 et sw=4 tw=75 */ diff --git a/src/vdr-plugin/iniparser.h b/src/vdr-plugin/iniparser.h new file mode 100644 index 0000000..78bf339 --- /dev/null +++ b/src/vdr-plugin/iniparser.h @@ -0,0 +1,284 @@ +/* + Copyright (c) 2000-2007 by Nicolas Devillard. + MIT License, see COPYING for more information. +*/ + +/*-------------------------------------------------------------------------*/ +/** + @file iniparser.h + @author N. Devillard + @date Sep 2007 + @version 3.0 + @brief Parser for ini files. +*/ +/*--------------------------------------------------------------------------*/ + +/* + $Id: iniparser.h,v 1.24 2007-11-23 21:38:19 ndevilla Exp $ + $Revision: 1.24 $ +*/ + +#ifndef _INIPARSER_H_ +#define _INIPARSER_H_ + +/*--------------------------------------------------------------------------- + Includes + ---------------------------------------------------------------------------*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* + * The following #include is necessary on many Unixes but not Linux. + * It is not needed for Windows platforms. + * Uncomment it if needed. + */ +/* #include <unistd.h> */ + +#include "dictionary.h" + +/*--------------------------------------------------------------------------- + Macros + ---------------------------------------------------------------------------*/ +/** For backwards compatibility only */ +#define iniparser_getstr(d, k) iniparser_getstring(d, k, NULL) +#define iniparser_setstr iniparser_setstring + +/*-------------------------------------------------------------------------*/ +/** + @brief Get number of sections in a dictionary + @param d Dictionary to examine + @return int Number of sections found in dictionary + + This function returns the number of sections found in a dictionary. + The test to recognize sections is done on the string stored in the + dictionary: a section name is given as "section" whereas a key is + stored as "section:key", thus the test looks for entries that do not + contain a colon. + + This clearly fails in the case a section name contains a colon, but + this should simply be avoided. + + This function returns -1 in case of error. + */ +/*--------------------------------------------------------------------------*/ + +int iniparser_getnsec(dictionary * d); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Get name for section n in a dictionary. + @param d Dictionary to examine + @param n Section number (from 0 to nsec-1). + @return Pointer to char string + + This function locates the n-th section in a dictionary and returns + its name as a pointer to a string statically allocated inside the + dictionary. Do not free or modify the returned string! + + This function returns NULL in case of error. + */ +/*--------------------------------------------------------------------------*/ + +char * iniparser_getsecname(dictionary * d, int n); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Save a dictionary to a loadable ini file + @param d Dictionary to dump + @param f Opened file pointer to dump to + @return void + + This function dumps a given dictionary into a loadable ini file. + It is Ok to specify @c stderr or @c stdout as output files. + */ +/*--------------------------------------------------------------------------*/ + +void iniparser_dump_ini(dictionary * d, FILE * f); + +/*-------------------------------------------------------------------------*/ +/** + @brief Dump a dictionary to an opened file pointer. + @param d Dictionary to dump. + @param f Opened file pointer to dump to. + @return void + + This function prints out the contents of a dictionary, one element by + line, onto the provided file pointer. It is OK to specify @c stderr + or @c stdout as output files. This function is meant for debugging + purposes mostly. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_dump(dictionary * d, FILE * f); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key + @param d Dictionary to search + @param key Key string to look for + @param def Default value to return if key not found. + @return pointer to statically allocated character string + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the pointer passed as 'def' is returned. + The returned char pointer is pointing to a string allocated in + the dictionary, do not free or modify it. + */ +/*--------------------------------------------------------------------------*/ +char * iniparser_getstring(dictionary * d, const char * key, char * def); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to an int + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + Supported values for integers include the usual C notation + so decimal, octal (starting with 0) and hexadecimal (starting with 0x) + are supported. Examples: + + - "42" -> 42 + - "042" -> 34 (octal -> decimal) + - "0x42" -> 66 (hexa -> decimal) + + Warning: the conversion may overflow in various ways. Conversion is + totally outsourced to strtol(), see the associated man page for overflow + handling. + + Credits: Thanks to A. Becker for suggesting strtol() + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getint(dictionary * d, const char * key, int notfound); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a double + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return double + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + */ +/*--------------------------------------------------------------------------*/ +double iniparser_getdouble(dictionary * d, char * key, double notfound); + +/*-------------------------------------------------------------------------*/ +/** + @brief Get the string associated to a key, convert to a boolean + @param d Dictionary to search + @param key Key string to look for + @param notfound Value to return in case of error + @return integer + + This function queries a dictionary for a key. A key as read from an + ini file is given as "section:key". If the key cannot be found, + the notfound value is returned. + + A true boolean is found if one of the following is matched: + + - A string starting with 'y' + - A string starting with 'Y' + - A string starting with 't' + - A string starting with 'T' + - A string starting with '1' + + A false boolean is found if one of the following is matched: + + - A string starting with 'n' + - A string starting with 'N' + - A string starting with 'f' + - A string starting with 'F' + - A string starting with '0' + + The notfound value returned if no boolean is identified, does not + necessarily have to be 0 or 1. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_getboolean(dictionary * d, const char * key, int notfound); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Set an entry in a dictionary. + @param ini Dictionary to modify. + @param entry Entry to modify (entry name) + @param val New value to associate to the entry. + @return int 0 if Ok, -1 otherwise. + + If the given entry can be found in the dictionary, it is modified to + contain the provided value. If it cannot be found, -1 is returned. + It is Ok to set val to NULL. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_setstring(dictionary * ini, char * entry, char * val); + + +/*-------------------------------------------------------------------------*/ +/** + @brief Delete an entry in a dictionary + @param ini Dictionary to modify + @param entry Entry to delete (entry name) + @return void + + If the given entry can be found, it is deleted from the dictionary. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_unset(dictionary * ini, char * entry); + +/*-------------------------------------------------------------------------*/ +/** + @brief Finds out if a given entry exists in a dictionary + @param ini Dictionary to search + @param entry Name of the entry to look for + @return integer 1 if entry exists, 0 otherwise + + Finds out if a given entry exists in the dictionary. Since sections + are stored as keys with NULL associated values, this is the only way + of querying for the presence of sections in a dictionary. + */ +/*--------------------------------------------------------------------------*/ +int iniparser_find_entry(dictionary * ini, char * entry) ; + +/*-------------------------------------------------------------------------*/ +/** + @brief Parse an ini file and return an allocated dictionary object + @param ininame Name of the ini file to read. + @return Pointer to newly allocated dictionary + + This is the parser for ini files. This function is called, providing + the name of the file to be read. It returns a dictionary object that + should not be accessed directly, but through accessor functions + instead. + + The returned dictionary must be freed using iniparser_freedict(). + */ +/*--------------------------------------------------------------------------*/ +dictionary * iniparser_load(const char * ininame); + +/*-------------------------------------------------------------------------*/ +/** + @brief Free all memory associated to an ini dictionary + @param d Dictionary to free + @return void + + Free all memory associated to an ini dictionary. + It is mandatory to call this function before the dictionary object + gets out of the current context. + */ +/*--------------------------------------------------------------------------*/ +void iniparser_freedict(dictionary * d); + +#endif diff --git a/src/vdr-plugin/menu.c b/src/vdr-plugin/menu.c new file mode 100644 index 0000000..3add4d4 --- /dev/null +++ b/src/vdr-plugin/menu.c @@ -0,0 +1,670 @@ +/* + * menu.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdlib.h> +#include <time.h> +#include <vdr/skins.h> +#include <vdr/tools.h> +#include <vdr/i18n.h> +#include <vdr/osdbase.h> +#include <vdr/skins.h> +#include <vdr/font.h> +#include <vdr/osd.h> +#include <vdr/interface.h> +#include "menu.h" +#include "download.h" +#include "config.h" +#include "common.h" +#include "history.h" +#include "timer.h" +#include "menu_timer.h" + +cCharSetConv csc = cCharSetConv("UTF-8", cCharSetConv::SystemCharacterTable()); +struct MenuPointers menuPointers; + +// --- cXMLMenu -------------------------------------------------- + +cXMLMenu::cXMLMenu(const char *Title, int c0, int c1, int c2, + int c3, int c4) +: cOsdMenu(Title, c0, c1, c2, c3, c4) +{ +} + +bool cXMLMenu::Deserialize(const char *xml) { + xmlDocPtr doc = xmlParseMemory(xml, strlen(xml)); + if (!doc) { + xmlErrorPtr xmlerr = xmlGetLastError(); + if (xmlerr) { + error("libxml error: %s", xmlerr->message); + } + + return false; + } + + xmlNodePtr node = xmlDocGetRootElement(doc); + if (node) + node = node->xmlChildrenNode; + + while (node) { + if (node->type == XML_ELEMENT_NODE) { + if (!CreateItemFromTag(doc, node)) { + warning("Failed to parse menu tag: %s", (char *)node->name); + } + } + node = node->next; + } + + xmlFreeDoc(doc); + return true; +} + +int cXMLMenu::Load(const char *xmlstr) { + Clear(); + Deserialize(xmlstr); + + return 0; +} + + +// --- cNavigationMenu ----------------------------------------------------- + +cNavigationMenu::cNavigationMenu(cHistory *History, + cProgressVector& dlsummaries) + : cXMLMenu("", 25), summaries(dlsummaries) +{ + title = NULL; + reference = NULL; + shortcutMode = 0; + history = History; + UpdateHelp(); +} + +cNavigationMenu::~cNavigationMenu() { + menuPointers.navigationMenu = NULL; + Clear(); + if (reference) + free(reference); +} + +bool cNavigationMenu::CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) { + if (!xmlStrcmp(node->name, BAD_CAST "link")) { + NewLinkItem(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "textfield")) { + NewTextField(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "itemlist")) { + NewItemList(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "textarea")) { + NewTextArea(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "button")) { + NewButton(doc, node); + return true; + } else if (!xmlStrcmp(node->name, BAD_CAST "title")) { + NewTitle(doc, node); + return true; + } + + return false; +} + +void cNavigationMenu::AddLinkItem(cOsdItem *item, + cLinkBase *ref, + cLinkBase *streamref) { + Add(item); + + if (ref) + links.Append(ref); + else + links.Append(NULL); + + if (streamref) + streams.Append(streamref); + else + streams.Append(NULL); +} + +void cNavigationMenu::NewLinkItem(xmlDocPtr doc, xmlNodePtr node) { + // label, ref and object tags + xmlChar *itemtitle = NULL, *ref = NULL, *streamref = NULL; + + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "ref")) { + if (ref) + xmlFree(ref); + ref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "stream")) { + if (streamref) + xmlFree(streamref); + streamref = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + itemtitle = xmlCharStrdup("???"); + + const char *titleconv = csc.Convert((char *)itemtitle); + cOsdItem *item = new cOsdItem(titleconv); + cSimpleLink *objlinkdata = NULL; + cSimpleLink *linkdata = NULL; + if (ref) + linkdata = new cSimpleLink((char *)ref); + if (streamref) { + // media object + objlinkdata = new cSimpleLink((char *)streamref); + } else { + // navigation link + char *bracketed = (char *)malloc((strlen(titleconv)+3)*sizeof(char)); + if (bracketed) { + bracketed[0] = '\0'; + strcat(bracketed, "["); + strcat(bracketed, titleconv); + strcat(bracketed, "]"); + item->SetText(bracketed, false); + } + } + AddLinkItem(item, linkdata, objlinkdata); + + xmlFree(itemtitle); + if (ref) + xmlFree(ref); + if (streamref) + xmlFree(streamref); +} + +void cNavigationMenu::NewTextField(xmlDocPtr doc, xmlNodePtr node) { + // name attribute + xmlChar *name = xmlGetProp(node, BAD_CAST "name"); + cHistoryObject *curhistpage = history->Current(); + + // label tag + xmlChar *text = NULL; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (text) + xmlFree(text); + text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!text) + text = xmlCharStrdup("???"); + + cTextFieldData *data = curhistpage->GetTextFieldData((char *)name); + cMenuEditStrItem *item = new cMenuEditStrItem(csc.Convert((char *)text), + data->GetValue(), + data->GetLength()); + AddLinkItem(item, NULL, NULL); + + free(text); + if (name) + xmlFree(name); +} + +void cNavigationMenu::NewItemList(xmlDocPtr doc, xmlNodePtr node) { + // name attribute + xmlChar *name = xmlGetProp(node, BAD_CAST "name"); + cHistoryObject *curhistpage = history->Current(); + + // label and item tags + xmlChar *text = NULL; + cStringList items; + cStringList itemvalues; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (text) + xmlFree(text); + text = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "item")) { + xmlChar *str = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + if (!str) + str = xmlCharStrdup("???"); + xmlChar *strvalue = xmlGetProp(node, BAD_CAST "value"); + if (!strvalue) + strvalue = xmlCharStrdup(""); + + items.Append(strdup((char *)str)); + itemvalues.Append(strdup((char *)strvalue)); + + xmlFree(str); + xmlFree(strvalue); + } + node = node->next; + } + if (!text) + text = xmlCharStrdup("???"); + + cItemListData *data = curhistpage->GetItemListData((const char *)name, + items, + itemvalues); + + cMenuEditStraItem *item = new cMenuEditStraItem(csc.Convert((char *)text), + data->GetValuePtr(), + data->GetNumStrings(), + data->GetStrings()); + AddLinkItem(item, NULL, NULL); + + xmlFree(text); + if (name) + xmlFree(name); +} + +void cNavigationMenu::NewTextArea(xmlDocPtr doc, xmlNodePtr node) { + // label tag + xmlChar *itemtitle = NULL; + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + return; + + const cFont *font = cFont::GetFont(fontOsd); + cTextWrapper tw(csc.Convert((char *)itemtitle), font, cOsd::OsdWidth()); + for (int i=0; i < tw.Lines(); i++) { + AddLinkItem(new cOsdItem(tw.GetLine(i), osUnknown, false), NULL, NULL); + } + + xmlFree(itemtitle); +} + +void cNavigationMenu::NewButton(xmlDocPtr doc, xmlNodePtr node) { + // label and submission tags + xmlChar *itemtitle = NULL, *submission = NULL; + cHistoryObject *curhistpage = history->Current(); + + node = node->xmlChildrenNode; + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "label")) { + if (itemtitle) + xmlFree(itemtitle); + itemtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (!xmlStrcmp(node->name, BAD_CAST "submission")) { + if (submission) + xmlFree(submission); + submission = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } + node = node->next; + } + if (!itemtitle) + itemtitle = xmlCharStrdup("???"); + + cSubmissionButtonData *data = \ + new cSubmissionButtonData((char *)submission, curhistpage); + const char *titleconv = csc.Convert((char *)itemtitle); // do not free + char *newtitle = (char *)malloc((strlen(titleconv)+3)*sizeof(char)); + if (newtitle) { + newtitle[0] = '\0'; + strcat(newtitle, "["); + strcat(newtitle, titleconv); + strcat(newtitle, "]"); + + cOsdItem *item = new cOsdItem(newtitle); + AddLinkItem(item, data, NULL); + free(newtitle); + } + + xmlFree(itemtitle); + if (submission) + xmlFree(submission); +} + +void cNavigationMenu::NewTitle(xmlDocPtr doc, xmlNodePtr node) { + xmlChar *newtitle = xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + if (newtitle) { + const char *conv = csc.Convert((char *)newtitle); + SetTitle(conv); + if (title) + free(title); + title = strdup(conv); + xmlFree(newtitle); + } +} + +eOSState cNavigationMenu::ProcessKey(eKeys Key) +{ + cWebviTimer *timer; + bool hasStreams; + int old = Current(); + eOSState state = cXMLMenu::ProcessKey(Key); + bool validItem = Current() >= 0 && Current() < links.Size(); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kInfo: + // The alternative link is active only when object links are + // present. + if (validItem && streams.At(Current())) + state = Select(links.At(Current()), LT_REGULAR); + break; + + case kOk: + // Primary action: download media object or, if not a media + // link, follow the navigation link. + if (validItem) { + if (streams.At(Current())) + state = Select(streams.At(Current()), LT_MEDIA); + else + state = Select(links.At(Current()), LT_REGULAR); + } + break; + + case kRed: + if (shortcutMode == 0) { + state = HistoryBack(); + } else { + menuPointers.statusScreen = new cStatusScreen(summaries); + state = AddSubMenu(menuPointers.statusScreen); + } + break; + + case kGreen: + if (shortcutMode == 0) { + state = HistoryForward(); + } else { + return AddSubMenu(new cWebviTimerListMenu(cWebviTimerManager::Instance())); + } + break; + + case kYellow: + if (shortcutMode == 0) { + hasStreams = false; + for (int i=0; i < streams.Size(); i++) { + if (streams[i]) { + hasStreams = true; + break; + } + } + + if (hasStreams || Interface->Confirm(tr("No streams on this page, create timer anyway?"))) { + timer = cWebviTimerManager::Instance().Create(title, reference); + if (timer) + return AddSubMenu(new cEditWebviTimerMenu(*timer, true, false)); + } + + state = osContinue; + } + break; + + case kBlue: + if (shortcutMode == 0) { + // Secondary action: start streaming if a media object + if (validItem && streams.At(Current())) + state = Select(streams.At(Current()), LT_STREAMINGMEDIA); + } + break; + + case k0: + shortcutMode = shortcutMode == 0 ? 1 : 0; + UpdateHelp(); + break; + + default: + break; + } + } else { + // If the key press caused the selected item to change, we need to + // update the help texts. + // + // In cMenuEditStrItem key == kOk with state == osContinue + // indicates leaving the edit mode. We want to update the help + // texts in this case also. + if ((old != Current()) || + ((Key == kOk) && (state == osContinue))) { + UpdateHelp(); + } + } + + return state; +} + +eOSState cNavigationMenu::Select(cLinkBase *link, eLinkType type) +{ + if (!link) { + return osContinue; + } + char *ref = link->GetURL(); + if (!ref) { + error("link->GetURL() == NULL in cNavigationMenu::Select"); + return osContinue; + } + + if (type == LT_MEDIA) { + cDownloadProgress *progress = summaries.NewDownload(); + cFileDownloadRequest *req = \ + new cFileDownloadRequest(history->Current()->GetID(), ref, + webvideoConfig->GetDownloadPath(), + progress); + cWebviThread::Instance().AddRequest(req); + + Skins.Message(mtInfo, tr("Downloading in the background")); + } else if (type == LT_STREAMINGMEDIA) { + cWebviThread::Instance().AddRequest(new cStreamUrlRequest(history->Current()->GetID(), + ref)); + Skins.Message(mtInfo, tr("Starting player...")); + return osEnd; + } else { + cWebviThread::Instance().AddRequest(new cMenuRequest(history->Current()->GetID(), + ref)); + Skins.Message(mtStatus, tr("Retrieving...")); + } + + return osContinue; +} + +void cNavigationMenu::Clear(void) { + cXMLMenu::Clear(); + SetTitle(""); + if (title) + free(title); + title = NULL; + for (int i=0; i < links.Size(); i++) { + if (links[i]) + delete links[i]; + if (streams[i]) + delete streams[i]; + } + links.Clear(); + streams.Clear(); +} + +void cNavigationMenu::Populate(const cHistoryObject *page, const char *statusmsg) { + Load(page->GetOSD()); + + if (reference) + free(reference); + reference = strdup(page->GetReference()); + + // Make sure that an item is selected (if there is at least + // one). The help texts are not updated correctly if no item is + // selected. + + SetCurrent(Get(page->GetSelected())); + UpdateHelp(); + SetStatus(statusmsg); +} + +eOSState cNavigationMenu::HistoryBack() { + cHistoryObject *cur = history->Current(); + + if (cur) + cur->RememberSelected(Current()); + + cHistoryObject *page = history->Back(); + if (page) { + Populate(page); + Display(); + } + return osContinue; +} + +eOSState cNavigationMenu::HistoryForward() { + cHistoryObject *before = history->Current(); + cHistoryObject *after = history->Forward(); + + if (before) + before->RememberSelected(Current()); + + // Update only if the menu really changed + if (before != after) { + Populate(after); + Display(); + } + return osContinue; +} + +void cNavigationMenu::UpdateHelp() { + const char *red = NULL; + const char *green = NULL; + const char *yellow = NULL; + const char *blue = NULL; + + if (shortcutMode == 0) { + red = (history->Current() != history->First()) ? tr("Back") : NULL; + green = (history->Current() != history->Last()) ? tr("Forward") : NULL; + yellow = (Current() >= 0) ? tr("Create timer") : NULL; + blue = ((Current() >= 0) && (streams.At(Current()))) ? tr("Play") : NULL; + } else { + red = tr("Status"); + green = tr("Timers"); + } + + SetHelp(red, green, yellow, blue); +} + +// --- cStatusScreen ------------------------------------------------------- + +cStatusScreen::cStatusScreen(cProgressVector& dlsummaries) + : cOsdMenu(tr("Unfinished downloads"), 40), summaries(dlsummaries) +{ + int charsperline = cOsd::OsdWidth() / cFont::GetFont(fontOsd)->Width('M'); + SetCols(charsperline-5); + + UpdateHelp(); + Update(); +} + +cStatusScreen::~cStatusScreen() { + menuPointers.statusScreen = NULL; +} + +void cStatusScreen::Update() { + int c = Current(); + + Clear(); + + if (summaries.Size() == 0) { + SetTitle(tr("No active downloads")); + } else { + + for (int i=0; i<summaries.Size(); i++) { + cString dltitle; + cDownloadProgress *s = summaries[i]; + dltitle = cString::sprintf("%s\t%s", + (const char *)s->GetTitle(), + (const char *)s->GetPercentage()); + + Add(new cOsdItem(dltitle)); + } + + if (c >= 0) + SetCurrent(Get(c)); + } + + lastupdate = time(NULL); + + UpdateHelp(); + Display(); +} + +bool cStatusScreen::NeedsUpdate() { + return (Count() > 0) && (time(NULL) - lastupdate >= updateInterval); +} + +eOSState cStatusScreen::ProcessKey(eKeys Key) { + cFileDownloadRequest *req; + int old = Current(); + eOSState state = cOsdMenu::ProcessKey(Key); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kYellow: + if ((Current() >= 0) && (Current() < summaries.Size())) { + if (summaries[Current()]->IsFinished()) { + delete summaries[Current()]; + summaries.Remove(Current()); + Update(); + } else if ((req = summaries[Current()]->GetRequest()) && + !req->IsFinished()) { + req->Abort(); + Update(); + } + } + return osContinue; + + case kOk: + case kInfo: + if (summaries[Current()]->Error()) { + cString msg = cString::sprintf("%s\n%s: %s", + (const char *)summaries[Current()]->GetTitle(), + tr("Error"), + (const char *)summaries[Current()]->GetStatusPharse()); + return AddSubMenu(new cMenuText(tr("Error details"), msg)); + } else { + cString msg = cString::sprintf("%s (%s)", + (const char *)summaries[Current()]->GetTitle(), + (const char *)summaries[Current()]->GetPercentage()); + return AddSubMenu(new cMenuText(tr("Download details"), msg)); + } + + return osContinue; + + default: + break; + } + } else { + // Update help if the key press caused the menu item to change. + if (old != Current()) + UpdateHelp(); + } + + return state; +} + +void cStatusScreen::UpdateHelp() { + bool remove = false; + if ((Current() >= 0) && (Current() < summaries.Size())) { + if (summaries[Current()]->IsFinished()) { + remove = true; + } + } + + const char *yellow = remove ? tr("Remove") : tr("Abort"); + + SetHelp(NULL, NULL, yellow, NULL); +} diff --git a/src/vdr-plugin/menu.h b/src/vdr-plugin/menu.h new file mode 100644 index 0000000..b1e67df --- /dev/null +++ b/src/vdr-plugin/menu.h @@ -0,0 +1,114 @@ +/* + * menu.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENU_H +#define __WEBVIDEO_MENU_H + +#include <time.h> +#include <vdr/osdbase.h> +#include <vdr/menuitems.h> +#include <vdr/menu.h> +#include <libxml/parser.h> +#include "download.h" +#include "menudata.h" + +extern cCharSetConv csc; + +// --- cXMLMenu -------------------------------------------------- + +class cXMLMenu : public cOsdMenu { +protected: + virtual bool Deserialize(const char *xml); + virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node) = 0; +public: + cXMLMenu(const char *Title, int c0 = 0, int c1 = 0, + int c2 = 0, int c3 = 0, int c4 = 0); + + int Load(const char *xmlstr); +}; + +// --- cNavigationMenu ----------------------------------------------------- + +enum eLinkType { LT_REGULAR, LT_MEDIA, LT_STREAMINGMEDIA }; + +class cHistory; +class cHistoryObject; +class cStatusScreen; + +class cNavigationMenu : public cXMLMenu { +private: + // links[i] is the navigation link of the i:th item + cVector<cLinkBase *> links; + // streams[i] is the media stream link of the i:th item + cVector<cLinkBase *> streams; + cProgressVector& summaries; + char *title; + char *reference; + int shortcutMode; + +protected: + cHistory *history; + + virtual bool CreateItemFromTag(xmlDocPtr doc, xmlNodePtr node); + void AddLinkItem(cOsdItem *item, cLinkBase *ref, cLinkBase *streamref); + void NewLinkItem(xmlDocPtr doc, xmlNodePtr node); + void NewTextField(xmlDocPtr doc, xmlNodePtr node); + void NewItemList(xmlDocPtr doc, xmlNodePtr node); + void NewTextArea(xmlDocPtr doc, xmlNodePtr node); + void NewButton(xmlDocPtr doc, xmlNodePtr node); + void NewTitle(xmlDocPtr doc, xmlNodePtr node); + void UpdateHelp(); + +public: + cNavigationMenu(cHistory *History, cProgressVector& dlsummaries); + virtual ~cNavigationMenu(); + + virtual eOSState ProcessKey(eKeys Key); + virtual eOSState Select(cLinkBase *link, eLinkType type); + virtual void Clear(void); + eOSState HistoryBack(); + eOSState HistoryForward(); + + const char *Reference() const { return reference; } + void Populate(const cHistoryObject *page, const char *statusmsg=NULL); +}; + +// --- cStatusScreen ------------------------------------------------------- + +class cStatusScreen : public cOsdMenu { +public: + const static time_t updateInterval = 5; // seconds +private: + cProgressVector& summaries; + time_t lastupdate; + +protected: + void UpdateHelp(); + +public: + cStatusScreen(cProgressVector& dlsummaries); + ~cStatusScreen(); + + void Update(); + bool NeedsUpdate(); + + virtual eOSState ProcessKey(eKeys Key); +}; + +// --- MenuPointers -------------------------------------------------------- + +struct MenuPointers { + cNavigationMenu *navigationMenu; + cStatusScreen *statusScreen; + + MenuPointers() : navigationMenu(NULL), statusScreen(NULL) {}; +}; + +extern struct MenuPointers menuPointers; + +#endif // __WEBVIDEO_MENU_H diff --git a/src/vdr-plugin/menu_timer.c b/src/vdr-plugin/menu_timer.c new file mode 100644 index 0000000..0501e0d --- /dev/null +++ b/src/vdr-plugin/menu_timer.c @@ -0,0 +1,150 @@ +/* + * menu.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <time.h> +#include <vdr/i18n.h> +#include <vdr/tools.h> +#include <vdr/menuitems.h> +#include <vdr/interface.h> +#include "menu_timer.h" + +#define ARRAYSIZE(a) sizeof(a)/sizeof(a[0]) + +/* +const char *intervalNames[] = {trNOOP("Once per day"), trNOOP("Once per week"), + trNOOP("Once per month")}; +*/ + +const char *intervalNames[] = {NULL, NULL, NULL}; +const int intervalValues[] = {24*60*60, 7*24*60*60, 30*24*60*60}; + +// --- cEditWebviTimerMenu ------------------------------------------------- + +cEditWebviTimerMenu::cEditWebviTimerMenu(cWebviTimer &timer, + bool refreshWhenDone, + bool execButton) + : cOsdMenu(tr("Edit timer"), 20), timer(timer), interval(1), + refresh(refreshWhenDone) +{ + // title + strn0cpy(title, timer.GetTitle(), maxTitleLen); + Add(new cMenuEditStrItem(tr("Title"), title, maxTitleLen)); + + // interval + for (unsigned i=0; i<ARRAYSIZE(intervalValues); i++) { + if (timer.GetInterval() == intervalValues[i]) { + interval = i; + break; + } + } + + if (!intervalNames[0]) { + // Initialize manually to make the translations work + intervalNames[0] = tr("Once per day"); + intervalNames[1] = tr("Once per week"); + intervalNames[2] = tr("Once per month"); + } + + Add(new cMenuEditStraItem(tr("Update interval"), &interval, + ARRAYSIZE(intervalNames), intervalNames)); + + // "execute now" button + if (execButton) + Add(new cOsdItem(tr("Execute now"), osUser1, true)); + + // last update time + char lastTime[25]; + if (timer.LastUpdate() == 0) { + // TRANSLATORS: at most 24 chars + strcpy(lastTime, tr("Never")); + } else { + time_t updateTime = timer.LastUpdate(); + strftime(lastTime, 25, "%x %X", localtime(&updateTime)); + } + + cString lastUpdated = cString::sprintf("%s\t%s", tr("Last fetched:"), lastTime); + Add(new cOsdItem(lastUpdated, osUnknown, false)); + + // error + if (!timer.Success()) { + Add(new cOsdItem(tr("Error on last refresh!"), osUnknown, false)); + Add(new cOsdItem(timer.LastError(), osUnknown, false)); + } +} + +cEditWebviTimerMenu::~cEditWebviTimerMenu() { + if (refresh) + timer.Execute(); +} + +eOSState cEditWebviTimerMenu::ProcessKey(eKeys Key) { + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osContinue) { + timer.SetTitle(title); + timer.SetInterval(intervalValues[interval]); + } else if (state == osUser1) { + timer.Execute(); + Skins.Message(mtInfo, tr("Downloading in the background")); + } + + return state; +} + +// --- cWebviTimerListMenu ------------------------------------------------- + +cWebviTimerListMenu::cWebviTimerListMenu(cWebviTimerManager &timers) + : cOsdMenu(tr("Timers")), timers(timers) +{ + cWebviTimer *t = timers.First(); + while (t) { + Add(new cOsdItem(t->GetTitle(), osUnknown, true)); + t = timers.Next(t); + } + + SetHelp(NULL, NULL, tr("Remove"), NULL); +} + +eOSState cWebviTimerListMenu::ProcessKey(eKeys Key) { + cWebviTimer *t; + eOSState state = cOsdMenu::ProcessKey(Key); + + if (HasSubMenu()) + return state; + + if (state == osUnknown) { + switch (Key) { + case kOk: + t = timers.GetLinear(Current()); + if (t) + return AddSubMenu(new cEditWebviTimerMenu(*t)); + break; + + case kYellow: + t = timers.GetLinear(Current()); + if (t) { + if (t->Running()) { + // FIXME: ask if the user wants to cancel the downloads + Skins.Message(mtInfo, tr("Timer running, can't remove")); + } else if (Interface->Confirm(tr("Remove timer?"))) { + timers.Remove(t); + Del(Current()); + Display(); + } + + return osContinue; + } + break; + + default: + break; + } + } + + return state; +} diff --git a/src/vdr-plugin/menu_timer.h b/src/vdr-plugin/menu_timer.h new file mode 100644 index 0000000..192c062 --- /dev/null +++ b/src/vdr-plugin/menu_timer.h @@ -0,0 +1,46 @@ +/* + * menu_timer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENU_TIMER_H +#define __WEBVIDEO_MENU_TIMER_H + +#include <vdr/osdbase.h> +#include "timer.h" + +// --- cEditWebviTimerMenu ------------------------------------------------- + +class cEditWebviTimerMenu : public cOsdMenu { +private: + static const int maxTitleLen = 128; + + cWebviTimer &timer; + char title[maxTitleLen]; + int interval; + bool refresh; + +public: + cEditWebviTimerMenu(cWebviTimer &timer, bool refreshWhenDone=false, + bool execButton=true); + ~cEditWebviTimerMenu(); + + virtual eOSState ProcessKey(eKeys Key); +}; + +// --- cWebviTimerListMenu ------------------------------------------------- + +class cWebviTimerListMenu : public cOsdMenu { +private: + cWebviTimerManager& timers; + +public: + cWebviTimerListMenu(cWebviTimerManager &timers); + + virtual eOSState ProcessKey(eKeys Key); +}; + +#endif diff --git a/src/vdr-plugin/menudata.c b/src/vdr-plugin/menudata.c new file mode 100644 index 0000000..45db133 --- /dev/null +++ b/src/vdr-plugin/menudata.c @@ -0,0 +1,179 @@ +/* + * menudata.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <vdr/tools.h> +#include "menudata.h" +#include "common.h" +#include "history.h" + +// --- cQueryData ---------------------------------------------------------- + +cQueryData::cQueryData(const char *Name) { + name = Name ? strdup(Name) : NULL; +} + +cQueryData::~cQueryData() { + if (name) + free(name); +} + +// --- cSimpleLink --------------------------------------------------------- + +cSimpleLink::cSimpleLink(const char *reference) { + ref = reference ? strdup(reference) : NULL; +} + +cSimpleLink::~cSimpleLink() { + if (ref) { + free(ref); + } +} + +char *cSimpleLink::GetURL() { + return ref; +} + +// --- cTextFieldData ------------------------------------------------------ + +cTextFieldData::cTextFieldData(const char *Name, int Length) +: cQueryData(Name) +{ + valuebufferlength = Length; + valuebuffer = (char *)malloc(Length*sizeof(char)); + *valuebuffer = '\0'; +} + +cTextFieldData::~cTextFieldData() { + if(valuebuffer) + free(valuebuffer); +} + +char *cTextFieldData::GetQueryFragment() { + const char *name = GetName(); + + if (name && *name && valuebuffer) { + char *encoded = URLencode(valuebuffer); + cString tmp = cString::sprintf("%s,%s", name, encoded); + free(encoded); + return strdup(tmp); + } + + return NULL; +} + +char *cTextFieldData::GetValue() { + return valuebuffer; +} + +int cTextFieldData::GetLength() { + return valuebufferlength; +} + +// --- cItemListData ------------------------------------------------------- + +cItemListData::cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings) +: cQueryData(Name) +{ + strings = Strings; + stringvalues = StringValues; + numstrings = NumStrings; + value = 0; +} + +cItemListData::~cItemListData() { + for (int i=0; i < numstrings; i++) { + free(strings[i]); + free(stringvalues[i]); + } + if (strings) + free(strings); + if (stringvalues) + free(stringvalues); +} + +char *cItemListData::GetQueryFragment() { + const char *name = GetName(); + + if (name && *name) { + cString tmp = cString::sprintf("%s,%s", name, stringvalues[value]); + return strdup(tmp); + } + + return NULL; +} + +char **cItemListData::GetStrings() { + return strings; +} + +char **cItemListData::GetStringValues() { + return stringvalues; +} + +int cItemListData::GetNumStrings() { + return numstrings; +} + +int *cItemListData::GetValuePtr() { + return &value; +} + +// --- cSubmissionButtonData ----------------------------------------------- + +cSubmissionButtonData::cSubmissionButtonData( + const char *queryUrl, const cHistoryObject *currentPage) +{ + querybase = queryUrl ? strdup(queryUrl) : NULL; + page = currentPage; +} + +cSubmissionButtonData::~cSubmissionButtonData() { + if (querybase) + free(querybase); + // do not free page +} + +char *cSubmissionButtonData::GetURL() { + if (!querybase) + return NULL; + + char *querystr = (char *)malloc(sizeof(char)*(strlen(querybase)+2)); + strcpy(querystr, querybase); + + if (!page) + return querystr; + + if (strchr(querystr, '?')) + strcat(querystr, "&"); + else + strcat(querystr, "?"); + + int numparameters = 0; + for (int i=0; i<page->QuerySize(); i++) { + char *parameter = page->GetQueryFragment(i); + if (parameter) { + querystr = (char *)realloc(querystr, (strlen(querystr)+strlen(parameter)+8)*sizeof(char)); + if (i > 0) + strcat(querystr, "&"); + strcat(querystr, "subst="); + strcat(querystr, parameter); + numparameters++; + + free(parameter); + } + } + + if (numparameters == 0) { + // remove the '?' or '&' because no parameters were added to the url + querystr[strlen(querystr)-1] = '\0'; + } + + return querystr; +} diff --git a/src/vdr-plugin/menudata.h b/src/vdr-plugin/menudata.h new file mode 100644 index 0000000..23a126c --- /dev/null +++ b/src/vdr-plugin/menudata.h @@ -0,0 +1,100 @@ +/* + * menudata.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MENUDATA_H +#define __WEBVIDEO_MENUDATA_H + +// --- cLinkBase ----------------------------------------------------------- + +class cLinkBase { +public: + virtual ~cLinkBase() {}; // avoids "virtual functions but + // non-virtual destructor" warning + + virtual char *GetURL() = 0; +}; + +// --- cQueryData ---------------------------------------------------------- + +class cQueryData { +private: + char *name; + +public: + cQueryData(const char *Name); + virtual ~cQueryData(); + + const char *GetName() { return name; } + virtual char *GetQueryFragment() = 0; +}; + +// --- cSimpleLink --------------------------------------------------------- + +class cSimpleLink : public cLinkBase { +private: + char *ref; +public: + cSimpleLink(const char *ref); + virtual ~cSimpleLink(); + + virtual char *GetURL(); +}; + +// --- cTextFieldData ------------------------------------------------------ + +class cTextFieldData : public cQueryData { +private: + char *name; + char *valuebuffer; + int valuebufferlength; +public: + cTextFieldData(const char *Name, int Length); + virtual ~cTextFieldData(); + + virtual char *GetQueryFragment(); + char *GetValue(); + int GetLength(); +}; + +// --- cItemListData ------------------------------------------------------- + +class cItemListData : public cQueryData { +private: + char *name; + int value; + int numstrings; + char **strings; + char **stringvalues; +public: + cItemListData(const char *Name, char **Strings, char **StringValues, int NumStrings); + virtual ~cItemListData(); + + virtual char *GetQueryFragment(); + char **GetStrings(); + char **GetStringValues(); + int GetNumStrings(); + int *GetValuePtr(); +}; + +// --- cSubmissionButtonData ----------------------------------------------- + +class cHistoryObject; + +class cSubmissionButtonData : public cLinkBase { +private: + char *querybase; + const cHistoryObject *page; +public: + cSubmissionButtonData(const char *queryUrl, + const cHistoryObject *currentPage); + virtual ~cSubmissionButtonData(); + + virtual char *GetURL(); +}; + +#endif diff --git a/src/vdr-plugin/mime.types b/src/vdr-plugin/mime.types new file mode 100644 index 0000000..beefdc3 --- /dev/null +++ b/src/vdr-plugin/mime.types @@ -0,0 +1,4 @@ +# Some non-standard, but common, MIME types + +video/flv flv +video/x-flv flv diff --git a/src/vdr-plugin/mimetypes.c b/src/vdr-plugin/mimetypes.c new file mode 100644 index 0000000..17c29e6 --- /dev/null +++ b/src/vdr-plugin/mimetypes.c @@ -0,0 +1,98 @@ +/* + * mimetypes.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <vdr/tools.h> +#include "mimetypes.h" +#include "common.h" + +// --- cMimeListObject ----------------------------------------------------- + +cMimeListObject::cMimeListObject(const char *mimetype, const char *extension) { + type = strdup(mimetype); + ext = strdup(extension); +} + +cMimeListObject::~cMimeListObject() { + free(type); + free(ext); +} + +// --- cMimeTypes ---------------------------------------------------------- + +cMimeTypes::cMimeTypes(const char **mimetypefiles) { + for (const char **filename=mimetypefiles; *filename; filename++) { + FILE *f = fopen(*filename, "r"); + if (!f) { + LOG_ERROR_STR((const char *)cString::sprintf("failed to open mime type file %s", *filename)); + continue; + } + + cReadLine rl; + char *line = rl.Read(f); + while (line) { + // Comment lines starting with '#' and empty lines are skipped + // Expected format for the lines: + // mime/type ext + if (*line && (*line != '#')) { + char *ptr = line; + while ((*ptr != '\0') && (!isspace(*ptr))) + ptr++; + + if (ptr == line) { + // empty line, ignore + line = rl.Read(f); + continue; + } + + char *mimetype = (char *)malloc(ptr-line+1); + strncpy(mimetype, line, ptr-line); + mimetype[ptr-line] = '\0'; + + while (*ptr && isspace(*ptr)) + ptr++; + char *eptr = ptr; + while (*ptr && !isspace(*ptr)) + ptr++; + + if (ptr == eptr) { + // no extension, ignore + free(mimetype); + line = rl.Read(f); + continue; + } + + char *extension = (char *)malloc(ptr-eptr+1); + strncpy(extension, eptr, ptr-eptr); + extension[ptr-eptr] = '\0'; + + types.Add(new cMimeListObject(mimetype, extension)); + free(extension); + free(mimetype); + } + line = rl.Read(f); + } + + fclose(f); + } +} + +char *cMimeTypes::ExtensionFromMimeType(const char *mimetype) { + if (!mimetype) + return NULL; + + for (cMimeListObject *m = types.First(); m; m = types.Next(m)) + if (strcmp(m->GetType(), mimetype) == 0) { + return strdup(m->GetExtension()); + } + + return NULL; +} diff --git a/src/vdr-plugin/mimetypes.h b/src/vdr-plugin/mimetypes.h new file mode 100644 index 0000000..76e735b --- /dev/null +++ b/src/vdr-plugin/mimetypes.h @@ -0,0 +1,35 @@ +/* + * mimetypes.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_MIMETYPES_H +#define __WEBVIDEO_MIMETYPES_H + +class cMimeListObject : public cListObject { +private: + char *type; + char *ext; +public: + cMimeListObject(const char *mimetype, const char *extension); + ~cMimeListObject(); + + char *GetType() { return type; }; + char *GetExtension() { return ext; }; +}; + +class cMimeTypes { +private: + cList<cMimeListObject> types; +public: + cMimeTypes(const char **filenames); + + char *ExtensionFromMimeType(const char *mimetype); +}; + +extern cMimeTypes *MimeTypes; + +#endif diff --git a/src/vdr-plugin/player.c b/src/vdr-plugin/player.c new file mode 100644 index 0000000..42bd56e --- /dev/null +++ b/src/vdr-plugin/player.c @@ -0,0 +1,73 @@ +/* + * player.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <stdio.h> +#include <string.h> +#include <vdr/plugin.h> +#include "player.h" +#include "common.h" + +bool cXineliboutputPlayer::Launch(const char *url) { + debug("launching xinelib player, url = %s", url); + + /* + * xineliboutput plugin insists on percent encoding (certain + * characters in) the URL. A properly encoded URL will get broken if + * we let xineliboutput to encode it the second time. For example, + * current (Feb 2009) Youtube URLs are affected by this. We will + * decode the URL before passing it to xineliboutput to fix Youtube + * + * On the other hand, some URLs will get broken if the encoding is + * removed here. There simply isn't a way to make all URLs work + * because of the way xineliboutput handles the encoding. + */ + char *decoded = URLdecode(url); + debug("decoded = %s", decoded); + bool ret = cPluginManager::CallFirstService("MediaPlayer-1.0", (void *)decoded); + free(decoded); + return ret; +} + +bool cMPlayerPlayer::Launch(const char *url) { + /* + * This code for launching mplayer plugin is just for testing, and + * most likely does not work. + */ + + debug("launching MPlayer"); + warning("Support for MPlayer is experimental. Don't expect this to work!"); + + struct MPlayerServiceData + { + int result; + union + { + const char *filename; + } data; + }; + + const char* const tmpPlayListFileName = "/tmp/webvideo.m3u"; + FILE *f = fopen(tmpPlayListFileName, "w"); + fwrite(url, strlen(url), 1, f); + fclose(f); + + MPlayerServiceData mplayerdata; + mplayerdata.data.filename = tmpPlayListFileName; + + if (!cPluginManager::CallFirstService("MPlayer-Play-v1", &mplayerdata)) { + debug("Failed to locate Mplayer service"); + return false; + } + + if (!mplayerdata.result) { + debug("Mplayer service failed"); + return false; + } + + return true; +} diff --git a/src/vdr-plugin/player.h b/src/vdr-plugin/player.h new file mode 100644 index 0000000..dbaf448 --- /dev/null +++ b/src/vdr-plugin/player.h @@ -0,0 +1,29 @@ +/* + * menu.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_PLAYER_H +#define __WEBVIDEO_PLAYER_H + +class cMediaPlayer { +public: + virtual ~cMediaPlayer() {}; + virtual bool Launch(const char *url) = 0; +}; + +class cXineliboutputPlayer : public cMediaPlayer { +public: + bool Launch(const char *url); +}; + +class cMPlayerPlayer : public cMediaPlayer { +public: + bool Launch(const char *url); +}; + + +#endif diff --git a/src/vdr-plugin/po/de_DE.po b/src/vdr-plugin/po/de_DE.po new file mode 100644 index 0000000..f096ba9 --- /dev/null +++ b/src/vdr-plugin/po/de_DE.po @@ -0,0 +1,137 @@ +# German translations for webvideo package. +# Copyright (C) 2009 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Antti Ajanki <antti.ajanki@iki.fi>, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2009-02-18 20:04+0200\n" +"Last-Translator: <cnc@gmx.de>\n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Download im Hintergrund" + +msgid "Starting player..." +msgstr "Player wird gestartet..." + +msgid "Retrieving..." +msgstr "Abrufen..." + +msgid "Back" +msgstr "Zurück" + +msgid "Forward" +msgstr "Vor" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "Play" + +msgid "Status" +msgstr "Status" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "Nicht beendete Downloads" + +msgid "No active downloads" +msgstr "Kein aktiver Download" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +msgid "Download details" +msgstr "" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "Abbruch" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +#, fuzzy +msgid "Aborted" +msgstr "Abbruch" + +msgid "Download video files from the web" +msgstr "Download Video Files aus dem Web" + +msgid "Streaming failed: no URL" +msgstr "Streaming fehlgeschlagen: Keine URL" + +msgid "Failed to launch media player" +msgstr "Media Player konnte nicht gestartet werden" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Ein Download komplett, %d verbleibend%s" + +msgid "Download aborted" +msgstr "" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Download fehlgeschlagen (Error = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d laufende Downloads" diff --git a/src/vdr-plugin/po/fi_FI.po b/src/vdr-plugin/po/fi_FI.po new file mode 100644 index 0000000..6e6df2f --- /dev/null +++ b/src/vdr-plugin/po/fi_FI.po @@ -0,0 +1,137 @@ +# Finnish translations for webvideo package. +# Copyright (C) 2008,2009 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Antti Ajanki <antti.ajanki@iki.fi>, 2008,2009. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.1.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2008-06-07 18:03+0300\n" +"Last-Translator: Antti Ajanki <antti.ajanki@iki.fi>\n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "Luo ajastin vaikka tällä sivulla ei videoita?" + +msgid "Downloading in the background" +msgstr "Ladataan taustalla" + +msgid "Starting player..." +msgstr "Käynnistetään toistin..." + +msgid "Retrieving..." +msgstr "Ladataan..." + +msgid "Back" +msgstr "Peruuta" + +msgid "Forward" +msgstr "Eteenpäin" + +msgid "Create timer" +msgstr "Luo ajastin" + +msgid "Play" +msgstr "Toista" + +msgid "Status" +msgstr "Tila" + +msgid "Timers" +msgstr "Ajastimet" + +msgid "Unfinished downloads" +msgstr "Ladattavat tiedostot" + +msgid "No active downloads" +msgstr "Ei keskeneräisia latauksia" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "Virhe" + +msgid "Error details" +msgstr "Virhe" + +msgid "Download details" +msgstr "Latauksen tiedot" + +msgid "Remove" +msgstr "Poista" + +msgid "Abort" +msgstr "Keskeytä" + +msgid "Edit timer" +msgstr "Muokkaa ajastinta" + +msgid "Title" +msgstr "Nimi" + +msgid "Once per day" +msgstr "Kerran päivässä" + +msgid "Once per week" +msgstr "Kerran viikossa" + +msgid "Once per month" +msgstr "Kerran kuussa" + +msgid "Update interval" +msgstr "Päivitystahti" + +msgid "Execute now" +msgstr "Suorita nyt" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "Ei koskaan" + +msgid "Last fetched:" +msgstr "Viimeisin päivitys" + +msgid "Error on last refresh!" +msgstr "Virhe edellisessä päivityksessä" + +msgid "Timer running, can't remove" +msgstr "Poisto ei onnistu, koska ajastin on käynnissä" + +msgid "Remove timer?" +msgstr "Poista ajastin?" + +msgid "Aborted" +msgstr "Keskeytetty" + +msgid "Download video files from the web" +msgstr "Lataa videotiedostoja Internetistä" + +msgid "Streaming failed: no URL" +msgstr "Toisto epäonnistui: ei URLia" + +msgid "Failed to launch media player" +msgstr "Toistimen käynnistäminen epäonnistui" + +msgid "timer" +msgstr "ajastin" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Yksi tiedosto ladattu, %d jäljellä%s" + +msgid "Download aborted" +msgstr "Lataaminen keskeytetty" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Lataus epäonnistui (virhe = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d tiedostoa lataamatta" diff --git a/src/vdr-plugin/po/fr_FR.po b/src/vdr-plugin/po/fr_FR.po new file mode 100644 index 0000000..79f31b5 --- /dev/null +++ b/src/vdr-plugin/po/fr_FR.po @@ -0,0 +1,156 @@ +# French translations for webvideo package. +# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Bruno ROUSSEL <bruno.roussel@free.fr>, 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.5\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2008-09-08 20:34+0100\n" +"Last-Translator: Bruno ROUSSEL <bruno.roussel@free.fr>\n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Téléchargement en tâche de fond" + +msgid "Starting player..." +msgstr "" + +msgid "Retrieving..." +msgstr "Récupération..." + +msgid "Back" +msgstr "Arrière" + +msgid "Forward" +msgstr "Avant" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "" + +msgid "Status" +msgstr "Status" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "" + +msgid "No active downloads" +msgstr "" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +#, fuzzy +msgid "Download details" +msgstr "Status du téléchargement" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +msgid "Aborted" +msgstr "" + +msgid "Download video files from the web" +msgstr "Téléchargement du fichier vidéo depuis le web" + +msgid "Streaming failed: no URL" +msgstr "" + +msgid "Failed to launch media player" +msgstr "" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Un téléchargement terminé, il en reste %d%s" + +msgid "Download aborted" +msgstr "" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Erreur de téléchargement (Erreur = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d téléchargement(s) non terminé(s)." + +#~ msgid "<No title>" +#~ msgstr "<Pas de titre>" + +#~ msgid "Can't download web page!" +#~ msgstr "Impossible de télécharger la page web !" + +#~ msgid "XSLT transformation produced no URL!" +#~ msgstr "La conversion XSLT n'a pas généré d'URL !" + +#~ msgid "XSLT transformation failed." +#~ msgstr "Erreur de conversion XSLT." + +#~ msgid "Unknown error!" +#~ msgstr "Erreur inconnue !" + +#~ msgid "Select video source" +#~ msgstr "Sélectionner la source vidéo" diff --git a/src/vdr-plugin/po/it_IT.po b/src/vdr-plugin/po/it_IT.po new file mode 100644 index 0000000..c7f1f00 --- /dev/null +++ b/src/vdr-plugin/po/it_IT.po @@ -0,0 +1,158 @@ +# Italian translations for webvideo package. +# Copyright (C) 2008 THE webvideo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the webvideo package. +# Diego Pierotto <vdr-italian@tiscali.it>, 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: webvideo 0.0.1\n" +"Report-Msgid-Bugs-To: <see README>\n" +"POT-Creation-Date: 2010-07-09 15:12+0300\n" +"PO-Revision-Date: 2009-04-11 01:48+0100\n" +"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Poedit-Language: Italian\n" +"X-Poedit-Country: ITALY\n" +"X-Poedit-SourceCharset: utf-8\n" + +msgid "No streams on this page, create timer anyway?" +msgstr "" + +msgid "Downloading in the background" +msgstr "Scaricamento in sottofondo" + +msgid "Starting player..." +msgstr "Avvio lettore..." + +msgid "Retrieving..." +msgstr "Recupero..." + +msgid "Back" +msgstr "Indietro" + +msgid "Forward" +msgstr "Avanti" + +msgid "Create timer" +msgstr "" + +msgid "Play" +msgstr "Riproduci" + +msgid "Status" +msgstr "Stato" + +msgid "Timers" +msgstr "" + +msgid "Unfinished downloads" +msgstr "Scaricamenti non completati" + +msgid "No active downloads" +msgstr "Nessun scaricamento attivo" + +#. TRANSLATORS: at most 5 characters +msgid "Error" +msgstr "" + +msgid "Error details" +msgstr "" + +#, fuzzy +msgid "Download details" +msgstr "Richiesta scaricamento fallita!" + +msgid "Remove" +msgstr "" + +msgid "Abort" +msgstr "Annulla" + +msgid "Edit timer" +msgstr "" + +msgid "Title" +msgstr "" + +msgid "Once per day" +msgstr "" + +msgid "Once per week" +msgstr "" + +msgid "Once per month" +msgstr "" + +msgid "Update interval" +msgstr "" + +msgid "Execute now" +msgstr "" + +#. TRANSLATORS: at most 24 chars +msgid "Never" +msgstr "" + +msgid "Last fetched:" +msgstr "" + +msgid "Error on last refresh!" +msgstr "" + +msgid "Timer running, can't remove" +msgstr "" + +msgid "Remove timer?" +msgstr "" + +msgid "Aborted" +msgstr "Annullato" + +msgid "Download video files from the web" +msgstr "Scarica file video dal web" + +msgid "Streaming failed: no URL" +msgstr "Trasmissione fallita: nessun URL" + +msgid "Failed to launch media player" +msgstr "Impossibile avviare il lettore multimediale" + +msgid "timer" +msgstr "" + +#, c-format +msgid "One download completed, %d remains%s" +msgstr "Scaricamento completato, %d rimanente/i%s" + +msgid "Download aborted" +msgstr "Scaricamento annullato" + +#, c-format +msgid "Download failed (error = %d)" +msgstr "Scaricamento fallito (errore = %d)" + +#, c-format +msgid "%d downloads not finished" +msgstr "%d scaricamenti non conclusi" + +#~ msgid "<No title>" +#~ msgstr "<Senza titolo>" + +#~ msgid "Can't download web page!" +#~ msgstr "Impossibile scaricare la pagina web!" + +#~ msgid "XSLT transformation produced no URL!" +#~ msgstr "La conversione XSLT non ha generato alcun URL!" + +#~ msgid "XSLT transformation failed." +#~ msgstr "Conversione XSLT fallita." + +#~ msgid "Unknown error!" +#~ msgstr "Errore sconosciuto!" + +#~ msgid "Select video source" +#~ msgstr "Seleziona fonte video" diff --git a/src/vdr-plugin/request.c b/src/vdr-plugin/request.c new file mode 100644 index 0000000..edc5432 --- /dev/null +++ b/src/vdr-plugin/request.c @@ -0,0 +1,432 @@ +/* + * request.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <vdr/tools.h> +#include <vdr/i18n.h> +#include "request.h" +#include "common.h" +#include "mimetypes.h" +#include "config.h" +#include "timer.h" + +// --- cDownloadProgress --------------------------------------------------- + +cDownloadProgress::cDownloadProgress() { + strcpy(name, "???"); + downloaded = -1; + total = -1; + statusCode = -1; + req = NULL; +} + +void cDownloadProgress::AssociateWith(cFileDownloadRequest *request) { + req = request; +} + +void cDownloadProgress::SetContentLength(long bytes) { + total = bytes; +} + +void cDownloadProgress::SetTitle(const char *title) { + cMutexLock lock(&mutex); + + strncpy(name, title, NAME_LEN-1); + name[NAME_LEN-1] = '\0'; +} + +void cDownloadProgress::Progress(long downloadedbytes) { + // Atomic operation, no mutex needed + downloaded = downloadedbytes; +} + +void cDownloadProgress::MarkDone(int errorcode, cString pharse) { + cMutexLock lock(&mutex); + + statusCode = errorcode; + statusPharse = pharse; +} + +bool cDownloadProgress::IsFinished() { + return statusCode != -1; +} + +cString cDownloadProgress::GetTitle() { + cMutexLock lock(&mutex); + + if (req && req->IsAborted()) + return cString::sprintf("[%s] %s", tr("Aborted"), name); + else + return cString(name); +} + +cString cDownloadProgress::GetPercentage() { + cMutexLock lock(&mutex); + + if ((const char*)statusPharse != NULL && statusCode != 0) + // TRANSLATORS: at most 5 characters + return cString(tr("Error")); + else if ((downloaded < 0) || (total < 0)) + return cString("???"); + else + return cString::sprintf("%3d%%", (int) (100*(float)downloaded/total + 0.5)); +} + +cString cDownloadProgress::GetStatusPharse() { + cMutexLock lock(&mutex); + + return statusPharse; +} + +bool cDownloadProgress::Error() { + return (const char *)statusPharse != NULL; +} + +// --- cProgressVector ----------------------------------------------------- + +cDownloadProgress *cProgressVector::NewDownload() { + cDownloadProgress *progress = new cDownloadProgress(); + Append(progress); + return progress; +} + +// --- cMenuRequest -------------------------------------------------------- + +cMenuRequest::cMenuRequest(int ID, const char *wvtreference) +: reqID(ID), aborted(false), finished(false), status(0), webvi(-1), + handle(-1), timer(NULL) +{ + wvtref = strdup(wvtreference); +} + +cMenuRequest::~cMenuRequest() { + if (handle != -1) { + if (!finished) + Abort(); + webvi_delete_handle(webvi, handle); + } + + // do not delete timer +} + +ssize_t cMenuRequest::WriteCallback(const char *ptr, size_t len, void *request) { + cMenuRequest *instance = (cMenuRequest *)request; + if (instance) + return instance->WriteData(ptr, len); + else + return len; +} + +ssize_t cMenuRequest::WriteData(const char *ptr, size_t len) { + return inBuffer.Put(ptr, len); +} + +char *cMenuRequest::ExtractSiteName(const char *ref) { + if (strncmp(ref, "wvt:///", 7) != 0) + return NULL; + + const char *first = ref+7; + const char *last = strchr(first, '/'); + if (!last) + last = first+strlen(first); + + return strndup(first, last-first); +} + +void cMenuRequest::AppendQualityParamsToRef() { + if (!wvtref) + return; + + char *site = ExtractSiteName(wvtref); + if (site) { + const char *min = webvideoConfig->GetMinQuality(site, GetType()); + const char *max = webvideoConfig->GetMaxQuality(site, GetType()); + free(site); + + if (min && !max) { + cString newref = cString::sprintf("%s&minquality=%s", wvtref, min); + free(wvtref); + wvtref = strdup((const char *)newref); + + } else if (!min && max) { + cString newref = cString::sprintf("%s&maxquality=%s", wvtref, max); + free(wvtref); + wvtref = strdup((const char *)newref); + + } else if (min && max) { + cString newref = cString::sprintf("%s&minquality=%s&maxquality=%s", wvtref, min, max); + free(wvtref); + wvtref = strdup((const char *)newref); + } + } +} + +WebviHandle cMenuRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_MENU); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +bool cMenuRequest::Start(WebviCtx webvictx) { + webvi = webvictx; + + if ((PrepareHandle() != -1) && (webvi_start_handle(webvi, handle) == WEBVIERR_OK)) { + finished = false; + return true; + } else + return false; +} + +void cMenuRequest::RequestDone(int errorcode, cString pharse) { + finished = true; + status = errorcode; + statusPharse = pharse; +} + +void cMenuRequest::Abort() { + if (finished || handle == -1) + return; + + aborted = true; + webvi_stop_handle(webvi, handle); +}; + +bool cMenuRequest::Success() { + return status == 0; +} + +cString cMenuRequest::GetStatusPharse() { + return statusPharse; +} + +cString cMenuRequest::GetResponse() { + size_t len = inBuffer.Length(); + const char *src = inBuffer.Get(); + char *buf = (char *)malloc((len+1)*sizeof(char)); + strncpy(buf, src, len); + buf[len] = '\0'; + return cString(buf, true); +} + +// --- cFileDownloadRequest ------------------------------------------------ + +cFileDownloadRequest::cFileDownloadRequest(int ID, const char *streamref, + const char *destdir, + cDownloadProgress *progress) +: cMenuRequest(ID, streamref), title(NULL), bytesDownloaded(0), + contentLength(-1), destfile(NULL), progressUpdater(progress) +{ + this->destdir = strdup(destdir); + if (progressUpdater) + progressUpdater->AssociateWith(this); + + AppendQualityParamsToRef(); +} + +cFileDownloadRequest::~cFileDownloadRequest() { + if (destfile) { + destfile->Close(); + delete destfile; + } + if (destdir) + free(destdir); + if (title) + free(title); + // do not delete progressUpdater +} + +WebviHandle cFileDownloadRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_FILE); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +ssize_t cFileDownloadRequest::WriteData(const char *ptr, size_t len) { + if (!destfile) { + if (!OpenDestFile()) + return -1; + } + + bytesDownloaded += len; + if (progressUpdater) + progressUpdater->Progress(bytesDownloaded); + + return destfile->Write(ptr, len); +} + +bool cFileDownloadRequest::OpenDestFile() { + char *contentType; + char *url; + char *ext; + cString destfilename; + int fd, i; + + if (handle == -1) { + error("handle == -1 while trying to open destination file"); + return false; + } + + if (destfile) + delete destfile; + + destfile = new cUnbufferedFile; + + webvi_get_info(webvi, handle, WEBVIINFO_URL, &url); + webvi_get_info(webvi, handle, WEBVIINFO_STREAM_TITLE, &title); + webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_TYPE, &contentType); + webvi_get_info(webvi, handle, WEBVIINFO_CONTENT_LENGTH, &contentLength); + + if (!contentType || !url) { + if(contentType) + free(contentType); + if (url) + free(url); + + error("no content type or url, can't infer extension"); + return false; + } + + ext = GetExtension(contentType, url); + + free(url); + free(contentType); + + char *basename = strdup(title ? title : "???"); + basename = safeFilename(basename); + + i = 1; + destfilename = cString::sprintf("%s/%s%s", destdir, basename, ext); + while (true) { + debug("trying to open %s", (const char *)destfilename); + + fd = destfile->Open(destfilename, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE); + + if (fd == -1 && errno == EEXIST) + destfilename = cString::sprintf("%s/%s-%d%s", destdir, basename, i++, ext); + else + break; + }; + + free(basename); + free(ext); + + if (fd < 0) { + error("Failed to open file %s: %m", (const char *)destfilename); + delete destfile; + destfile = NULL; + return false; + } + + info("Saving to %s", (const char *)destfilename); + + if (progressUpdater) { + progressUpdater->SetTitle(title); + progressUpdater->SetContentLength(contentLength); + } + + return true; +} + +char *cFileDownloadRequest::GetExtension(const char *contentType, const char *url) { + // Get extension from Content-Type + char *ext = NULL; + char *ext2 = MimeTypes->ExtensionFromMimeType(contentType); + + // Workaround for buggy servers: If the server claims that the mime + // type is text/plain, ignore the server and fall back to extracting + // the extension from the URL. This function should be called only + // for video, audio or ASX files and therefore text/plain is clearly + // incorrect. + if (ext2 && contentType && !strcasecmp(contentType, "text/plain")) { + debug("Ignoring content type text/plain, getting extension from url."); + free(ext2); + ext2 = NULL; + } + + if (ext2) { + // Append dot in the start of the extension + ext = (char *)malloc(strlen(ext2)+2); + ext[0] = '.'; + ext[1] = '\0'; + strcat(ext, ext2); + free(ext2); + return ext; + } + + // Get extension from URL + ext = extensionFromUrl(url); + if (ext) + return ext; + + // No extension! + return strdup(""); +} + +void cFileDownloadRequest::RequestDone(int errorcode, cString pharse) { + cMenuRequest::RequestDone(errorcode, pharse); + if (progressUpdater) + progressUpdater->MarkDone(errorcode, pharse); + if (destfile) + destfile->Close(); +} + +// --- cStreamUrlRequest --------------------------------------------------- + +cStreamUrlRequest::cStreamUrlRequest(int ID, const char *ref) +: cMenuRequest(ID, ref) { + AppendQualityParamsToRef(); +} + +WebviHandle cStreamUrlRequest::PrepareHandle() { + if (handle == -1) { + handle = webvi_new_request(webvi, wvtref, WEBVIREQ_STREAMURL); + + if (handle != -1) { + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEFUNC, WriteCallback); + webvi_set_opt(webvi, handle, WEBVIOPT_WRITEDATA, this); + } + } + + return handle; +} + +// --- cTimerRequest ------------------------------------------------------- + +cTimerRequest::cTimerRequest(int ID, const char *ref) +: cMenuRequest(ID, ref) +{ +} + +// --- cRequestVector ------------------------------------------------------ + +cMenuRequest *cRequestVector::FindByHandle(WebviHandle handle) { + for (int i=0; i<Size(); i++) + if (At(i)->GetHandle() == handle) + return At(i); + + return NULL; +} diff --git a/src/vdr-plugin/request.h b/src/vdr-plugin/request.h new file mode 100644 index 0000000..f481fc8 --- /dev/null +++ b/src/vdr-plugin/request.h @@ -0,0 +1,170 @@ +/* + * request.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_REQUEST_H +#define __WEBVIDEO_REQUEST_H + +#include <vdr/tools.h> +#include <vdr/thread.h> +#include <libwebvi.h> +#include "buffer.h" + +enum eRequestType { REQT_NONE, REQT_MENU, REQT_FILE, REQT_STREAM, REQT_TIMER }; + +class cFileDownloadRequest; +class cWebviTimer; + +// --- cDownloadProgress --------------------------------------------------- + +class cDownloadProgress { +private: + const static int NAME_LEN = 128; + + char name[NAME_LEN]; + long downloaded; + long total; + int statusCode; + cString statusPharse; + cFileDownloadRequest *req; + cMutex mutex; +public: + cDownloadProgress(); + + void AssociateWith(cFileDownloadRequest *request); + void SetContentLength(long bytes); + void SetTitle(const char *title); + void Progress(long downloadedbytes); + void MarkDone(int errorcode, cString pharse); + bool IsFinished(); + + cString GetTitle(); + cString GetPercentage(); + cString GetStatusPharse(); + bool Error(); + cFileDownloadRequest *GetRequest() { return req; } +}; + +// --- cProgressVector ----------------------------------------------------- + +class cProgressVector : public cVector<cDownloadProgress *> { +public: + cDownloadProgress *NewDownload(); +}; + +// --- cMenuRequest ---------------------------------------------------- + +class cMenuRequest { +private: + int reqID; + bool aborted; + bool finished; + int status; + cString statusPharse; + +protected: + WebviCtx webvi; + WebviHandle handle; + char *wvtref; + cMemoryBuffer inBuffer; + cWebviTimer *timer; + + virtual ssize_t WriteData(const char *ptr, size_t len); + virtual WebviHandle PrepareHandle(); + static ssize_t WriteCallback(const char *ptr, size_t len, void *request); + + char *ExtractSiteName(const char *ref); + void AppendQualityParamsToRef(); + +public: + cMenuRequest(int ID, const char *wvtreference); + virtual ~cMenuRequest(); + + int GetID() { return reqID; } + WebviHandle GetHandle() { return handle; } + const char *GetReference() { return wvtref; } + + bool Start(WebviCtx webvictx); + virtual void RequestDone(int errorcode, cString pharse); + bool IsFinished() { return finished; } + void Abort(); + bool IsAborted() { return aborted; } + + // Return true if the lastest status code indicates success. + bool Success(); + // Return the status code + int GetStatusCode() { return status; } + // Return the response pharse + cString GetStatusPharse(); + + virtual eRequestType GetType() { return REQT_MENU; } + + // Return the content of the reponse message + virtual cString GetResponse(); + + void SetTimer(cWebviTimer *t) { timer = t; } + cWebviTimer *GetTimer() { return timer; } +}; + +// --- cFileDownloadRequest ------------------------------------------------ + +class cFileDownloadRequest : public cMenuRequest { +private: + char *destdir; + char *title; + long bytesDownloaded; + long contentLength; + cUnbufferedFile *destfile; + cDownloadProgress *progressUpdater; + +protected: + virtual WebviHandle PrepareHandle(); + virtual ssize_t WriteData(const char *ptr, size_t len); + bool OpenDestFile(); + char *GetExtension(const char *contentType, const char *url); + +public: + cFileDownloadRequest(int ID, const char *streamref, + const char *destdir, + cDownloadProgress *progress); + virtual ~cFileDownloadRequest(); + + eRequestType GetType() { return REQT_FILE; } + void RequestDone(int errorcode, cString pharse); +}; + +// --- cStreamUrlRequest --------------------------------------------------- + +class cStreamUrlRequest : public cMenuRequest { +protected: + virtual WebviHandle PrepareHandle(); + +public: + cStreamUrlRequest(int ID, const char *ref); + + eRequestType GetType() { return REQT_STREAM; } +}; + +// --- cTimerRequest ------------------------------------------------------- + +class cTimerRequest : public cMenuRequest { +public: + cTimerRequest(int ID, const char *ref); + + eRequestType GetType() { return REQT_TIMER; } +}; + +// --- cRequestVector ------------------------------------------------------ + +class cRequestVector : public cVector<cMenuRequest *> { +public: + cRequestVector(int Allocated = 10) : cVector<cMenuRequest *>(Allocated) {} + + cMenuRequest *FindByHandle(WebviHandle handle); +}; + +#endif diff --git a/src/vdr-plugin/timer.c b/src/vdr-plugin/timer.c new file mode 100644 index 0000000..f9fef59 --- /dev/null +++ b/src/vdr-plugin/timer.c @@ -0,0 +1,465 @@ +/* + * timer.c: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <string.h> +#include <errno.h> +#include <libxml/parser.h> +#include "timer.h" +#include "request.h" +#include "common.h" +#include "download.h" +#include "config.h" + +// --- cWebviTimer ----------------------------------------------- + +cWebviTimer::cWebviTimer(int ID, const char *title, + const char *ref, cWebviTimerManager *manager, + time_t last, int interval, bool success, + const char *errmsg) + : id(ID), title(title ? strdup(title) : strdup("???")), + reference(ref ? strdup(ref) : NULL), lastUpdate(last), + interval(interval), running(false), lastSucceeded(success), + lastError(errmsg ? strdup(errmsg) : NULL), + parent(manager) +{ +} + +cWebviTimer::~cWebviTimer() { + if(title) + free(title); + if (reference) + free(reference); + if (lastError) + free(lastError); +} + +void cWebviTimer::SetTitle(const char *newTitle) { + if (title) + free(title); + title = newTitle ? strdup(newTitle) : strdup("???"); + + parent->SetModified(); +} + +void cWebviTimer::SetInterval(int interval) { + if (interval < MIN_TIMER_INTERVAL) + this->interval = MIN_TIMER_INTERVAL; + else + this->interval = interval; + + parent->SetModified(); +} + +int cWebviTimer::GetInterval() const { + return interval; +} + +time_t cWebviTimer::NextUpdate() const { + int delta = interval; + + // Retry again soon if the last try failed + if (!lastSucceeded && delta > RETRY_TIMER_INTERVAL) + delta = RETRY_TIMER_INTERVAL; + + return lastUpdate + delta; +} + +void cWebviTimer::Execute() { + if (running) { + debug("previous instance of this timer is still running"); + return; + } + + info("Executing timer \"%s\"", title); + + running = true; + cTimerRequest *req = new cTimerRequest(id, reference); + req->SetTimer(this); + cWebviThread::Instance().AddRequest(req); + + lastUpdate = time(NULL); + SetError("Unfinished"); + parent->SetModified(); + + activeStreams.Clear(); +} + +void cWebviTimer::SetError(const char *errmsg) { + bool oldSuccess = lastSucceeded; + + if (lastError) + free(lastError); + lastError = NULL; + + if (errmsg) { + lastSucceeded = false; + lastError = strdup(errmsg); + } else { + lastSucceeded = true; + } + + if (oldSuccess != lastSucceeded) + parent->SetModified(); +} + +const char *cWebviTimer::LastError() const { + return lastError ? lastError : ""; +} + +void cWebviTimer::DownloadStreams(const char *menuxml, cProgressVector& summaries) { + if (!menuxml) { + SetError("xml == NULL"); + return; + } + + xmlDocPtr doc = xmlParseMemory(menuxml, strlen(menuxml)); + if (!doc) { + xmlErrorPtr xmlerr = xmlGetLastError(); + if (xmlerr) + error("libxml error: %s", xmlerr->message); + SetError(xmlerr->message); + return; + } + + xmlNodePtr node = xmlDocGetRootElement(doc); + if (node) + node = node->xmlChildrenNode; + + while (node) { + if (!xmlStrcmp(node->name, BAD_CAST "link")) { + xmlNodePtr node2 = node->children; + + while(node2) { + if (!xmlStrcmp(node2->name, BAD_CAST "stream")) { + xmlChar *streamref = xmlNodeListGetString(doc, node2->xmlChildrenNode, 1); + const char *ref = (const char *)streamref; + + if (parent->AlreadyDownloaded(ref)) { + debug("timer: %s has already been downloaded", ref); + } else if (*ref) { + info("timer: downloading %s", ref); + + activeStreams.Append(strdup(ref)); + cFileDownloadRequest *req = \ + new cFileDownloadRequest(REQ_ID_TIMER, ref, + webvideoConfig->GetDownloadPath(), + summaries.NewDownload()); + req->SetTimer(this); + cWebviThread::Instance().AddRequest(req); + } + + xmlFree(streamref); + } + + node2 = node2->next; + } + } + + node = node->next; + } + + xmlFreeDoc(doc); + + if (activeStreams.Size() == 0) { + SetError(NULL); + running = false; + } +} + +void cWebviTimer::CheckFailed(const char *errmsg) { + SetError(errmsg); + running = false; +} + +void cWebviTimer::RequestFinished(const char *ref, const char *errmsg) { + if (errmsg && !lastError) + SetError(errmsg); + + if (ref) { + if (parent) + parent->MarkDownloaded(ref); + + int i = activeStreams.Find(ref); + if (i != -1) { + free(activeStreams[i]); + activeStreams.Remove(i); + } + } + + if (activeStreams.Size() == 0) { + info("timer \"%s\" done", title); + running = false; + } else { + debug("timer %s is still downloading %d streams", reference, activeStreams.Size()); + } +} + +// --- cWebviTimerManager ---------------------------------------- + +cWebviTimerManager::cWebviTimerManager() + : nextID(1), modified(false), disableSaving(false) +{ +} + +cWebviTimerManager &cWebviTimerManager::Instance() { + static cWebviTimerManager instance; + + return instance; +} + +void cWebviTimerManager::LoadTimers(FILE *f) { + cReadLine rl; + long lastRefresh; + int interval; + int success; + char *ref; + const char *ver; + const char *title; + const char *errmsg; + int n, i; + + ver = rl.Read(f); + if (strcmp(ver, "# WVTIMER1") != 0) { + error("Can't load timers. Unknown format: %s", ver); + disableSaving = true; + return; + } + + i = 1; + while (true) { + n = fscanf(f, "%ld %d %d %ms", &lastRefresh, &interval, &success, &ref); + if (n != 4) { + if (n != EOF) { + error("Error while reading webvi timers file"); + } else if (ferror(f)) { + LOG_ERROR_STR("webvi timers file"); + } + + break; + } + + title = rl.Read(f); + title = title ? skipspace(title) : "???"; + errmsg = success ? NULL : ""; + + info("timer %d: title %s", i++, title); + debug(" ref %s, lastRefresh %ld, interval %d", ref, lastRefresh, interval); + + timers.Add(new cWebviTimer(nextID++, title, ref, this, + (time_t)lastRefresh, interval, + success, errmsg)); + + free(ref); + } +} + +void cWebviTimerManager::LoadHistory(FILE *f) { + cReadLine rl; + char *line; + + while ((line = rl.Read(f))) + refHistory.Append(strdup(line)); + + debug("loaded history: len = %d", refHistory.Size()); +} + +void cWebviTimerManager::SaveTimers(FILE *f) { + // Format: space separated field in this order: + // lastUpdate interval lastSucceeded reference title + + fprintf(f, "# WVTIMER1\n"); + + cWebviTimer *t = timers.First(); + while (t) { + if (fprintf(f, "%ld %d %d %s %s\n", + t->LastUpdate(), t->GetInterval(), t->Success(), + t->GetReference(), t->GetTitle()) < 0) { + error("Failed to save timer data!"); + } + + t = timers.Next(t); + } +} + +void cWebviTimerManager::SaveHistory(FILE *f) { + int size = refHistory.Size(); + int first; + + if (size <= MAX_TIMER_HISTORY_SIZE) + first = 0; + else + first = size - MAX_TIMER_HISTORY_SIZE; + + for (int i=first; i<size; i++) { + const char *ref = refHistory[i]; + if (fwrite(ref, strlen(ref), 1, f) != 1 || + fwrite("\n", 1, 1, f) != 1) { + error("Error while writing timer history"); + break; + } + } +} + +bool cWebviTimerManager::Load(const char *path) { + FILE *f; + bool ok = true; + + cString timersname = AddDirectory(path, "timers.dat"); + f = fopen(timersname, "r"); + if (f) { + debug("loading webvi timers from %s", (const char *)timersname); + LoadTimers(f); + fclose(f); + } else { + if (errno != ENOENT) + LOG_ERROR_STR("Can't load webvi timers"); + ok = false; + } + + cString historyname = AddDirectory(path, "timers.hst"); + f = fopen(historyname, "r"); + if (f) { + debug("loading webvi history from %s", (const char *)historyname); + LoadHistory(f); + fclose(f); + } else { + if (errno != ENOENT) + LOG_ERROR_STR("Can't load webvi timer history"); + ok = false; + } + + return ok; +} + +bool cWebviTimerManager::Save(const char *path) { + FILE *f; + bool ok = true; + + if (!modified) + return true; + if (disableSaving) { + error("Not saving timers because the file format is unknown."); + return false; + } + + cString timersname = AddDirectory(path, "timers.dat"); + f = fopen(timersname, "w"); + if (f) { + debug("saving webvi timers to %s", (const char *)timersname); + SaveTimers(f); + fclose(f); + } else { + LOG_ERROR_STR("Can't save webvi timers"); + ok = false; + } + + cString historyname = AddDirectory(path, "timers.hst"); + f = fopen(historyname, "w"); + if (f) { + debug("saving webvi timer history to %s", (const char *)historyname); + SaveHistory(f); + fclose(f); + } else { + LOG_ERROR_STR("Can't save webvi timer history"); + ok = false; + } + + modified = !ok; + + return ok; +} + +void cWebviTimerManager::Update() { + char timestr[25]; + cWebviTimer *timer = timers.First(); + if (!timer) + return; + + time_t now = time(NULL); + +#ifdef DEBUG + strftime(timestr, 25, "%x %X", localtime(&now)); + debug("Running webvi timers update at %s", timestr); +#endif + + while (timer) { + if (timer->NextUpdate() < now) { + debug("%d. %s: launching now", + timer->GetID(), timer->GetTitle()); + timer->Execute(); + } else { +#ifdef DEBUG + time_t next = timer->NextUpdate(); + strftime(timestr, 25, "%x %X", localtime(&next)); + debug("%d. %s: next update at %s", + timer->GetID(), timer->GetTitle(), timestr); +#endif + } + + timer = timers.Next(timer); + } +} + +cWebviTimer *cWebviTimerManager::GetByID(int id) const { + cWebviTimer *timer = timers.First(); + + while (timer) { + if (timer->GetID() == id) + return timer; + + timer = timers.Next(timer); + } + + return NULL; +} + +cWebviTimer *cWebviTimerManager::Create(const char *title, + const char *ref, + bool getExisting) { + cWebviTimer *t; + + if (!ref) + return NULL; + + if (getExisting) { + t = timers.First(); + while (t) { + if (strcmp(t->GetReference(), ref) == 0) { + return t; + } + + t = timers.Next(t); + } + } + + t = new cWebviTimer(nextID++, title, ref, this); + timers.Add(t); + + modified = true; + + return t; +} + +void cWebviTimerManager::Remove(cWebviTimer *timer) { + timers.Del(timer); + modified = true; +} + +void cWebviTimerManager::MarkDownloaded(const char *ref) { + if (!ref) + return; + + if (refHistory.Find(ref) == -1) { + refHistory.Append(strdup(ref)); + modified = true; + } +} + +bool cWebviTimerManager::AlreadyDownloaded(const char *ref) { + return refHistory.Find(ref) != -1; +} diff --git a/src/vdr-plugin/timer.h b/src/vdr-plugin/timer.h new file mode 100644 index 0000000..048014a --- /dev/null +++ b/src/vdr-plugin/timer.h @@ -0,0 +1,111 @@ +/* + * timer.h: Web video plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#ifndef __WEBVIDEO_TIMER_H +#define __WEBVIDEO_TIMER_H + +#include <time.h> +#include <stdio.h> +#include <vdr/tools.h> +#include "request.h" + +#define REQ_ID_TIMER -2 +#define DEFAULT_TIMER_INTERVAL 7*24*60*60 +#define RETRY_TIMER_INTERVAL 60*60 +#define MIN_TIMER_INTERVAL 10*60 +#define MAX_TIMER_HISTORY_SIZE 2000 + +class cWebviTimerManager; + +// --- cWebviTimer ----------------------------------------------- + +class cWebviTimer : public cListObject { +private: + int id; + char *title; + char *reference; + + time_t lastUpdate; + int interval; + + bool running; + cStringList activeStreams; + bool lastSucceeded; + char *lastError; + + cWebviTimerManager *parent; + +public: + cWebviTimer(int ID, const char *title, const char *ref, + cWebviTimerManager *manager, + time_t last=0, int interval=DEFAULT_TIMER_INTERVAL, + bool success=true, const char *errmsg=NULL); + ~cWebviTimer(); + + int GetID() const { return id; } + void SetTitle(const char *newTitle); + const char *GetTitle() const { return title; } + void SetInterval(int interval); + int GetInterval() const; + const char *GetReference() const { return reference; } + + time_t LastUpdate() const { return lastUpdate; } + time_t NextUpdate() const; + + void SetError(const char *errmsg); + bool Success() const { return lastSucceeded; } + const char *LastError() const; + + void Execute(); + bool Running() { return running; } + void DownloadStreams(const char *menuxml, cProgressVector& summaries); + void CheckFailed(const char *errmsg); + void RequestFinished(const char *ref, const char *errmsg); +}; + +// --- cWebviTimerManager ---------------------------------------- + +class cWebviTimerManager { +private: + cList<cWebviTimer> timers; + int nextID; + cStringList refHistory; + bool modified; + bool disableSaving; + + cWebviTimerManager(); + ~cWebviTimerManager() {}; + cWebviTimerManager(const cWebviTimerManager &); // intentionally undefined + cWebviTimerManager &operator=(const cWebviTimerManager &); // intentionally undefined + + void LoadTimers(FILE *f); + void LoadHistory(FILE *f); + void SaveTimers(FILE *f); + void SaveHistory(FILE *f); + +public: + static cWebviTimerManager &Instance(); + + bool Load(const char *path); + bool Save(const char *path); + + cWebviTimer *Create(const char *title, const char *reference, + bool getExisting=true); + void Remove(cWebviTimer *timer); + cWebviTimer *First() const { return timers.First(); } + cWebviTimer *Next(const cWebviTimer *cur) const { return timers.Next(cur); } + cWebviTimer *GetLinear(int idx) const { return timers.Get(idx); } + cWebviTimer *GetByID(int id) const; + void SetModified() { modified = true; } + + void Update(); + void MarkDownloaded(const char *ref); + bool AlreadyDownloaded(const char *ref); +}; + +#endif diff --git a/src/vdr-plugin/webvideo.c b/src/vdr-plugin/webvideo.c new file mode 100644 index 0000000..554ef28 --- /dev/null +++ b/src/vdr-plugin/webvideo.c @@ -0,0 +1,444 @@ +/* + * webvideo.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <getopt.h> +#include <time.h> +#include <vdr/plugin.h> +#include <vdr/tools.h> +#include <vdr/videodir.h> +#include <vdr/i18n.h> +#include <vdr/skins.h> +#include <libwebvi.h> +#include "menu.h" +#include "history.h" +#include "download.h" +#include "request.h" +#include "mimetypes.h" +#include "config.h" +#include "player.h" +#include "common.h" +#include "timer.h" + +const char *VERSION = "0.3.0"; +static const char *DESCRIPTION = trNOOP("Download video files from the web"); +static const char *MAINMENUENTRY = "Webvideo"; +cMimeTypes *MimeTypes = NULL; + +class cPluginWebvideo : public cPlugin { +private: + // Add any member variables or functions you may need here. + cHistory history; + cProgressVector summaries; + cString templatedir; + cString destdir; + cString conffile; + + static int nextMenuID; + + void UpdateOSDFromHistory(const char *statusmsg=NULL); + void UpdateStatusMenu(bool force=false); + bool StartStreaming(const cString &streamurl); + void ExecuteTimers(void); + void HandleFinishedRequests(void); + +public: + cPluginWebvideo(void); + virtual ~cPluginWebvideo(); + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return tr(DESCRIPTION); } + virtual const char *CommandLineHelp(void); + virtual bool ProcessArgs(int argc, char *argv[]); + virtual bool Initialize(void); + virtual bool Start(void); + virtual void Stop(void); + virtual void Housekeeping(void); + virtual void MainThreadHook(void); + virtual cString Active(void); + virtual const char *MainMenuEntry(void) { return MAINMENUENTRY; } + virtual cOsdObject *MainMenuAction(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *Name, const char *Value); + virtual bool Service(const char *Id, void *Data = NULL); + virtual const char **SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char *Command, const char *Option, int &ReplyCode); + }; + +int cPluginWebvideo::nextMenuID = 1; + +cPluginWebvideo::cPluginWebvideo(void) +{ + // Initialize any member variables here. + // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL + // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT! +} + +cPluginWebvideo::~cPluginWebvideo() +{ + // Clean up after yourself! + webvi_cleanup(0); +} + +const char *cPluginWebvideo::CommandLineHelp(void) +{ + // Return a string that describes all known command line options. + return " -d DIR, --downloaddir=DIR Save downloaded files to DIR\n" \ + " -t DIR, --templatedir=DIR Read video site templates from DIR\n" \ + " -c FILE, --conf=FILE Load settings from FILE\n"; +} + +bool cPluginWebvideo::ProcessArgs(int argc, char *argv[]) +{ + // Implement command line argument processing here if applicable. + static struct option long_options[] = { + { "downloaddir", required_argument, NULL, 'd' }, + { "templatedir", required_argument, NULL, 't' }, + { "conf", required_argument, NULL, 'c' }, + { NULL } + }; + + int c; + while ((c = getopt_long(argc, argv, "d:t:c:", long_options, NULL)) != -1) { + switch (c) { + case 'd': + destdir = cString(optarg); + break; + case 't': + templatedir = cString(optarg); + break; + case 'c': + conffile = cString(optarg); + break; + default: + return false; + } + } + return true; +} + +bool cPluginWebvideo::Initialize(void) +{ + // Initialize any background activities the plugin shall perform. + + // Test that run-time and compile-time libxml versions are compatible + LIBXML_TEST_VERSION; + + // default values if not given on the command line + if ((const char *)destdir == NULL) + destdir = cString(VideoDirectory); + if ((const char *)conffile == NULL) + conffile = AddDirectory(ConfigDirectory(Name()), "webvi.plugin.conf"); + + webvideoConfig->SetDownloadPath(destdir); + webvideoConfig->SetTemplatePath(templatedir); + webvideoConfig->ReadConfigFile(conffile); + + cString mymimetypes = AddDirectory(ConfigDirectory(Name()), "mime.types"); + const char *mimefiles [] = {"/etc/mime.types", (const char *)mymimetypes, NULL}; + MimeTypes = new cMimeTypes(mimefiles); + + if (webvi_global_init() != 0) { + error("Failed to initialize libwebvi"); + return false; + } + + cWebviTimerManager::Instance().Load(ConfigDirectory(Name())); + + cWebviThread::Instance().SetTemplatePath(webvideoConfig->GetTemplatePath()); + + return true; +} + +bool cPluginWebvideo::Start(void) +{ + // Start any background activities the plugin shall perform. + cWebviThread::Instance().Start(); + + return true; +} + +void cPluginWebvideo::Stop(void) +{ + // Stop any background activities the plugin shall perform. + cWebviThread::Instance().Stop(); + delete MimeTypes; + + cWebviTimerManager::Instance().Save(ConfigDirectory(Name())); + + xmlCleanupParser(); +} + +void cPluginWebvideo::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. + + cWebviTimerManager::Instance().Save(ConfigDirectory(Name())); +} + +void cPluginWebvideo::MainThreadHook(void) +{ + // Perform actions in the context of the main program thread. + // WARNING: Use with great care - see PLUGINS.html! + ExecuteTimers(); + + HandleFinishedRequests(); +} + +void cPluginWebvideo::ExecuteTimers(void) +{ + static int counter = 0; + + // don't do this too often + if (counter++ > 1800) { + cWebviTimerManager::Instance().Update(); + counter = 0; + } +} + +void cPluginWebvideo::HandleFinishedRequests(void) +{ + bool forceStatusUpdate = false; + cMenuRequest *req; + cFileDownloadRequest *dlreq; + cString streamurl; + cWebviTimer *timer; + cString timermsg; + + while ((req = cWebviThread::Instance().GetFinishedRequest())) { + int cid = -1; + int code = req->GetStatusCode(); + if (history.Current()) { + cid = history.Current()->GetID(); + } + + debug("Finished request: %d (current: %d), type = %d, status = %d", + req->GetID(), cid, req->GetType(), code); + + if (req->Success()) { + switch (req->GetType()) { + case REQT_MENU: + // Only change the menu if the request was launched from the + // current menu. + if (req->GetID() == cid) { + if (cid == 0) { + // Special case: replace the placeholder menu + history.Clear(); + } + + if (history.Current()) + history.Current()->RememberSelected(menuPointers.navigationMenu->Current()); + history.TruncateAndAdd(new cHistoryObject(req->GetResponse(), + req->GetReference(), + nextMenuID++)); + UpdateOSDFromHistory(); + } + break; + + case REQT_STREAM: + streamurl = req->GetResponse(); + if (streamurl[0] == '\0') + Skins.Message(mtError, tr("Streaming failed: no URL")); + else if (!StartStreaming(streamurl)) + Skins.Message(mtError, tr("Failed to launch media player")); + break; + + case REQT_FILE: + dlreq = dynamic_cast<cFileDownloadRequest *>(req); + + if (dlreq) { + for (int i=0; i<summaries.Size(); i++) { + if (summaries[i]->GetRequest() == dlreq) { + delete summaries[i]; + summaries.Remove(i); + break; + } + } + } + + timermsg = cString(""); + if (req->GetTimer()) { + req->GetTimer()->RequestFinished(req->GetReference(), NULL); + + timermsg = cString::sprintf(" (%s)", tr("timer")); + } + + Skins.Message(mtInfo, cString::sprintf(tr("One download completed, %d remains%s"), + cWebviThread::Instance().GetUnfinishedCount(), + (const char *)timermsg)); + forceStatusUpdate = true; + break; + + case REQT_TIMER: + timer = req->GetTimer(); + if (timer) + timer->DownloadStreams(req->GetResponse(), summaries); + break; + + default: + break; + } + } else { // failed request + if (req->GetType() == REQT_TIMER) { + warning("timer request failed (%d: %s)", + code, (const char*)req->GetStatusPharse()); + + timer = req->GetTimer(); + if (timer) + timer->CheckFailed(req->GetStatusPharse()); + } else { + warning("request failed (%d: %s)", + code, (const char*)req->GetStatusPharse()); + + if (code == -2 || code == 402) + Skins.Message(mtError, tr("Download aborted")); + else + Skins.Message(mtError, cString::sprintf(tr("Download failed (error = %d)"), code)); + + dlreq = dynamic_cast<cFileDownloadRequest *>(req); + if (dlreq) { + for (int i=0; i<summaries.Size(); i++) { + if (summaries[i]->GetRequest() == dlreq) { + summaries[i]->AssociateWith(NULL); + break; + } + } + } + + if (req->GetTimer()) + req->GetTimer()->RequestFinished(req->GetReference(), + (const char*)req->GetStatusPharse()); + + forceStatusUpdate = true; + } + } + + delete req; + } + + UpdateStatusMenu(forceStatusUpdate); +} + +cString cPluginWebvideo::Active(void) +{ + // Return a message string if shutdown should be postponed + int c = cWebviThread::Instance().GetUnfinishedCount(); + if (c > 0) + return cString::sprintf(tr("%d downloads not finished"), c); + else + return NULL; +} + +cOsdObject *cPluginWebvideo::MainMenuAction(void) +{ + // Perform the action when selected from the main VDR menu. + const char *mainMenuReference = "wvt:///?srcurl=mainmenu"; + const char *placeholderMenu = "<wvmenu><title>Webvideo</title></wvmenu>"; + const char *statusmsg = NULL; + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 100*1000*1000; // 100 ms + + menuPointers.navigationMenu = new cNavigationMenu(&history, summaries); + + cHistoryObject *hist = history.Home(); + if (!hist) { + cWebviThread::Instance().AddRequest(new cMenuRequest(0, mainMenuReference)); + cHistoryObject *placeholder = new cHistoryObject(placeholderMenu, mainMenuReference, 0); + history.TruncateAndAdd(placeholder); + + // The main menu response should come right away. Try to update + // the menu here without having to wait for the next + // MainThreadHook call by VDR main loop. + for (int i=0; i<4; i++) { + nanosleep(&ts, NULL); + HandleFinishedRequests(); + if (history.Current() != placeholder) { + return menuPointers.navigationMenu; + } + }; + + statusmsg = tr("Retrieving..."); + } + + UpdateOSDFromHistory(statusmsg); + return menuPointers.navigationMenu; +} + +cMenuSetupPage *cPluginWebvideo::SetupMenu(void) +{ + // Return a setup menu in case the plugin supports one. + return NULL; +} + +bool cPluginWebvideo::SetupParse(const char *Name, const char *Value) +{ + // Parse your own setup parameters and store their values. + return false; +} + +bool cPluginWebvideo::Service(const char *Id, void *Data) +{ + // Handle custom service requests from other plugins + return false; +} + +const char **cPluginWebvideo::SVDRPHelpPages(void) +{ + // Return help text for SVDRP commands this plugin implements + return NULL; +} + +cString cPluginWebvideo::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode) +{ + // Process SVDRP commands this plugin implements + return NULL; +} + +void cPluginWebvideo::UpdateOSDFromHistory(const char *statusmsg) { + if (menuPointers.navigationMenu) { + cHistoryObject *hist = history.Current(); + menuPointers.navigationMenu->Populate(hist, statusmsg); + menuPointers.navigationMenu->Display(); + } else { + debug("OSD is not ours."); + } +} + +void cPluginWebvideo::UpdateStatusMenu(bool force) { + if (menuPointers.statusScreen && + (force || menuPointers.statusScreen->NeedsUpdate())) { + menuPointers.statusScreen->Update(); + } +} + +bool cPluginWebvideo::StartStreaming(const cString &streamurl) { + cMediaPlayer *players[2]; + + if (webvideoConfig->GetPreferXineliboutput()) { + players[0] = new cXineliboutputPlayer(); + players[1] = new cMPlayerPlayer(); + } else { + players[0] = new cMPlayerPlayer(); + players[1] = new cXineliboutputPlayer(); + } + + bool ret = false; + for (int i=0; i<2; i++) { + if (players[i]->Launch(streamurl)) { + ret = true; + break; + } + } + + for (int i=0; i<2 ; i++) { + delete players[i]; + } + + return ret; +} + +VDRPLUGINCREATOR(cPluginWebvideo); // Don't touch this! diff --git a/src/version b/src/version new file mode 100644 index 0000000..9325c3c --- /dev/null +++ b/src/version @@ -0,0 +1 @@ +0.3.0
\ No newline at end of file diff --git a/src/webvicli/webvi b/src/webvicli/webvi new file mode 100755 index 0000000..b8fa190 --- /dev/null +++ b/src/webvicli/webvi @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# menu.py - starter script for webvicli +# +# Copyright (c) 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 +from webvicli import client +client.main(sys.argv[1:]) diff --git a/src/webvicli/webvicli/__init__.py b/src/webvicli/webvicli/__init__.py new file mode 100644 index 0000000..1cf59b7 --- /dev/null +++ b/src/webvicli/webvicli/__init__.py @@ -0,0 +1 @@ +__all__ = ['client', 'menu'] diff --git a/src/webvicli/webvicli/client.py b/src/webvicli/webvicli/client.py new file mode 100644 index 0000000..782c47c --- /dev/null +++ b/src/webvicli/webvicli/client.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python + +# webvicli.py - webvi command line client +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 cStringIO +import sys +import cmd +import mimetypes +import select +import os.path +import subprocess +import time +import re +import libxml2 +import webvi.api +import webvi.utils +from optparse import OptionParser +from ConfigParser import RawConfigParser +from webvi.constants import WebviRequestType, WebviErr, WebviOpt, WebviInfo, WebviSelectBitmask, WebviConfig +from . import menu + +VERSION = '0.3.0' + +# Default options +DEFAULT_PLAYERS = ['vlc --play-and-exit "%s"', + 'totem "%s"', + 'mplayer "%s"', + 'xine "%s"'] + +# These mimetypes are common but often missing +mimetypes.init() +mimetypes.add_type('video/flv', '.flv') +mimetypes.add_type('video/x-flv', '.flv') + +def safe_filename(name): + """Sanitize a filename. No paths (replace '/' -> '!') and no + names starting with a dot.""" + res = name.replace('/', '!').lstrip('.') + res = res.encode(sys.getfilesystemencoding(), 'ignore') + return res + +class DownloadData: + def __init__(self, handle, progressstream): + self.handle = handle + self.destfile = None + self.destfilename = '' + self.contentlength = -1 + self.bytes_downloaded = 0 + self.progress = ProgressMeter(progressstream) + +class ProgressMeter: + def __init__(self, stream): + self.last_update = None + self.samples = [] + self.total_bytes = 0 + self.stream = stream + self.progress_len = 0 + self.starttime = time.time() + + def pretty_bytes(self, bytes): + """Pretty print bytes as kB or MB.""" + if bytes < 1100: + return '%d B' % bytes + elif bytes < 1024*1024: + return '%.1f kB' % (float(bytes)/1024) + elif bytes < 1024*1024*1024: + return '%.1f MB' % (float(bytes)/1024/1024) + else: + return '%.1f GB' % (float(bytes)/1024/1024/1024) + + def pretty_time(self, seconds): + """Pretty print seconds as hour and minutes.""" + seconds = int(round(seconds)) + if seconds < 60: + return '%d s' % seconds + elif seconds < 60*60: + secs = seconds % 60 + mins = seconds/60 + return '%d min %d s' % (mins, secs) + else: + hours = seconds / (60*60) + mins = (seconds-60*60*hours) / 60 + return '%d hours %d min' % (hours, mins) + + def update(self, bytes): + """Update progress bar. + + Updates the estimates of download rate and remaining time. + Prints progress bar, if at least one second has passed since + the previous update. + """ + now = time.time() + + if self.total_bytes > 0: + percentage = float(bytes)/self.total_bytes * 100.0 + else: + percentage = 0 + + if self.total_bytes > 0 and bytes >= self.total_bytes: + self.stream.write('\r') + self.stream.write(' '*self.progress_len) + self.stream.write('\r') + self.stream.write('%3.f %% of %s downloaded in %s (%.1f kB/s)\n' % + (percentage, self.pretty_bytes(self.total_bytes), + self.pretty_time(now-self.starttime), + float(bytes)/(now-self.starttime)/1024.0)) + self.stream.flush() + return + + force_refresh = False + if self.last_update is None: + # This is a new progress meter + self.last_update = now + force_refresh = True + + if (not force_refresh) and (now <= self.last_update + 1): + # do not update too often + return + + self.last_update = now + + # Estimate bytes per second rate from the last 10 samples + self.samples.append((bytes, now)) + if len(self.samples) > 10: + self.samples.pop(0) + + bytes_old, time_old = self.samples[0] + if now > time_old: + rate = float(bytes-bytes_old)/(now-time_old) + else: + rate = 0 + + if self.total_bytes > 0: + remaining = self.total_bytes - bytes + + if rate > 0: + time_left = self.pretty_time(remaining/rate) + else: + time_left = '???' + + progress = '%3.f %% of %s (%.1f kB/s) %s remaining' % \ + (percentage, self.pretty_bytes(self.total_bytes), + rate/1024.0, time_left) + else: + progress = '%s downloaded (%.1f kB/s)' % \ + (self.pretty_bytes(bytes), rate/1024.0) + + new_progress_len = len(progress) + if new_progress_len < self.progress_len: + progress += ' '*(self.progress_len - new_progress_len) + self.progress_len = new_progress_len + + self.stream.write('\r') + self.stream.write(progress) + self.stream.flush() + + +class WVClient: + def __init__(self, streamplayers, downloadlimits, streamlimits): + self.streamplayers = streamplayers + self.history = [] + self.history_pointer = 0 + self.quality_limits = {'download': downloadlimits, + 'stream': streamlimits} + + def parse_page(self, page): + if page is None: + return None + try: + doc = libxml2.parseDoc(page) + except libxml2.parserError: + return None + + root = doc.getRootElement() + if root.name != 'wvmenu': + return None + queryitems = [] + menupage = menu.Menu() + node = root.children + while node: + if node.name == 'title': + menupage.title = webvi.utils.get_content_unicode(node) + elif node.name == 'link': + menuitem = self.parse_link(node) + menupage.add(menuitem) + elif node.name == 'textfield': + menuitem = self.parse_textfield(node) + menupage.add(menuitem) + queryitems.append(menuitem) + elif node.name == 'itemlist': + menuitem = self.parse_itemlist(node) + menupage.add(menuitem) + queryitems.append(menuitem) + elif node.name == 'textarea': + menuitem = self.parse_textarea(node) + menupage.add(menuitem) + elif node.name == 'button': + menuitem = self.parse_button(node, queryitems) + menupage.add(menuitem) + node = node.next + doc.freeDoc() + return menupage + + def parse_link(self, node): + label = '' + ref = None + stream = None + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'ref': + ref = webvi.utils.get_content_unicode(child) + elif child.name == 'stream': + stream = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemLink(label, ref, stream) + + def parse_textfield(self, node): + label = '' + name = node.prop('name') + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemTextField(label, name) + + def parse_textarea(self, node): + label = '' + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemTextArea(label) + + def parse_itemlist(self, node): + label = '' + name = node.prop('name') + items = [] + values = [] + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'item': + items.append(webvi.utils.get_content_unicode(child)) + values.append(child.prop('value')) + child = child.next + return menu.MenuItemList(label, name, items, values, sys.stdout) + + def parse_button(self, node, queryitems): + label = '' + submission = None + child = node.children + while child: + if child.name == 'label': + label = webvi.utils.get_content_unicode(child) + elif child.name == 'submission': + submission = webvi.utils.get_content_unicode(child) + child = child.next + return menu.MenuItemSubmitButton(label, submission, queryitems) + + def guess_extension(self, mimetype, url): + ext = mimetypes.guess_extension(mimetype) + if (ext is None) or (mimetype == 'text/plain'): + # This function is only called for video files. Try to + # extract the extension from url because text/plain is + # clearly wrong. + lastcomponent = re.split(r'[?#]', url, 1)[0].split('/')[-1] + i = lastcomponent.rfind('.') + if i == -1: + ext = '' + else: + ext = lastcomponent[i:] + + return ext + + def execute_webvi(self, handle): + """Call webvi.api.perform until handle is finished.""" + while True: + rescode, readfds, writefds, excfds, maxfd = webvi.api.fdset() + if [] == readfds == writefds == excfds: + finished, status, errmsg, remaining = webvi.api.pop_message() + if finished == handle: + return (status, errmsg) + else: + return (501, 'No active sockets') + + readyread, readywrite, readyexc = select.select(readfds, writefds, excfds, 30.0) + + for fd in readyread: + webvi.api.perform(fd, WebviSelectBitmask.READ) + for fd in readywrite: + webvi.api.perform(fd, WebviSelectBitmask.WRITE) + + remaining = -1 + while remaining != 0: + finished, status, errmsg, remaining = webvi.api.pop_message() + if finished == handle: + return (status, errmsg) + + def collect_data(self, inp, inplen, dlbuffer): + """Callback that writes the downloaded data to dlbuffer. + """ + dlbuffer.write(inp) + return inplen + + def open_dest_file(self, inp, inplen, dldata): + """Initial download callback. This opens the destination file, + and reseats the callback to self.write_to_dest. The + destination file can not be opened until now, because the + stream title and final URL are not known before. + """ + title = webvi.api.get_info(dldata.handle, WebviInfo.STREAM_TITLE)[1] + contenttype = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_TYPE)[1] + contentlength = webvi.api.get_info(dldata.handle, WebviInfo.CONTENT_LENGTH)[1] + url = webvi.api.get_info(dldata.handle, WebviInfo.URL)[1] + ext = self.guess_extension(contenttype, url) + destfilename = self.next_available_file_name(safe_filename(title), ext) + + try: + destfile = open(destfilename, 'w') + except IOError, err: + print 'Failed to open the destination file %s: %s' % (destfilename, err.args[1]) + return -1 + + dldata.destfile = destfile + dldata.destfilename = destfilename + dldata.contentlength = contentlength + dldata.progress.total_bytes = contentlength + webvi.api.set_opt(dldata.handle, WebviOpt.WRITEFUNC, self.write_to_dest) + + return self.write_to_dest(inp, inplen, dldata) + + def write_to_dest(self, inp, inplen, dldata): + """Callback that writes downloaded data to self.destfile.""" + try: + dldata.destfile.write(inp) + except IOError, err: + print 'IOError while writing to %s: %s' % \ + (dldata.destfilename, err.args[1]) + return -1 + + dldata.bytes_downloaded += inplen + + dldata.progress.update(dldata.bytes_downloaded) + + return inplen + + def getmenu(self, ref): + dlbuffer = cStringIO.StringIO() + handle = webvi.api.new_request(ref, WebviRequestType.MENU) + if handle == -1: + print 'Failed to open handle' + return (-1, '', None) + + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer) + webvi.api.start_handle(handle) + + status, err = self.execute_webvi(handle) + webvi.api.delete_handle(handle) + + if status != 0: + print 'Download failed:', err + return (status, err, None) + + return (status, err, self.parse_page(dlbuffer.getvalue())) + + def get_quality_params(self, videosite, streamtype): + params = [] + lim = self.quality_limits[streamtype].get(videosite, {}) + + if lim.has_key('min'): + params.append('minquality=' + lim['min']) + if lim.has_key('max'): + params.append('maxquality=' + lim['max']) + + return '&'.join(params) + + def download(self, stream): + m = re.match(r'wvt:///([^/]+)/', stream) + if m is not None: + stream += '&' + self.get_quality_params(m.group(1), 'download') + + handle = webvi.api.new_request(stream, WebviRequestType.FILE) + if handle == -1: + print 'Failed to open handle' + return False + + dldata = DownloadData(handle, sys.stdout) + + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.open_dest_file) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dldata) + webvi.api.start_handle(handle) + + status, err = self.execute_webvi(handle) + if dldata.destfile is not None: + dldata.destfile.close() + + webvi.api.delete_handle(handle) + + if status not in (0, 504): + print 'Download failed:', err + return + + if dldata.contentlength != -1 and \ + dldata.bytes_downloaded != dldata.contentlength: + print 'Warning: the size of the file (%d) differs from expected (%d)' % \ + (dldata.bytes_downloaded, dldata.contentlength) + + print 'Saved to %s' % dldata.destfilename + + return True + + def play_stream(self, ref): + streamurl = self.get_stream_url(ref) + if streamurl == '': + print 'Did not find URL' + return False + + # Found url, now find a working media player + for player in self.streamplayers: + if '%s' not in player: + playcmd = player + ' ' + streamurl + else: + try: + playcmd = player % streamurl + except TypeError: + print 'Can\'t substitute URL in', player + continue + + try: + print 'Trying player: ' + playcmd + retcode = subprocess.call(playcmd, shell=True) + if retcode > 0: + print 'Player failed with returncode', retcode + else: + return True + except OSError, err: + print 'Execution failed:', err + + return False + + def get_stream_url(self, ref): + m = re.match(r'wvt:///([^/]+)/', ref) + if m is not None: + ref += '&' + self.get_quality_params(m.group(1), 'stream') + + handle = webvi.api.new_request(ref, WebviRequestType.STREAMURL) + if handle == -1: + print 'Failed to open handle' + return '' + + dlbuffer = cStringIO.StringIO() + webvi.api.set_opt(handle, WebviOpt.WRITEFUNC, self.collect_data) + webvi.api.set_opt(handle, WebviOpt.WRITEDATA, dlbuffer) + webvi.api.start_handle(handle) + status, err = self.execute_webvi(handle) + webvi.api.delete_handle(handle) + + if status != 0: + print 'Download failed:', err + return '' + + return dlbuffer.getvalue() + + def next_available_file_name(self, basename, ext): + fullname = basename + ext + if not os.path.exists(fullname): + return fullname + i = 1 + while os.path.exists('%s-%d%s' % (basename, i, ext)): + i += 1 + return '%s-%d%s' % (basename, i, ext) + + def get_current_menu(self): + if (self.history_pointer >= 0) and \ + (self.history_pointer < len(self.history)): + return self.history[self.history_pointer] + else: + return None + + def history_add(self, menupage): + if menupage is not None: + self.history = self.history[:(self.history_pointer+1)] + self.history.append(menupage) + self.history_pointer = len(self.history)-1 + + def history_back(self): + if self.history_pointer > 0: + self.history_pointer -= 1 + return self.get_current_menu() + + def history_forward(self): + if self.history_pointer < len(self.history)-1: + self.history_pointer += 1 + return self.get_current_menu() + + +class WVShell(cmd.Cmd): + def __init__(self, client, completekey='tab', stdin=None, stdout=None): + cmd.Cmd.__init__(self, completekey, stdin, stdout) + self.prompt = '> ' + self.client = client + + def preloop(self): + self.stdout.write('webvicli %s starting\n' % VERSION) + self.do_menu(None) + + def precmd(self, arg): + try: + int(arg) + return 'select ' + arg + except ValueError: + return arg + + def onecmd(self, c): + try: + return cmd.Cmd.onecmd(self, c) + except Exception: + import traceback + print 'Exception occured while handling command "' + c + '"' + print traceback.format_exc() + return False + + def emptyline(self): + pass + + def display_menu(self, menupage): + if menupage is not None: + self.stdout.write(unicode(menupage).encode(self.stdout.encoding, 'replace')) + + def _get_numbered_item(self, arg): + menupage = self.client.get_current_menu() + try: + v = int(arg)-1 + if (menupage is None) or (v < 0) or (v >= len(menupage)): + raise ValueError + except ValueError: + self.stdout.write('Invalid selection: %s\n' % arg) + return None + return menupage[v] + + def do_select(self, arg): + """select x +Select the link whose index is x. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + ref = menuitem.activate() + if ref is not None: + status, statusmsg, menupage = self.client.getmenu(ref) + if menupage is not None: + self.client.history_add(menupage) + else: + self.stdout.write('Error: %d %s\n' % (status, statusmsg)) + else: + menupage = self.client.get_current_menu() + self.display_menu(menupage) + return False + + def do_download(self, arg): + """download x +Download media stream whose index is x to a file. Downloadable items +are the ones without brackets. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + elif hasattr(menuitem, 'stream') and menuitem.stream is not None: + self.client.download(menuitem.stream) + else: + self.stdout.write('Not a stream\n') + return False + + def do_stream(self, arg): + """stream x +Play the media file whose index is x. Streams are the ones +without brackets. + """ + menuitem = self._get_numbered_item(arg) + if menuitem is None: + return False + elif hasattr(menuitem, 'stream') and menuitem.stream is not None: + self.client.play_stream(menuitem.stream) + else: + self.stdout.write('Not a stream\n') + return False + + def do_display(self, arg): + """Redisplay the current menu.""" + if not arg: + self.display_menu(self.client.get_current_menu()) + else: + self.stdout.write('Unknown parameter %s\n' % arg) + return False + + def do_menu(self, arg): + """Get back to the main menu.""" + status, statusmsg, menupage = self.client.getmenu('wvt:///?srcurl=mainmenu') + if menupage is not None: + self.client.history_add(menupage) + self.display_menu(menupage) + else: + self.stdout.write('Error: %d %s\n' % (status, statusmsg)) + return True + return False + + def do_back(self, arg): + """Go to the previous menu in the history.""" + menupage = self.client.history_back() + self.display_menu(menupage) + return False + + def do_forward(self, arg): + """Go to the next menu in the history.""" + menupage = self.client.history_forward() + self.display_menu(menupage) + return False + + def do_quit(self, arg): + """Quit the program.""" + return True + + def do_EOF(self, arg): + """Quit the program.""" + return True + + +def load_config(options): + """Load options from config files.""" + cfgprs = RawConfigParser() + cfgprs.read(['/etc/webvi.conf', os.path.expanduser('~/.webvi')]) + for sec in cfgprs.sections(): + if sec == 'webvi': + for opt, val in cfgprs.items('webvi'): + options[opt] = val + + elif sec.startswith('site-'): + sitename = sec[5:] + + if not options.has_key('download-limits'): + options['download-limits'] = {} + if not options.has_key('stream-limits'): + options['stream-limits'] = {} + options['download-limits'][sitename] = {} + options['stream-limits'][sitename] = {} + + for opt, val in cfgprs.items(sec): + if opt == 'download-min-quality': + options['download-limits'][sitename]['min'] = val + elif opt == 'download-max-quality': + options['download-limits'][sitename]['max'] = val + elif opt == 'stream-min-quality': + options['stream-limits'][sitename]['min'] = val + elif opt == 'stream-max-quality': + options['stream-limits'][sitename]['max'] = val + + return options + +def parse_command_line(cmdlineargs, options): + parser = OptionParser() + parser.add_option('-t', '--templatepath', type='string', + dest='templatepath', + help='read video site templates from DIR', metavar='DIR', + default=None) + cmdlineopt = parser.parse_args(cmdlineargs)[0] + + if cmdlineopt.templatepath is not None: + options['templatepath'] = cmdlineopt.templatepath + + return options + +def player_list(options): + """Return a sorted list of player commands extracted from options + dictionary.""" + # Load streamplayer items from the config file and sort them + # according to quality. + players = [] + for opt, val in options.iteritems(): + m = re.match(r'streamplayer([1-9])$', opt) + if m is not None: + players.append((int(m.group(1)), val)) + + players.sort() + ret = [] + for quality, playcmd in players: + ret.append(playcmd) + + # If the config file did not define any players use the default + # players + if not ret: + ret = list(DEFAULT_PLAYERS) + + return ret + +def main(argv): + options = load_config({}) + options = parse_command_line(argv, options) + + if options.has_key('templatepath'): + webvi.api.set_config(WebviConfig.TEMPLATE_PATH, options['templatepath']) + + shell = WVShell(WVClient(player_list(options), + options.get('download-limits', {}), + options.get('stream-limits', {}))) + shell.cmdloop() + +if __name__ == '__main__': + main([]) diff --git a/src/webvicli/webvicli/menu.py b/src/webvicli/webvicli/menu.py new file mode 100644 index 0000000..70ef6ea --- /dev/null +++ b/src/webvicli/webvicli/menu.py @@ -0,0 +1,171 @@ +# menu.py - menu elements for webvicli +# +# Copyright (c) 2009, 2010 Antti Ajanki <antti.ajanki@iki.fi> +# +# 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 textwrap +import urllib + +LINEWIDTH = 72 + +class Menu: + def __init__(self): + self.title = None + self.items = [] + + def __str__(self): + s = u'' + if self.title: + s = self.title + '\n' + '='*len(self.title) + '\n' + for i, item in enumerate(self.items): + if isinstance(item, MenuItemTextArea): + num = ' ' + else: + num = '%d.' % (i+1) + + s += u'%s %s\n' % (num, unicode(item).replace('\n', '\n ')) + return s + + def __getitem__(self, i): + return self.items[i] + + def __len__(self): + return len(self.items) + + def add(self, menuitem): + self.items.append(menuitem) + + +class MenuItemLink: + def __init__(self, label, ref, stream): + self.label = label + if type(ref) == unicode: + self.ref = ref.encode('utf-8') + else: + self.ref = ref + self.stream = stream + + def __str__(self): + res = self.label + if not self.stream: + res = '[' + res + ']' + return res + + def activate(self): + return self.ref + + +class MenuItemTextField: + def __init__(self, label, name): + self.label = label + self.name = name + self.value = u'' + + def __str__(self): + return u'%s: %s' % (self.label, self.value) + + def get_query(self): + return {self.name: self.value} + + def activate(self): + self.value = unicode(raw_input('%s> ' % self.label), sys.stdin.encoding) + return None + + +class MenuItemTextArea: + def __init__(self, label): + self.label = label + + def __str__(self): + return textwrap.fill(self.label, width=LINEWIDTH) + + def activate(self): + return None + + +class MenuItemList: + def __init__(self, label, name, items, values, stdout): + self.label = label + self.name = name + assert len(items) == len(values) + self.items = items + self.values = values + self.current = 0 + self.stdout = stdout + + def __str__(self): + itemstrings = [] + for i, itemname in enumerate(self.items): + if i == self.current: + itemstrings.append('<' + itemname + '>') + else: + itemstrings.append(itemname) + + lab = self.label + ': ' + return textwrap.fill(u', '.join(itemstrings), width=LINEWIDTH, + initial_indent=lab, + subsequent_indent=' '*len(lab)) + + def get_query(self): + if (self.current >= 0) and (self.current < len(self.items)): + return {self.name: self.values[self.current]} + else: + return {} + + def activate(self): + itemstrings = [] + for i, itemname in enumerate(self.items): + itemstrings.append('%d. %s' % (i+1, itemname)) + + self.stdout.write(u'\n'.join(itemstrings).encode(self.stdout.encoding, 'replace')) + self.stdout.write('\n') + + tmp = raw_input('Select item (1-%d)> ' % len(self.items)) + try: + i = int(tmp) + if (i < 1) or (i > len(self.items)): + raise ValueError + self.current = i-1 + except ValueError: + self.stdout.write('Must be an integer in the range 1 - %d\n' % len(self.items)) + return None + + +class MenuItemSubmitButton: + def __init__(self, label, baseurl, subitems): + self.label = label + if type(baseurl) == unicode: + self.baseurl = baseurl.encode('utf-8') + else: + self.baseurl = baseurl + self.subitems = subitems + + def __str__(self): + return '[' + self.label + ']' + + def activate(self): + baseurl = self.baseurl + if baseurl.find('?') == -1: + baseurl += '?' + else: + baseurl += '&' + + parts = [] + for sub in self.subitems: + for key, val in sub.get_query().iteritems(): + parts.append('subst=' + urllib.quote_plus(key.encode('utf-8')) + ',' + urllib.quote_plus(val.encode('utf-8'))) + + return baseurl + '&'.join(parts) diff --git a/templates/bin/ruutu-dl b/templates/bin/ruutu-dl new file mode 100755 index 0000000..be8d01e --- /dev/null +++ b/templates/bin/ruutu-dl @@ -0,0 +1,36 @@ +#!/bin/sh + +# Downloads a video stream from ruutu.fi to stdout using +# rtmpdump(-yle). The first parameter is the rtmp URL, the second +# parameter is the video page URL. + +RTMPDUMP= + +which rtmpdump > /dev/null 2>&1 +if [ $? = 0 ]; then + RTMPDUMP=rtmpdump +else + which rtmpdump-yle > /dev/null 2>&1 + if [ $? = 0 ]; then + RTMPDUMP=rtmpdump-yle + fi +fi + +if [ "x$RTMPDUMP" = "x" ]; then + echo "ERROR: neither rtmpdump nor rtmpdump-yle not on \$PATH" 1>&2 + exit 1 +fi + +if [ "x$1" = "x" ]; then + echo "Expected rtmp URL as parameter" 1>&2 + exit 1 +fi + +if [ "x$2" = "x" ]; then + echo "Expected ruutu.fi video page URL as parameter" 1>&2 + exit 1 +fi + +$RTMPDUMP -r $1 -q --swfUrl http://n.sestatic.fi/sites/all/modules/media/Nelonen_mediaplayer_4.6.swf --pageUrl $2 -o - + +exit $? diff --git a/templates/bin/yle-dl b/templates/bin/yle-dl new file mode 100755 index 0000000..a317b12 --- /dev/null +++ b/templates/bin/yle-dl @@ -0,0 +1,22 @@ +#!/bin/sh + +# Downloads a video stream from Yle Areena to stdout using yle-dl +# script. The first parameter is the video page URL. + +YLEDL=yle-dl + +which $YLEDL > /dev/null 2>&1 +if [ $? != 0 ]; then + echo "ERROR: $YLEDL is not on \$PATH" 1>&2 + echo "Install rtmpdump-yle from http://users.tkk.fi/~aajanki/rtmpdump-yle/index.html" 1>&2 + exit 1 +fi + +if [ "x$1" = "x" ]; then + echo "Expected Areena URL as parameter" 1>&2 + exit 1 +fi + +$YLEDL $1 -q -o - + +exit $? diff --git a/templates/google/description.xsl b/templates/google/description.xsl new file mode 100644 index 0000000..b7cab19 --- /dev/null +++ b/templates/google/description.xsl @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="//title"/></title> + + <textarea> + <label><xsl:value-of select="//span[@id='long-desc']"/></label> + </textarea> + <textarea> + <label>Duration: <xsl:value-of select="//span[@id='video-duration']"/></label> + </textarea> + <textarea> + <label>Date: <xsl:value-of select="//span[@id='video-date']"/></label> + </textarea> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/google/search.xsl b/templates/google/search.xsl new file mode 100644 index 0000000..a7a3ab0 --- /dev/null +++ b/templates/google/search.xsl @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Google video search</title> + + <textfield name="q"> + <label>Search terms</label> + </textfield> + + <itemlist name="so"> + <label>Sort by</label> + <item value="0">Relevance</item> + <item value="3">Rating</item> + <item value="4">Popularity</item> + <item value="1">Date</item> + </itemlist> + + <itemlist name="dur"> + <label>Duration</label> + <item value="">All durations</item> + <item value="1">Short (< 4 min)</item> + <item value="2">Medium (4-20 min)</item> + <item value="3">Long (> 20 min)</item> + </itemlist> + + <button> + <label>Search</label> + <submission>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://video.google.com/videosearch?q={q}&so={so}&dur={dur}', true())"/></submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/google/searchresults.xsl b/templates/google/searchresults.xsl new file mode 100644 index 0000000..863d1d8 --- /dev/null +++ b/templates/google/searchresults.xsl @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Search results</title> + + <xsl:choose> + <xsl:when test="not(//div[@class='rl-item'])"> + <textarea> + <label> + <xsl:text>Your search did not return any results.</xsl:text> + </label> + </textarea> + </xsl:when> + + <xsl:otherwise> + <xsl:for-each select="//div[@class='rl-item']"> + <xsl:choose> + <xsl:when test="starts-with(div/@srcurl, 'http://www.youtube.com/')"> + <link> + <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)" /></label> + <stream>wvt:///youtube/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream> + <ref>wvt:///youtube/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://gdata.youtube.com/feeds/api/videos/', substring-after(div/@srcurl, 'v='), '?v=2'), true())"/></ref> + </link> + </xsl:when> + + <xsl:when test="starts-with(div/@srcurl, 'http://video.google.com/')"> + <link> + <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label> + <stream>wvt:///google/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream> + <ref>wvt:///google/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></ref> + </link> + </xsl:when> + + <xsl:when test="starts-with(div/@srcurl, 'http://www.metacafe.com/')"> + <link> + <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label> + <stream>wvt:///metacafe/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl)"/></stream> + <ref>wvt:///metacafe/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl)"/></ref> + </link> + </xsl:when> + + <xsl:when test="starts-with(div/@srcurl, 'http://vimeo.com/')"> + <link> + <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label> + <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="substring-after(div/@srcurl, 'http://vimeo.com/')"/></stream> + <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="substring-after(div/@srcurl, 'http://vimeo.com/')"/>.xml</ref> + </link> + </xsl:when> + + <xsl:when test="starts-with(div/@srcurl, 'http://svtplay.se/')"> + <link> + <label><xsl:value-of select="normalize-space(div/div/div[@class='rl-title']/a)"/></label> + <stream>wvt:///svtplay/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></stream> + <ref>wvt:///svtplay/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(div/@srcurl, true())"/></ref> + </link> + </xsl:when> + + </xsl:choose> + </xsl:for-each> + + <xsl:if test="//td[@class='prev']/a"> + <link> + <label>Previous</label> + <ref>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri(//td[@class='prev']/a/@href, true())"/></ref> + </link> + </xsl:if> + + <xsl:if test="//td[@class='next']/a"> + <link> + <label>Next</label> + <ref>wvt:///google/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri(//td[@class='next']/a/@href, true())"/></ref> + </link> + </xsl:if> + </xsl:otherwise> + </xsl:choose> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/google/service.xml b/templates/google/service.xml new file mode 100644 index 0000000..3e02e93 --- /dev/null +++ b/templates/google/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<service> + <title>Google Video</title> + <ref>wvt:///google/search.xsl</ref> + <description>Google video search</description> +</service> diff --git a/templates/google/video.xsl b/templates/google/video.xsl new file mode 100644 index 0000000..52d6d98 --- /dev/null +++ b/templates/google/video.xsl @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="/html/head/title" /></title> + <xsl:for-each select="/html/body/script"> + <xsl:variable name="videourl" select="str:decode-uri(substring-before(substring-after(., 'videoUrl\x3d'), '\x26'))"/> + <xsl:if test="$videourl"> + <url><xsl:value-of select="$videourl"/></url> + </xsl:if> + </xsl:for-each> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/katsomo/mainmenu.xsl b/templates/katsomo/mainmenu.xsl new file mode 100644 index 0000000..b7ba1cf --- /dev/null +++ b/templates/katsomo/mainmenu.xsl @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<wvmenu> + <title>MTV3 Katsomo</title> + + <link> + <label>Haku</label> + <ref>wvt:///katsomo/search.xsl</ref> + </link> + + <xsl:for-each select="id('mainMenu')/li[a/@href != '/']"> + <link> + <label><xsl:value-of select="a"/></label> + <ref>wvt:///katsomo/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/katsomo/navigation.xsl b/templates/katsomo/navigation.xsl new file mode 100644 index 0000000..e43753d --- /dev/null +++ b/templates/katsomo/navigation.xsl @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="docurl"/> + +<xsl:template match="ol[@class='categoryList']/li"> + <link> + <label><xsl:value-of select="normalize-space(a)"/></label> + <ref>wvt:///katsomo/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> +</xsl:template> + +<xsl:template match="ol[@class='programList']/li"> + <xsl:variable name="progId" select="substring-after(a/@href, 'progId=')"/> + <xsl:variable name="treeId" select="substring-after($docurl, 'treeId=')"/> + <xsl:variable name="title" select="normalize-space(a[string(.)])"/> + + <link> + <label><xsl:value-of select="$title"/></label> + <stream>wvt:///katsomo/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://katsomo.fi/showContent.do?treeId=', $treeId, '&progId=', $progId, '&adData=%7B%22ad%22%3A%20%7B%7D%7D&ajax=true&serial=1'), true())"/>&param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0</stream> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/html/head/meta[@name='title']/@content"/></title> + + <xsl:if test="//ol[@class='categoryList']/li and //ol[@class='programList']/li"> + <textarea> + <label>Ohjelmat</label> + </textarea> + </xsl:if> + <xsl:apply-templates select="//ol[@class='categoryList']/li"/> + + <xsl:if test="//ol[@class='categoryList']/li and //ol[@class='programList']/li"> + <textarea> + <label>Jaksot</label> + </textarea> + </xsl:if> + <xsl:apply-templates select="//ol[@class='programList']/li"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/katsomo/search.xsl b/templates/katsomo/search.xsl new file mode 100644 index 0000000..c963b71 --- /dev/null +++ b/templates/katsomo/search.xsl @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTf-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<wvmenu> + <title>Haku</title> + + <textfield name="query"> + <label>Hakusana</label> + </textfield> + + <button> + <label>Hae</label> + <submission>wvt:///katsomo/searchresults.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://katsomo.fi/search.do?keywords={query}&treeId=9992', true())"/></submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/katsomo/searchresults.xsl b/templates/katsomo/searchresults.xsl new file mode 100644 index 0000000..2747afc --- /dev/null +++ b/templates/katsomo/searchresults.xsl @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="a"> + <xsl:variable name="progId" select="substring-after(@href, 'progId=')"/> + <xsl:variable name="title" select="normalize-space(.)"/> + + <link> + <label><xsl:value-of select="$title"/></label> + <stream>wvt:///katsomo/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat('http://katsomo.fi/showContent.do?progId=', $progId, '&adData=%7B%22ad%22%3A%20%7B%7D%7D&ajax=true&serial=1'), true())"/>&param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&HTTP-header=cookie,webtv.bandwidth%3D1000%3BautoFullScreen%3Dfalse%3Bwebtv.playerPlatform%3D0</stream> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Hakutulokset: <xsl:value-of select="id('searchResults')/div/div[@class='description']/span"/></title> + + <xsl:if test="not(id('resultList')/div[@class='item'])"> + <textarea> + <label><xsl:value-of select="normalize-space(id('siteMapList')/p)"/></label> + </textarea> + </xsl:if> + + <xsl:apply-templates select="id('resultList')/div[@class='item']/h6/a[not(@class='programType')]"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/katsomo/service.xml b/templates/katsomo/service.xml new file mode 100644 index 0000000..b1bd0bc --- /dev/null +++ b/templates/katsomo/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<service> + <title>MTV3 Katsomo</title> + <ref>wvt:///katsomo/mainmenu.xsl?srcurl=http%3A//katsomo.fi/</ref> + <description>Net TV service of the Finnish broadcasting company MTV3</description> +</service> diff --git a/templates/katsomo/video.xsl b/templates/katsomo/video.xsl new file mode 100644 index 0000000..9d20c49 --- /dev/null +++ b/templates/katsomo/video.xsl @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="title">katsomovideo</xsl:param> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="$title"/></title> + + <url><xsl:value-of select='substring-before(substring-after(//script, "metaUrl': '"), "'")'/></url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/categories.xsl b/templates/metacafe/categories.xsl new file mode 100644 index 0000000..7dc155e --- /dev/null +++ b/templates/metacafe/categories.xsl @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title>Metacafe</title> + + <link> + <label>Search</label> + <ref>wvt:///metacafe/search.xsl</ref> + </link> + + <link> + <label>Most viewed channels</label> + <ref>wvt:///metacafe/channellist.xsl?srcurl=/api/channels/</ref> + </link> + + <xsl:for-each select="id('LeftCol')/ul/li/a"> + <!-- '18+ Only' is empty unless family filter is off. Ignore the + category until I find a way to turn off the filter. --> + <xsl:if test="@title != '18+ Only'"> + <link> + <label><xsl:value-of select="@title"/></label> + <ref>wvt:///metacafe/navigation.xsl?srcurl=/api/videos/-/<xsl:value-of select="substring-after(@href, '/videos/')"/></ref> + </link> + </xsl:if> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/channellist.xsl b/templates/metacafe/channellist.xsl new file mode 100644 index 0000000..2bb74ec --- /dev/null +++ b/templates/metacafe/channellist.xsl @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform" +xmlns:str="http://exslt.org/strings"> + +<xsl:template match="item"> + <link> + <label><xsl:value-of select="title" /> (<xsl:value-of select="videos"/> videos, avg. rank: <xsl:value-of select="avg_rank"/>)</label> + <ref>wvt:///metacafe/navigation.xsl?srcurl=/api/users/<xsl:value-of select="str:encode-uri(translate(title, ' ', '+'), true())"/>/channel?time=all_time</ref> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/rss/channel/title"/></title> + + <xsl:apply-templates select="/rss/channel/item"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/description.xsl b/templates/metacafe/description.xsl new file mode 100644 index 0000000..3cb7f2b --- /dev/null +++ b/templates/metacafe/description.xsl @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform" +xmlns:media="http://search.yahoo.com/mrss/"> + +<!-- Convert $seconds to hours:min:sec format --> +<xsl:template name="pretty-print-seconds"> + <xsl:param name="seconds"/> + + <xsl:variable name="sec" select="$seconds mod 60"/> + <xsl:variable name="min" select="floor($seconds div 60) mod 60"/> + <xsl:variable name="hour" select="floor($seconds div 3600)"/> + + <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/rss/channel/item/title"/></title> + + <textarea> + <label><xsl:value-of select="/rss/channel/item/media:description"/></label> + </textarea> + + <textarea> + <label>Duration: <xsl:call-template name="pretty-print-seconds"> + <xsl:with-param name="seconds"> + <xsl:value-of select="/rss/channel/item/media:content/@duration"/> + </xsl:with-param> + </xsl:call-template> + </label> + </textarea> + + <textarea> + <label>Rating: <xsl:value-of select="/rss/channel/item/rank"/></label> + </textarea> + + <textarea> + <label>published: <xsl:value-of select="/rss/channel/item/pubDate"/></label> + </textarea> + + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/navigation.xsl b/templates/metacafe/navigation.xsl new file mode 100644 index 0000000..4ff821a --- /dev/null +++ b/templates/metacafe/navigation.xsl @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="item"> + <link> + <label><xsl:value-of select="title" /></label> + <xsl:choose> + <xsl:when test="starts-with(id, 'yt-')"> + <stream>wvt:///youtube/video.xsl?srcurl=http%3A//www.youtube.com/watch%3Fv=<xsl:value-of select="substring(id, 4)"/></stream> + </xsl:when> + <xsl:otherwise> + <stream>wvt:///metacafe/video.xsl?srcurl=<xsl:value-of select="link"/></stream> + </xsl:otherwise> + </xsl:choose> + + <ref>wvt:///metacafe/description.xsl?srcurl=/api/item/<xsl:value-of select="id"/></ref> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/rss/channel/title"/></title> + + <xsl:apply-templates select="/rss/channel/item"/> + + <xsl:if test="count(/rss/channel/item) = 0"> + <textarea> + <label>No matching results.</label> + </textarea> + </xsl:if> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/search.xsl b/templates/metacafe/search.xsl new file mode 100644 index 0000000..205bd98 --- /dev/null +++ b/templates/metacafe/search.xsl @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title>Metacafe Search</title> + + <textfield name="vq"> + <label>Keywords</label> + </textfield> + + <itemlist name="orderby"> + <label>Sort by</label> + <item value="updated">Most recent</item> + <item value="viewCount">View Count</item> + <item value="discussed">Most discussed</item> + </itemlist> + + <itemlist name="time"> + <label>Published</label> + <item value="all_time">Anytime</item> + <item value="today">During last 24 hours</item> + <item value="this_week">This week</item> + <item value="this_month">This month</item> + </itemlist> + + <button> + <label>Search</label> + <submission>wvt:///metacafe/navigation.xsl?srcurl=http%3A//www.metacafe.com/api/videos%3Fvq=%7Bvq%7D%26orderby=%7Borderby%7D%26time=%7Btime%7D</submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/metacafe/service.xml b/templates/metacafe/service.xml new file mode 100644 index 0000000..8e9fc33 --- /dev/null +++ b/templates/metacafe/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<service> + <title>Metacafe</title> + <ref>wvt:///metacafe/categories.xsl?srcurl=http%3A//www.metacafe.com/videos/</ref> + <description>Video sharing site specializing in short-form original content</description> +</service> diff --git a/templates/metacafe/video.xsl b/templates/metacafe/video.xsl new file mode 100644 index 0000000..884e87f --- /dev/null +++ b/templates/metacafe/video.xsl @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" +xmlns:xsl="http://www.w3.org/1999/XSL/Transform" +xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> + <mediaurl> + <title><xsl:value-of select="normalize-space(id('ItemTitle'))"/></title> + <url><xsl:value-of select="str:decode-uri(substring-before(substring-after(//param[@name='flashvars']/@value, 'mediaURL='), '&'))"/></url> + </mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/description.xsl b/templates/ruutufi/description.xsl new file mode 100644 index 0000000..ad04d79 --- /dev/null +++ b/templates/ruutufi/description.xsl @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="docurl"/> + +<!-- Convert $seconds to hours:min:sec format --> +<xsl:template name="pretty-print-seconds"> + <xsl:param name="seconds"/> + + <xsl:variable name="sec" select="$seconds mod 60"/> + <xsl:variable name="min" select="floor($seconds div 60) mod 60"/> + <xsl:variable name="hour" select="floor($seconds div 3600)"/> + + <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/Playerdata/Behavior/Program/@program_name"/></title> + + <xsl:if test="/Playerdata/Behavior/Program/@description"> + <textarea> + <label><xsl:value-of select="/Playerdata/Behavior/Program/@description"/></label> + </textarea> + </xsl:if> + + <textarea> + <label><xsl:value-of select="/Playerdata/Behavior/Program/@episode_name"/></label> + </textarea> + + <textarea> + <label>Kesto: <xsl:call-template name="pretty-print-seconds"> + <xsl:with-param name="seconds"> + <xsl:value-of select="/Playerdata/Behavior/Program/@episode_duration"/> + </xsl:with-param> + </xsl:call-template> + </label> + </textarea> + + <link> + <label>Lataa</label> + <stream>wvt:///ruutufi/video.xsl?srcurl=<xsl:value-of select="str:encode-uri($docurl, true())"/></stream> + </link> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/mainmenu.xsl b/templates/ruutufi/mainmenu.xsl new file mode 100644 index 0000000..1d70ac3 --- /dev/null +++ b/templates/ruutufi/mainmenu.xsl @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title>Ruutu.fi</title> + + <link> + <label>Haku</label> + <ref>wvt:///ruutufi/search.xsl</ref> + </link> + + <link> + <label>Listaa sarjat</label> + <ref>wvt:///ruutufi/series.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_netti_tv_series_list/all/false&postprocess=json2xml</ref> + </link> + + <link> + <label>Uusimmat</label> + <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/latestdesc/0/25/true/__&postprocess=json2xml</ref> + </link> + + <link> + <label>Katsotuimmat</label> + <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_media/all/video_episode/__/most_watched/0/25/true/__&postprocess=json2xml</ref> + </link> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/program.xsl b/templates/ruutufi/program.xsl new file mode 100644 index 0000000..593036f --- /dev/null +++ b/templates/ruutufi/program.xsl @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="utf-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="docurl"/> + +<xsl:template match="dict"> + <xsl:param name="mediatype" select="video"/> + + <xsl:variable name="videoid"> + <xsl:choose> + <xsl:when test="video_id_to_use"> + <xsl:value-of select="video_id_to_use"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="substring-after(nodeurl, 'vid=')"/> + </xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <link> + <label> + <xsl:choose> + <xsl:when test="program_episode_name"> + <xsl:value-of select="concat(program_episode_name, ' ', video_datetime_to_use)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="title"/> + </xsl:otherwise> + </xsl:choose> + </label> + + <xsl:variable name="videourl">http://www.nelonen.fi/utils/video_config/%3Fq%3D<xsl:value-of select="$mediatype"/>/<xsl:value-of select="$videoid"/>%26site%3Dwww.ruutu.fi%26ageCheckURL%3Dhttp://sso.nelonenmedia.fi/ajax/check_age/%26current_page%3Dhttp://www.ruutu.fi/video</xsl:variable> + + <ref>wvt:///ruutufi/description.xsl?srcurl=<xsl:value-of select="$videourl"/></ref> + <stream>wvt:///ruutufi/video.xsl?srcurl=<xsl:value-of select="$videourl"/></stream> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <xsl:variable name="start"> + <xsl:value-of select="number(str:tokenize($docurl, '/')[9])"/> + </xsl:variable> + + <!-- title --> + <title> + <xsl:choose> + <xsl:when test="/jsondocument/dict/video_episode/list/li[1]/dict/series_name"> + <xsl:value-of select="/jsondocument/dict/video_episode/list/li[1]/dict/series_name"/> + </xsl:when> + <xsl:when test="/jsondocument/dict/video/list/li[1]/dict/clip_series_name"> + <xsl:value-of select="/jsondocument/dict/video/list/li[1]/dict/clip_series_name"/> + </xsl:when> + <xsl:otherwise>Ruutu.fi</xsl:otherwise> + </xsl:choose> + </title> + + <!-- Video links --> + <xsl:if test="not(/jsondocument/dict/video | /jsondocument/dict/video_episode)"> + <textarea> + <label>Ei jaksoja</label> + </textarea> + </xsl:if> + + <xsl:apply-templates select="/jsondocument/dict/video_episode/list/li/dict"> + <xsl:with-param name="mediatype">video_episode</xsl:with-param> + </xsl:apply-templates> + <xsl:apply-templates select="/jsondocument/dict/video/list/li/dict"> + <xsl:with-param name="mediatype">video</xsl:with-param> + </xsl:apply-templates> + + <xsl:if test="contains($docurl, '/video_episode/') and ($start = 0)"> + <link> + <label>Klipit</label> + <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:replace($docurl, '/video_episode/', '/video/')"/>&postprocess=json2xml</ref> + </link> + </xsl:if> + + <!-- prev/next links --> + <xsl:variable name="total"> + <xsl:value-of select="number(/jsondocument/dict/total_count)"/> + </xsl:variable> + + <xsl:variable name="urlend"> + <xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[10]"/><xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[11]"/><xsl:text>/</xsl:text><xsl:value-of select="str:tokenize($docurl, '/')[12]"/> + </xsl:variable> + + <xsl:variable name="prevstart"> + <xsl:choose> + <xsl:when test="$start >= 25"> + <xsl:value-of select="string($start - 25)"/> + </xsl:when> + <xsl:otherwise> + <xsl:text>0</xsl:text> + </xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <xsl:if test="$start > 0"> + <link> + <label>Edellinen</label> + <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri(str:replace($docurl, concat(string($start), $urlend), concat($prevstart, $urlend)), true())"/>&postprocess=json2xml</ref> + </link> + </xsl:if> + + <xsl:if test="$start + 25 < $total"> + <link> + <label>Seuraava</label> + <ref>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri(str:replace($docurl, concat(string($start), $urlend), concat(string($start+25), $urlend)), true())"/>&postprocess=json2xml</ref> + </link> + </xsl:if> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/search.xsl b/templates/ruutufi/search.xsl new file mode 100644 index 0000000..07f2700 --- /dev/null +++ b/templates/ruutufi/search.xsl @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTf-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<wvmenu> + <title>Haku</title> + + <textfield name="query"> + <label>Hakusana</label> + </textfield> + + <button> + <label>Hae</label> + <submission>wvt:///ruutufi/program.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://www.ruutu.fi/search/search_new.php?params=%7B%22search%22%3A%22{query}%22%2C%22groups%22%3A%7B%22video%22%3A%7B%22types%22%3A%5B%22video_clip%22%5D%7D%2C%22video_episode%22%3A%7B%22types%22%3A%5B%22video_episode%22%5D%7D%2C%22audio%22%3A%7B%22types%22%3A%5B%22audio%22%5D%7D%7D%7D', true())"/>&postprocess=json2xml</submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/series.xsl b/templates/ruutufi/series.xsl new file mode 100644 index 0000000..2e8f4d2 --- /dev/null +++ b/templates/ruutufi/series.xsl @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:output method="xml" version="1.0" encoding="UTF-8" /> + +<xsl:template match="dict"> + <xsl:if test="is_video=1"> + <link> + <label><xsl:value-of select="name"/></label> + <ref>wvt:///ruutufi/program.xsl?srcurl=http://www.ruutu.fi/ajax/media_get_nettitv_video/all/video_episode/<xsl:value-of select="str:encode-uri(str:encode-uri(url_encode_name, true()), true())"/>/latestdesc/0/25/true/__&postprocess=json2xml</ref> + <!-- Yes, ruutu.fi really expects url_encode_name to be double-url-encoded! --> + </link> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Kaikki sarjat</title> + + <xsl:apply-templates select="/jsondocument/list/li/dict"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/service.xml b/templates/ruutufi/service.xml new file mode 100644 index 0000000..7a106aa --- /dev/null +++ b/templates/ruutufi/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<service> + <title>ruutu.fi</title> + <ref>wvt:///ruutufi/mainmenu.xsl?srcurl=http%3A//www.ruutu.fi/</ref> + <description>Net TV service of the Finnish broadcasting company Nelonen</description> +</service> diff --git a/templates/ruutufi/video.xsl b/templates/ruutufi/video.xsl new file mode 100644 index 0000000..e6af547 --- /dev/null +++ b/templates/ruutufi/video.xsl @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="concat(/Playerdata/Behavior/Program/@program_name, ' ', /Playerdata/Behavior/Program/@episode_name)"/></title> + + <xsl:choose> + <xsl:when test="starts-with(/Playerdata/Clip/SourceFile, 'rtmp://')"> + <url priority="50">wvt:///bin/ruutu-dl?contenttype=video/x-flv&arg=<xsl:value-of select="str:encode-uri(/Playerdata/Clip/SourceFile, true())"/>&arg=http://www.ruutu.fi/video</url> + </xsl:when> + <xsl:otherwise> + <url priority="50"><xsl:value-of select="/Playerdata/Clip/SourceFile"/></url> + </xsl:otherwise> + </xsl:choose> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/ruutufi/video2.xsl b/templates/ruutufi/video2.xsl new file mode 100644 index 0000000..39bef06 --- /dev/null +++ b/templates/ruutufi/video2.xsl @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="concat(id('ruutuVideoInfo')/p[@class='name'], ' ', id('ruutuVideoInfo')/p[@class='timeStamp'])"/></title> + + <url priority="50">wvt:///bin/ruutu-dl?contenttype=video/x-flv&arg=<xsl:value-of select='substring-before(substring-after(//script[contains(., "vplayer1")], "providerURL', '"), "'")'/>&arg=<xsl:value-of select="str:encode-uri($docurl, true())"/></url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/subtv/description.xsl b/templates/subtv/description.xsl new file mode 100644 index 0000000..c914f3b --- /dev/null +++ b/templates/subtv/description.xsl @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="title"/> +<xsl:param name="desc"/> +<xsl:param name="pubdate"/> +<xsl:param name="pid"/> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="$title"/></title> + + <textarea> + <label><xsl:value-of select="$desc"/></label> + </textarea> + + <textarea> + <label><xsl:value-of select="$pubdate"/></label> + </textarea> + + <link> + <label>Lataa</label> + <stream>wvt:///subtv/video.xsl?param=pid,<xsl:value-of select="$pid"/>&param=title,<xsl:value-of select="str:encode-uri($title, true())"/></stream> + </link> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/subtv/mainmenu.xsl b/templates/subtv/mainmenu.xsl new file mode 100644 index 0000000..2158295 --- /dev/null +++ b/templates/subtv/mainmenu.xsl @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<wvmenu> + <title>Subin netti-TV</title> + + <xsl:for-each select="//div[@class='netissakaikki']/ul/li/a"> + <link> + <label><xsl:value-of select="."/></label> + <ref>wvt:///subtv/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref> + </link> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/subtv/navigation.xsl b/templates/subtv/navigation.xsl new file mode 100644 index 0000000..3c0b039 --- /dev/null +++ b/templates/subtv/navigation.xsl @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="docurl"/> +<xsl:variable name="programname" select="id('page')/div[@class='ohjelma_yla ohjelmanavi']/h1"/> + +<xsl:template match="li"> + <xsl:variable name="progId" select="substring-after(div[@class='outerwrap']//a/@href, '?')"/> + <xsl:variable name="title" select="concat($programname, ' - ', normalize-space(.//h5))"/> + + <xsl:if test="$progId"> + <link> + <label><xsl:value-of select="normalize-space(.//h5)"/></label> + <stream>wvt:///subtv/video.xsl?srcurl=<xsl:value-of select="str:encode-uri($docurl, true())"/>&param=pid,<xsl:value-of select="$progId"/>&param=title,<xsl:value-of select="str:encode-uri($title, true())"/></stream> + <ref>wvt:///subtv/description.xsl?param=title,<xsl:value-of select="str:encode-uri($title, true())"/>&param=desc,<xsl:value-of select="str:encode-uri(.//span[@class='verho_content']/div, true())"/>&param=pubdate,<xsl:value-of select="str:encode-uri(p[@class='julkaistu'], true())"/>&param=pid,<xsl:value-of select="$progId"/></ref> + </link> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="$programname"/></title> + + <xsl:choose> + <xsl:when test="id('uusimmat')/li"> + <xsl:apply-templates select="id('uusimmat')/li"/> + </xsl:when> + <xsl:otherwise> + <textarea> + <label>Ei jaksoja</label> + </textarea> + </xsl:otherwise> + </xsl:choose> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/subtv/service.xml b/templates/subtv/service.xml new file mode 100644 index 0000000..6a7a44f --- /dev/null +++ b/templates/subtv/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<service> + <title>Subtv</title> + <ref>wvt:///subtv/mainmenu.xsl?srcurl=http%3A//www.sub.fi/katsonetista/</ref> + <description>Sub is the third biggest commercial tv channel in Finland.</description> +</service> diff --git a/templates/subtv/video.xsl b/templates/subtv/video.xsl new file mode 100644 index 0000000..32e5b1e --- /dev/null +++ b/templates/subtv/video.xsl @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:param name="title"/> +<xsl:param name="pid"/> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="$title"/></title> + + <url><xsl:value-of select="concat('http://www.katsomo.fi/metafile.asx?p=', $pid, '&bw=800')"/></url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/svtplay/categories.xsl b/templates/svtplay/categories.xsl new file mode 100644 index 0000000..bd64abd --- /dev/null +++ b/templates/svtplay/categories.xsl @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title>SVT Play</title> + + <xsl:for-each select="//div[@id='categorylist']//ul/li//a"> + <link> + <label><xsl:value-of select="span[@class='category-header']"/></label> + <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="@href"/></ref> + </link> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/svtplay/description.xsl b/templates/svtplay/description.xsl new file mode 100644 index 0000000..f3c3ae6 --- /dev/null +++ b/templates/svtplay/description.xsl @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="div[@class='info']/ul"> + <textarea> + <label> + <xsl:value-of select="normalize-space(li[@class='title']/div)"/> + </label> + </textarea> + <textarea> + <label> + <xsl:value-of select="normalize-space(li[@class='episode']/div)"/> + </label> + </textarea> + <textarea> + <label> + <xsl:value-of select="concat(normalize-space(li[1]/span[2]), ' ', normalize-space(li/span[2]/following-sibling::text()))"/> + </label> + </textarea> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title> + <xsl:choose> + <xsl:when test="normalize-space(//h1/a/img/@alt)"> + <xsl:value-of select="concat(normalize-space(//h1/a/img/@alt), ' ', //div[@class='info']//h2)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="concat(normalize-space(//h1/a), ' ', //div[@class='info']//h2)"/> + </xsl:otherwise> + </xsl:choose> + </title> + + <xsl:apply-templates select="//div[@class='info']/ul"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/svtplay/navigation.xsl b/templates/svtplay/navigation.xsl new file mode 100644 index 0000000..7071dfe --- /dev/null +++ b/templates/svtplay/navigation.xsl @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="text()" /> + +<xsl:template match="div[@id='pb']"> + <xsl:apply-templates/> +</xsl:template> + +<xsl:template match="div[@id='sb']"> + <xsl:apply-templates/> +</xsl:template> + +<xsl:template match="div[@id='se']"> + <xsl:apply-templates/> +</xsl:template> + +<!-- Programs --> +<xsl:template match="div[@class='content']//ul/li/a[1]"> + <link> + <label><xsl:value-of select="normalize-space(span)"/></label> + <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref> + </link> +</xsl:template> + +<!-- next/prev links --> +<xsl:template match="div[@class='footer']/div[@class='pagination']/ul[@class='pagination program']/li"> + <xsl:if test="@class='prev '"> + <link> + <label><xsl:value-of select="a/img/@alt"/></label> + <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> + </xsl:if> + + <xsl:if test="@class='next '"> + <link> + <label><xsl:value-of select="a/img/@alt"/></label> + <ref>wvt:///svtplay/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title> + <xsl:choose> + <xsl:when test="normalize-space(//h1)"> + <xsl:value-of select="normalize-space(//h1)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="normalize-space(//h1/a/img/@alt)"/> + </xsl:otherwise> + </xsl:choose> + </title> + + <!-- In most categories the content is in pb and se nodes, except + for Nyheter and Sport, where the content is in sb and se nodes. + On the other hand, we can't match sb unconditionally because in + Öppet arkiv sb contains klips instead of programs! --> + <xsl:choose> + <xsl:when test="//div[@id='pb']"> + <xsl:apply-templates select="//div[@id='pb']|//div[@id='se']"/> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates select="//div[@id='sb']|//div[@id='se']"/> + </xsl:otherwise> + </xsl:choose> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/svtplay/programmenu.xsl b/templates/svtplay/programmenu.xsl new file mode 100644 index 0000000..4bd120c --- /dev/null +++ b/templates/svtplay/programmenu.xsl @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="text()" /> + +<xsl:template match="div[@id='pb']"> + <xsl:apply-templates/> +</xsl:template> + +<!-- Broadcasts --> +<xsl:template match="div[@class='content']//ul/li/a"> + <link> + <label><xsl:value-of select="normalize-space(span)"/></label> + <ref>wvt:///svtplay/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref> + <stream>wvt:///svtplay/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></stream> + </link> +</xsl:template> + +<!-- next/prev links --> +<xsl:template match="div[@class='footer']/div[@class='pagination']/ul[@class='pagination program']/li"> + <xsl:if test="@class='prev '"> + <link> + <label><xsl:value-of select="a/img/@alt"/></label> + <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> + </xsl:if> + + <xsl:if test="@class='next '"> + <link> + <label><xsl:value-of select="a/img/@alt"/></label> + <ref>wvt:///svtplay/programmenu.xsl?srcurl=<xsl:value-of select="str:encode-uri(a/@href, true())"/></ref> + </link> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title> + <xsl:choose> + <xsl:when test="normalize-space(//h1/a)"> + <xsl:value-of select="normalize-space(//h1/a)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="normalize-space(//h1/a/img/@alt)"/> + </xsl:otherwise> + </xsl:choose> + </title> + + <xsl:apply-templates select="//div[@id='sb']"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/svtplay/service.xml b/templates/svtplay/service.xml new file mode 100644 index 0000000..86a36f6 --- /dev/null +++ b/templates/svtplay/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<service> + <title>SVT Play</title> + <ref>wvt:///svtplay/categories.xsl?srcurl=http://svtplay.se/kategorier</ref> + <description>Swedish Television, online TV service</description> +</service> diff --git a/templates/svtplay/video.xsl b/templates/svtplay/video.xsl new file mode 100644 index 0000000..af6aeb9 --- /dev/null +++ b/templates/svtplay/video.xsl @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<mediaurl> + <title> + <xsl:choose> + <xsl:when test="normalize-space(//h1/a/img/@alt)"> + <xsl:value-of select="concat(normalize-space(//h1/a/img/@alt), ' ', //div[@class='info']//h2)"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="concat(normalize-space(//h1/a), ' ', //div[@class='info']//h2)"/> + </xsl:otherwise> + </xsl:choose> + </title> + + <url priority="50"><xsl:value-of select="substring-before(substring-after((//object/param[@name='flashvars'])[1]/@value, 'pathflv='), '&')"/></url> + <url priority="40"><xsl:value-of select="//a[@class='external-player']/@href"/></url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/channels.xsl b/templates/vimeo/channels.xsl new file mode 100644 index 0000000..ae3c0d8 --- /dev/null +++ b/templates/vimeo/channels.xsl @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Vimeo Channels</title> + + <xsl:for-each select="//div[@class='title']/a"> + <link> + <label><xsl:value-of select="."/></label> + <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/channel/<xsl:value-of select="str:split(@href, '/')[last()]"/>/videos.xml</ref> + </link> + </xsl:for-each> + + <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a"> + <link> + <xsl:if test="img/@alt = 'previous'"> + <label>Previous</label> + </xsl:if> + <xsl:if test="img/@alt = 'next'"> + <label>Next</label> + </xsl:if> + <ref>wvt:///vimeo/channels.xsl?srcurl=<xsl:value-of select="./@href"/></ref> + </link> + </xsl:for-each> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/description.xsl b/templates/vimeo/description.xsl new file mode 100644 index 0000000..a8797cd --- /dev/null +++ b/templates/vimeo/description.xsl @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<!-- Convert $seconds to hours:min:sec format --> +<xsl:template name="pretty-print-seconds"> + <xsl:param name="seconds"/> + + <xsl:variable name="sec" select="$seconds mod 60"/> + <xsl:variable name="min" select="floor($seconds div 60) mod 60"/> + <xsl:variable name="hour" select="floor($seconds div 3600)"/> + + <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/videos/video/title"/></title> + <textarea> + <label><xsl:value-of select="/videos/video/description"/></label> + </textarea> + + <textarea> + <label>Duration: <xsl:call-template name="pretty-print-seconds"> + <xsl:with-param name="seconds"> + <xsl:value-of select="/videos/video/duration"/> + </xsl:with-param> + </xsl:call-template> + </label> + </textarea> + + <textarea> + <label>Views: <xsl:value-of select="/videos/video/stats_number_of_plays"/></label> + </textarea> + + <textarea> + <label>Likes: <xsl:value-of select="/videos/video/stats_number_of_likes"/></label> + </textarea> + + <textarea> + <label>Published: <xsl:value-of select="/videos/video/upload_date"/></label> + </textarea> + + <link> + <label>More videos by <xsl:value-of select="/videos/video/user_name"/></label> + <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/<xsl:value-of select="str:split(/videos/video/user_url, '/')[last()]"/>/videos.xml</ref> + </link> + + <link> + <label>Download this video</label> + <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="/videos/video/id"/></stream> + </link> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/groups.xsl b/templates/vimeo/groups.xsl new file mode 100644 index 0000000..2379058 --- /dev/null +++ b/templates/vimeo/groups.xsl @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Vimeo Groups</title> + + <xsl:for-each select="//div[@class='title']/a"> + <link> + <label><xsl:value-of select="."/></label> + <ref>wvt:///vimeo/navigation.xsl?srcurl=http://vimeo.com/api/v2/group/<xsl:value-of select="str:split(@href, '/')[last()]"/>/videos.xml</ref> + </link> + </xsl:for-each> + + <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a"> + <link> + <xsl:if test="img/@alt = 'previous'"> + <label>Previous</label> + </xsl:if> + <xsl:if test="img/@alt = 'next'"> + <label>Next</label> + </xsl:if> + <ref>wvt:///vimeo/groups.xsl?srcurl=<xsl:value-of select="./@href"/></ref> + </link> + </xsl:for-each> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/mainmenu.xsl b/templates/vimeo/mainmenu.xsl new file mode 100644 index 0000000..3667ed7 --- /dev/null +++ b/templates/vimeo/mainmenu.xsl @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title>Vimeo</title> + + <link> + <label>Search</label> + <ref>wvt:///vimeo/search.xsl?srcurl=http://www.vimeo.com/</ref> + </link> + + <link> + <label>Channels</label> + <ref>wvt:///vimeo/channels.xsl?srcurl=http://www.vimeo.com/channels/all</ref> + </link> + + <link> + <label>Groups</label> + <ref>wvt:///vimeo/groups.xsl?srcurl=http://www.vimeo.com/groups/all</ref> + </link> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/navigation.xsl b/templates/vimeo/navigation.xsl new file mode 100644 index 0000000..8583212 --- /dev/null +++ b/templates/vimeo/navigation.xsl @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="video"> + <link> + <label><xsl:value-of select="title"/></label> + <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="id"/></stream> + <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="id"/>.xml</ref> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Vimeo videos</title> + + <xsl:apply-templates select="/videos/video"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/search.xsl b/templates/vimeo/search.xsl new file mode 100644 index 0000000..4939e25 --- /dev/null +++ b/templates/vimeo/search.xsl @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Vimeo Search</title> + + <textfield name="keywords"> + <label>Search terms</label> + </textfield> + + <itemlist name="orderby"> + <label>Show me</label> + <item value="">most relevant</item> + <item value="/sort:newest">newest</item> + <item value="/sort:plays">most played</item> + <item value="/sort:likes">most liked</item> + </itemlist> + + <button> + <label>Search</label> + <submission>wvt:///vimeo/searchresults.xsl?srcurl=<xsl:value-of select="concat(str:encode-uri('http://vimeo.com/videos/search:', true()), '{keywords}/', substring(id('xsrft')/@value, 0, 9), '{orderby}')"/>&HTTP-header=cookie,xsrft%3D<xsl:value-of select="substring(id('xsrft')/@value, 0, 9)"/></submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/searchresults.xsl b/templates/vimeo/searchresults.xsl new file mode 100644 index 0000000..6f5d817 --- /dev/null +++ b/templates/vimeo/searchresults.xsl @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>Search results</title> + + <xsl:for-each select="//div[@class='title']/a"> + <link> + <label><xsl:value-of select="."/></label> + <stream>wvt:///vimeo/video.xsl?srcurl=http://www.vimeo.com/moogaloop/load/clip:<xsl:value-of select="str:split(@href, '/')[last()]"/></stream> + <ref>wvt:///vimeo/description.xsl?srcurl=http://vimeo.com/api/v2/video/<xsl:value-of select="str:split(@href, '/')[last()]"/>.xml</ref> + </link> + </xsl:for-each> + + <xsl:for-each select="//div[@class='pagination']/ul/li[@class='arrow']/a"> + <link> + <xsl:if test="img/@alt = 'previous'"> + <label>Previous</label> + </xsl:if> + <xsl:if test="img/@alt = 'next'"> + <label>Next</label> + </xsl:if> + <ref>wvt:///vimeo/searchresults.xsl?srcurl=<xsl:value-of select="./@href"/></ref> + </link> + </xsl:for-each> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/vimeo/service.xml b/templates/vimeo/service.xml new file mode 100644 index 0000000..77af401 --- /dev/null +++ b/templates/vimeo/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<service> + <title>Vimeo</title> + <ref>wvt:///vimeo/mainmenu.xsl</ref> + <description>Vimeo is a video-centric social networking site</description> +</service> diff --git a/templates/vimeo/video.xsl b/templates/vimeo/video.xsl new file mode 100644 index 0000000..3b1b7a9 --- /dev/null +++ b/templates/vimeo/video.xsl @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<mediaurl> + <title><xsl:value-of select="/xml/video/caption"/></title> + + <url priority="50">http://www.vimeo.com/moogaloop/play/clip:<xsl:value-of select="/xml/video/nodeId"/>/<xsl:value-of select="/xml/request_signature"/>/<xsl:value-of select="/xml/request_signature_expires"/>/?q=sd</url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/description.xsl b/templates/yleareena/description.xsl new file mode 100644 index 0000000..2340cb2 --- /dev/null +++ b/templates/yleareena/description.xsl @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="normalize-space(//h1[@class='cliptitle'])"/></title> + <textarea> + <label><xsl:value-of select="normalize-space(id('relatedinfo')//div[@class='relatedinfo-text description'])"/></label> + </textarea> + <textarea> + <!-- Kesto --> + <label><xsl:value-of select="id('relatedinfo')/div/div/div[@class='relatedinfo-text meta']/ul/li[contains(., 'Kesto')]"/></label> + </textarea> + <textarea> + <!-- Julkaistu --> + <label><xsl:value-of select="id('relatedinfo-more')/div/div[1]/ul/li[contains(., 'Julkaistu')]"/></label> + </textarea> + <textarea> + <!-- Kieli --> + <label><xsl:value-of select="id('relatedinfo-more')/div/div[2]/ul[1]/li[1]"/></label> + </textarea> + <textarea> + <!-- Kanava --> + <label><xsl:value-of select="id('relatedinfo')//div[@class='relatedinfo-text meta']/ul/li[1]"/></label> + </textarea> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/livebroadcasts.xsl b/templates/yleareena/livebroadcasts.xsl new file mode 100644 index 0000000..865fcee --- /dev/null +++ b/templates/yleareena/livebroadcasts.xsl @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="text()"/> + +<!-- Käynnissä olevat lähetykset --> +<xsl:template match="div[@class='ongoing']//div[@class='showlistitem-description']"> + <link> + <label><xsl:value-of select="a"/></label> + <stream>wvt:///yleareena/livestream.xsl?param=stream,<xsl:value-of select='substring-before(substring-after(a/@onclick, "stream', '"), "'")'/></stream> + </link> +</xsl:template> + +<!-- "Aina suorana" --> +<xsl:template match="div[contains(@class, 'live-container')]"> + <link> + <label><xsl:value-of select="h2/span/a"/></label> + <stream>wvt:///yleareena/livestream.xsl?param=stream,<xsl:value-of select='substring-before(substring-after(h2/span/a/@onclick, "stream', '"), "'")'/></stream> + </link> +</xsl:template> + +<!-- Tulevat lähetykset --> +<xsl:template match="div[@class='upcoming']/div/div[@class='showlistitem-description']"> + <textarea> + <label><xsl:value-of select="h3"/>, <xsl:value-of select="ul/li[1]"/></label> + </textarea> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Suorat lähetykset</title> + + <xsl:apply-templates select="id('liveshows')/div[@class='ongoing']"/> + + <xsl:apply-templates select="id('liveshows')/div/div[contains(@class, 'live-container')]"/> + + <textarea> + <label>Tulossa seuraavaksi:</label> + </textarea> + <xsl:apply-templates select="id('liveshows')/div[@class='upcoming']"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/livestream.xsl b/templates/yleareena/livestream.xsl new file mode 100644 index 0000000..b6d7ee2 --- /dev/null +++ b/templates/yleareena/livestream.xsl @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:param name="stream"></xsl:param> + +<xsl:template match="/"> + <mediaurl> + <title>livestream-<xsl:value-of select="$stream"/></title> + <xsl:choose> + <xsl:when test="$stream"> + <url>wvt:///bin/yle-dl?contenttype=video/x-flv&arg=http%3A//areena.yle.fi/player/index.php%3Fstream%3D<xsl:value-of select="$stream"/>%26language%3Dfi</url> + </xsl:when> + <xsl:otherwise> + <url/> + </xsl:otherwise> + </xsl:choose> +</mediaurl> + +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/mainmenu.xsl b/templates/yleareena/mainmenu.xsl new file mode 100644 index 0000000..d17ede6 --- /dev/null +++ b/templates/yleareena/mainmenu.xsl @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="/"> +<wvmenu> + <title>YLE Areena</title> + + <link> + <label>Haku</label> + <ref>wvt:///yleareena/search.xsl?srcurl=http://areena.yle.fi/haku</ref> + </link> + + <link> + <label>Suorat lähetykset</label> + <ref>wvt:///yleareena/livebroadcasts.xsl?srcurl=http://areena.yle.fi/live</ref> + </link> + + <link> + <label>Kaikki ohjelmat</label> + <ref>wvt:///yleareena/programlist.xsl?srcurl=http://areena.yle.fi/ohjelmat</ref> + </link> + + <xsl:for-each select="//div[h4='Sisältö aihealueittain']/ul/li/a"> + <link> + <label><xsl:value-of select="."/></label> + <ref><xsl:value-of select="concat('wvt:///yleareena/navigation.xsl?srcurl=', str:encode-uri(concat(./@href, '/feed/rss'), true()))"/></ref> + </link> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/navigation.xsl b/templates/yleareena/navigation.xsl new file mode 100644 index 0000000..bbf0ad7 --- /dev/null +++ b/templates/yleareena/navigation.xsl @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:param name="docurl"/> +<xsl:param name="title" select="/rss/channel/title"/> + +<xsl:template name="prevnextlinks"> + <!-- Add previous and next links for a navigation page. + + Extract the current page number from the URL (the number after + /sivu/) and adds links to previous and following pages. If the + page number is missing, it is assumed to be 1. + + BUG: if the last page has 20 links, an extra "next" link is + generated + --> + + <xsl:variable name="page" select="number(substring-before(substring-after($docurl, '/sivu/'), '/'))"/> + + <xsl:choose> + <xsl:when test="$page > 1"> + + <xsl:variable name="urlprefix" select="substring-before($docurl, '/sivu/')"/> + <xsl:variable name="urlpostfix" select="substring-after(substring-after($docurl, '/sivu/'), '/')"/> + + <xsl:variable name="prevurl" select="concat($urlprefix, '/sivu/', $page - 1, '/', $urlpostfix)"/> + <xsl:variable name="nexturl" select="concat($urlprefix, '/sivu/', $page + 1, '/', $urlpostfix)"/> + + <link> + <label>Edellinen</label> + <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($prevurl, true())"/></ref> + </link> + + <xsl:if test="count(/rss/channel/item) >= 20"> + <link> + <label>Seuraava</label> + <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($nexturl, true())"/></ref> + </link> + </xsl:if> + </xsl:when> + + <xsl:otherwise> + + <xsl:if test="count(/rss/channel/item) >= 20"> + <xsl:variable name="nexturl"> + <xsl:choose> + <xsl:when test="contains($docurl, '/sivu/')"> + <xsl:value-of select="concat(substring-before($docurl, '/sivu/'), '/sivu/2/', substring-after(substring-after($docurl, '/sivu/'), '/'))"/> + </xsl:when> + + <xsl:when test="contains($docurl, '/feed/rss')"> + <xsl:value-of select="str:replace($docurl, '/feed/rss', '/sivu/2/feed/rss')"/> + </xsl:when> + + <xsl:otherwise> + <xsl:value-of select="concat($docurl, '/sivu/2')"/> + </xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <link> + <label>Seuraava</label> + <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri($nexturl, true())"/></ref> + </link> + </xsl:if> + + </xsl:otherwise> + + + </xsl:choose> +</xsl:template> + + +<xsl:template match="/rss/channel/item"> + <link> + <label><xsl:value-of select="title"/></label> + <ref>wvt:///yleareena/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(link, true())"/></ref> + <stream>wvt:///yleareena/video.xsl?srcurl=<xsl:value-of select="str:encode-uri(link, true())"/>&param=title,<xsl:value-of select="str:encode-uri(concat(title, '-', str:split(pubDate, ' ')[2], '-', str:split(pubDate, ' ')[3], '-', str:split(pubDate, ' ')[4]), true())"/></stream> + </link> +</xsl:template> + + +<xsl:template match="/"> +<wvmenu> + <xsl:choose> + + <!-- Regular video links --> + <xsl:when test="/rss"> + <title><xsl:value-of select="$title"/></title> + + <xsl:apply-templates select="/rss/channel/item"/> + + <xsl:call-template name="prevnextlinks"/> + </xsl:when> + + <!-- No search results --> + <xsl:otherwise> + <title>Hae Areenasta: Ei osumia</title> + + <textarea> + <xsl:choose> + <xsl:when test="//h4"> + <label><xsl:value-of select="//h4"/></label> + </xsl:when> + <xsl:otherwise> + <label>Ei osumia</label> + </xsl:otherwise> + </xsl:choose> + </textarea> + </xsl:otherwise> + + </xsl:choose> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/programlist.xsl b/templates/yleareena/programlist.xsl new file mode 100644 index 0000000..0a4ece4 --- /dev/null +++ b/templates/yleareena/programlist.xsl @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="tr"> + <link> + <label><xsl:value-of select="td[1]/a"/></label> + <ref>wvt:///yleareena/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(concat(td[1]/a/@href, '/feed/rss'), true())"/>&param=title,<xsl:value-of select="str:encode-uri(td[1]/a, true())"/></ref> + </link> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Ohjelmat A-Ö</title> + + <xsl:apply-templates select="id('programlist-ao')/table/tbody/tr[td]"/> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/search.xsl b/templates/yleareena/search.xsl new file mode 100644 index 0000000..fa487f4 --- /dev/null +++ b/templates/yleareena/search.xsl @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:template match="fieldset"> + <xsl:if test="select"> + <itemlist> + <xsl:attribute name="name"><xsl:value-of select="select/@name"/></xsl:attribute> + <label><xsl:value-of select="label"/></label> + <xsl:for-each select="select/option|select/optgroup/option"> + <item> + <xsl:attribute name="value"><xsl:value-of select="@value"/></xsl:attribute> + <xsl:value-of select="."/> + </item> + </xsl:for-each> + </itemlist> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title>Hae Areenasta</title> + + <textfield name="keyword"> + <label>Hakusana</label> + </textfield> + + <xsl:apply-templates select="id('widesearch')/form/fieldset[not(contains(@class, 'search-keyword'))]"/> + + <itemlist name="naytetaan_ulkomailla"> + <label>Vain Suomen ulkopuolella katsottavat</label> + <item value="kaikki">Kaikki</item> + <item value="kylla">Kyllä</item> + </itemlist> + + <button> + <label>Hae</label> + <submission>wvt:///yleareena/navigation.xsl?srcurl=http%3A//areena.yle.fi/haku/{category}/uusimmat/hakusana/{keyword}/kanava/{channel}/media/{mediatype}/julkaistu/{date}/kieli/{language}/naytetaan_ulkomailla/{naytetaan_ulkomailla}/feed/rss</submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/yleareena/service.xml b/templates/yleareena/service.xml new file mode 100644 index 0000000..0d7aa03 --- /dev/null +++ b/templates/yleareena/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<service> + <title>YLE Areena</title> + <ref>wvt:///yleareena/mainmenu.xsl?srcurl=http%3A//areena.yle.fi/</ref> + <description>Video service by YLE, the Finland's national public service broadcasting company</description> +</service> diff --git a/templates/yleareena/video.xsl b/templates/yleareena/video.xsl new file mode 100644 index 0000000..f0c2d6a --- /dev/null +++ b/templates/yleareena/video.xsl @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings"> + +<xsl:param name="title"/> +<xsl:param name="docurl"/> + +<xsl:template match="/"> + <mediaurl> + <title><xsl:value-of select="$title"/></title> + <url>wvt:///bin/yle-dl?contenttype=video/x-flv&arg=<xsl:value-of select="str:encode-uri($docurl, true())"/></url> +</mediaurl> + +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/youtube/categories.xsl b/templates/youtube/categories.xsl new file mode 100644 index 0000000..080218b --- /dev/null +++ b/templates/youtube/categories.xsl @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + xmlns:app="http://www.w3.org/2007/app" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:yt="http://gdata.youtube.com/schemas/2007" + exclude-result-prefixes="str app atom yt"> + +<xsl:template match="/"> +<wvmenu> + <title>Youtube</title> + + <link> + <label>Search</label> + <ref>wvt:///youtube/search.xsl</ref> + </link> + + <xsl:for-each select="/app:categories/atom:category[yt:browsable]"> + <link> + <label><xsl:value-of select="@label"/></label> + <ref>wvt:///youtube/navigation.xsl?srcurl=http://gdata.youtube.com/feeds/api/standardfeeds/most_popular_<xsl:value-of select="str:encode-uri(@term, true())"/>%3Fmax-results%3D20%26v%3D2</ref> + </link> + </xsl:for-each> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/youtube/description.xsl b/templates/youtube/description.xsl new file mode 100644 index 0000000..e728961 --- /dev/null +++ b/templates/youtube/description.xsl @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:media="http://search.yahoo.com/mrss/" + xmlns:gd="http://schemas.google.com/g/2005" + xmlns:yt="http://gdata.youtube.com/schemas/2007" + exclude-result-prefixes="atom str media gd yt"> + +<!-- Convert $seconds to hours:min:sec format --> +<xsl:template name="pretty-print-seconds"> + <xsl:param name="seconds"/> + + <xsl:variable name="sec" select="$seconds mod 60"/> + <xsl:variable name="min" select="floor($seconds div 60) mod 60"/> + <xsl:variable name="hour" select="floor($seconds div 3600)"/> + + <xsl:value-of select="concat($hour, ':', format-number($min, '00'), ':', format-number($sec, '00'))"/> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/atom:entry/atom:title"/></title> + <textarea> + <label><xsl:value-of select="/atom:entry/media:group/media:description"/></label> + </textarea> + + <textarea> + <label>Duration: <xsl:call-template name="pretty-print-seconds"> + <xsl:with-param name="seconds"> + <xsl:value-of select="/atom:entry/media:group/yt:duration/@seconds"/> + </xsl:with-param> + </xsl:call-template> + </label> + </textarea> + + <textarea> + <label>Views: <xsl:value-of select="/atom:entry/yt:statistics/@viewCount"/></label> + </textarea> + + <textarea> + <label>Rating: <xsl:value-of select="/atom:entry/gd:rating/@average"/></label> + </textarea> + + <textarea> + <label>Published: <xsl:value-of select="str:replace(str:replace(/atom:entry/atom:published, '.000', ' '), 'T', ' ')"/></label> + </textarea> + + <xsl:if test="/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.responses']"> + <link> + <label>Video responses</label> + <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.responses']/@href, true())"/></ref> + </link> + </xsl:if> + + <xsl:if test="/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.related']"> + <link> + <label>Related videos</label> + <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:entry/atom:link[@rel='http://gdata.youtube.com/schemas/2007#video.related']/@href, true())"/></ref> + </link> + </xsl:if> + + <link> + <label>Download this video</label> + <stream>wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v=<xsl:value-of select="/atom:entry/media:group/yt:videoid"/></stream> + </link> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/youtube/navigation.xsl b/templates/youtube/navigation.xsl new file mode 100644 index 0000000..a5fd1c7 --- /dev/null +++ b/templates/youtube/navigation.xsl @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + xmlns:media="http://search.yahoo.com/mrss/" + xmlns:yt="http://gdata.youtube.com/schemas/2007" + exclude-result-prefixes="atom str media yt"> + +<xsl:template match="atom:entry"> + <link> + <label><xsl:value-of select="atom:title"/></label> + <stream>wvt:///youtube/video.xsl?srcurl=http://www.youtube.com/watch?v=<xsl:value-of select="media:group/yt:videoid"/></stream> + <ref>wvt:///youtube/description.xsl?srcurl=<xsl:value-of select="str:encode-uri(atom:link[@rel='self']/@href, true())"/></ref> + </link> +</xsl:template> + +<xsl:template match="atom:link"> + <xsl:if test="@rel = 'previous'"> + <link> + <label>Previous</label> + <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref> + </link> + </xsl:if> + + <xsl:if test="@rel = 'next'"> + <link> + <label>Next</label> + <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(@href, true())"/></ref>a + </link> + </xsl:if> +</xsl:template> + +<xsl:template match="/"> +<wvmenu> + <title><xsl:value-of select="/atom:feed/atom:title"/></title> + + <xsl:if test="/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']"> + <link> + <label>Did you mean <xsl:value-of select="/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']/@title"/>?</label> + <ref>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri(/atom:feed/atom:link[@rel='http://schemas.google.com/g/2006#spellcorrection']/@href, true())"/></ref> + </link> + </xsl:if> + + + <!-- Video links --> + <xsl:apply-templates select="/atom:feed/atom:entry"/> + + <xsl:if test="count(/atom:feed/atom:entry) = 0"> + <textarea> + <label>No match</label> + </textarea> + </xsl:if> + + <!-- Next and prev links --> + <xsl:apply-templates select="/atom:feed/atom:link[@rel='previous']|/atom:feed/atom:link[@rel='next']"/> + +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/youtube/search.xsl b/templates/youtube/search.xsl new file mode 100644 index 0000000..d28e3c6 --- /dev/null +++ b/templates/youtube/search.xsl @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:str="http://exslt.org/strings" + exclude-result-prefixes="str"> + +<xsl:template match="/"> +<wvmenu> + <title>Youtube Search</title> + + <textfield name="q"> + <label>Search terms</label> + </textfield> + + <itemlist name="orderby"> + <label>Sort by</label> + <item value="relevance">Relevance</item> + <item value="published">Date Added</item> + <item value="viewCount">View Count</item> + <item value="rating">Rating</item> + </itemlist> + + <itemlist name="time"> + <label>Uploaded</label> + <item value="all_time">Anytime</item> + <item value="today">Today</item> + <item value="this_week">This week</item> + <item value="this_month">This month</item> + </itemlist> + + <button> + <label>Search</label> + <submission>wvt:///youtube/navigation.xsl?srcurl=<xsl:value-of select="str:encode-uri('http://gdata.youtube.com/feeds/api/videos?q={q}&orderby={orderby}&time={time}&max-results=20&safeSearch=none&format=5&v=2', true())"/></submission> + </button> +</wvmenu> +</xsl:template> + +</xsl:stylesheet> diff --git a/templates/youtube/service.xml b/templates/youtube/service.xml new file mode 100644 index 0000000..f5719a6 --- /dev/null +++ b/templates/youtube/service.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<service> + <title>YouTube</title> + <ref>wvt:///youtube/categories.xsl?srcurl=http://gdata.youtube.com/schemas/2007/categories.cat</ref> + <description>Video sharing service on which users worldwide can upload their videos</description> +</service> diff --git a/templates/youtube/video.xsl b/templates/youtube/video.xsl new file mode 100644 index 0000000..d38a99e --- /dev/null +++ b/templates/youtube/video.xsl @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:strip-space elements="div" /> + +<!-- old variables (before appr. April 2010) --> +<xsl:variable name="t1" select="substring-before(substring-after(html/head/script[contains(., 'swfArgs') or contains(., 'SWF_ARGS')], '"t": "'), '"')"/> +<xsl:variable name="video_id1" select="substring-before(substring-after(html/head/script[contains(., 'swfArgs') or contains(., 'SWF_ARGS')], '"video_id": "'), '"')"/> + +<!-- new variables --> +<xsl:variable name="t2" select="substring-before(substring-after(//script[contains(., 'swfHTML')], '&t='), '&')"/> +<xsl:variable name="video_id2" select="substring-before(substring-after(//script[contains(., 'swfHTML')], '&video_id='), '&')"/> + +<xsl:variable name="t"> + <xsl:choose> + <xsl:when test="$t1"><xsl:value-of select="$t1"/></xsl:when> + <xsl:otherwise><xsl:value-of select="$t2"/></xsl:otherwise> + </xsl:choose> +</xsl:variable> +<xsl:variable name="video_id"> + <xsl:choose> + <xsl:when test="$video_id1"><xsl:value-of select="$video_id1"/></xsl:when> + <xsl:otherwise><xsl:value-of select="$video_id2"/></xsl:otherwise> + </xsl:choose> +</xsl:variable> + +<xsl:template match="/"> +<mediaurl> + <title> + <xsl:choose> + <xsl:when test="/html/head/meta[@name='title']/@content"> + <xsl:value-of select="/html/head/meta[@name='title']/@content"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="//div[@id='watch-vid-title']//h1"/> + </xsl:otherwise> + </xsl:choose> + </title> + + <url priority="70">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&t=<xsl:value-of select="$t"/>&fmt=22</url> + <url priority="60">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&t=<xsl:value-of select="$t"/>&fmt=18</url> + <url priority="50">http://www.youtube.com/get_video?video_id=<xsl:value-of select="$video_id"/>&t=<xsl:value-of select="$t"/></url> +</mediaurl> +</xsl:template> + +</xsl:stylesheet> |