diff options
author | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-06 09:07:49 +0300 |
---|---|---|
committer | Antti Ajanki <antti.ajanki@iki.fi> | 2013-08-06 09:07:49 +0300 |
commit | 7c81286a59639e139ac7e947378be24410701a5e (patch) | |
tree | 88e43b758dc2330e8711ebae80eee0039cc57322 | |
download | vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.gz vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.bz2 |
import to vdr-developer repo
62 files changed, 7548 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4213f10 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.6) +PROJECT(libwebvi) + +ENABLE_TESTING() + +# Version number +SET(MAJOR_VERSION "0") +SET(MINOR_VERSION "99") +SET(PATCH_VERSION "0") + +CONFIGURE_FILE( + "${CMAKE_SOURCE_DIR}/src/version.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/version.h" + @ONLY) +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}) + +ADD_SUBDIRECTORY(src/libwebvi) +ADD_SUBDIRECTORY(tests) + @@ -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,281 @@ +VDR Plugin 'webvideo' Revision History +-------------------------------------- + +2013-04-14: Version 0.5.0 + +- Compatible with VDR 2.0.0. VDR 1.6.x support dropped. +- Fixed ruutu.fi, katsomo.fi (low-quality mobile streams) + +2012-09-30: Version 0.4.9 + +- Fixed support for Youtube + +2012-09-24: Version 0.4.8 + +- Fixed support for Youtube, Google Video, ruutu.fi, MoonTV + +2012-06-02: Version 0.4.7 + +- Support new Yle Areena +- Removed broken Vimeo support +- Fixed MoonTV and Metacafe + +2012-02-26: Version 0.4.6 + +- webvi: New option --url for downloading a single video + +2012-01-29: Version 0.4.5 + +- Fixed Youtube, Google Video breakage +- Removed SubTV support. SubTV programs are available in Katsomo. + +2011-08-08: Version 0.4.4 + +- Fixed Youtube support +- Minor improvements in Katsomo and ruutu.fi support +- Disabled broken Vimeo search + +2011-07-12: Version 0.4.3 + +- Streaming works on YLE Areena and on other sources which use + external downloaders (playback in VLC does not work very well) +- Prefer mplayer over VLC when playing streams +- Fixed "Error 501: No active sockets" by properly implementing + timeouts +- Improved ruutu.fi support + +2011-06-12: Version 0.4.2 + +- Fixed katsomo.fi module. +- Watch on vdr bookmarklet now works also on katsomo.fi. +- webvi: Space in search terms was errornously replaced by plus. + +2011-04-17: Version 0.4.1 + +- Fixed Youtube, Metacafe and Vimeo modules. Streaming does not work + on Vimeo. +- Removed SVT Play module which was not working anymore. +- Accept -p as alternative to --postprocess (thanks to Matti + Lehtimäki). +- New option --prefermplayer prefers mplayer over xineliboutput when + streaming (thanks to Matti Lehtimäki). +- Bookmarklet for saving a video from the web browser (a patch by + Samuli Sorvakko). +- Updated Italian translation (thanks to Diego Pierotto) +- New option --verbose (webvi) +- New option --vfat (or vfat in config file) generates Windows + compatible filenames (plugin, webvi) + +2010-11-18: Version 0.4.0 + +- SVDRP commands for playing and downloading videos (based on a patch + by Matti Lehtimäki). +- Bookmarklet for sending a video from web browser to VDR. +- Correct template path in webvi.plugin.conf, respect user CXXFLAGS, + SYSLIBDIR and DESTDIR in Makefiles, fix typos (patches by Ville + Skyttä). +- Support VDR 1.7 series by including Make.global. +- Command line arguments override config file options. +- Fixes for Youtube, Metacafe and Google modules. + +2010-08-26: Version 0.3.2 + +- Plugin: Possibility to run a script after downloading. +- New video service: MoonTV (contributed by Matti Lehtimäki). +- ruutu.fi uses rtmpe for some videos. +- Stream low bandwidth Youtube videos by default (configurable in + /etc/webvi.conf) +- Fixed Google video module. +- Disabled ruutu.fi search which is not working. + +2010-07-25: Version 0.3.1 + +- Updated Italian translation (thanks to Diego Pierotto). +- Fixed Youtube extractor. +- Fixed Vimeo search. +- Timers are no longer marked as "Unfinished" after the download has + been completed. +- Retry failed timers again later. + +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. + +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-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-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. + +2009-10-27: Version 0.1.7 + +- Compatibility fixes for Youtube and Metacafe modules. + +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-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-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-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-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-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-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 + +2008-12-06: Version 0.0.6 + +- French translation (Thanks to Bruno Roussel) +- Fixed Youtube parsing to accommodate to recent changes + +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-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-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-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-06-25: Version 0.0.1 + +- Initial revision. @@ -0,0 +1,41 @@ +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 websites 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 [2] +* ruutu.fi [3] +* YLE Areena [4] +* YouTube + +[1] Only videos hosted by Google, YouTube, Metacafe + +[2] Experimental, requires curl and avconv + +[3] Requires rtmpdump: http://rtmpdump.mplayerhq.hu/ + +[4] Requires yle-dl: http://aajanki.github.com/yle-dl/ + +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..0887c93 --- /dev/null +++ b/README.vdrplugin @@ -0,0 +1,233 @@ +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 websites 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/plugins) +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 +-p CMD, --postprocess=CMD Execute CMD after downloading +-m, --prefermplayer Prefer mplayer over xineliboutput when streaming +--vfat Generate Windows compatible filenames + +Config file +----------- + +Config file VDRPLUGINCONFDIR/webvi.plugin.conf (the default path can +be overridden with the --conf argument) can be used to configure the +plugin. + +The global options that can be set in section [webvi]: + +vfat + +Generate Windows compatible filenames. Allowed values: true, false. + +Quality of the downloaded and streamed videos can be selected in video +site specific sections. Currently only YouTube module supports +multiple qualities. The following options are recognized in section +[www.youtube.com]: + + +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: 480x360 MP4 + 65: 480p WebM + 70: 720p MP4 + 75: 720p WebM + 80: 1080p MP4 + +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: + +[www.youtube.com] +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 Info shows more +information about the stream. Pressing Blue starts to play the stream +using xineliboutput or mplayer plugin if one of them is installed. +Xineliboutput works straight out of the box. For mplayer, add +following to mplayersouces.conf: /tmp;Webvideo;0;*.m3u + +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 a stream without saving +Info Show details about a stream +0 Switch between history and status modes + +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. + +Executing a script after downloading +------------------------------------ + +The option -p sets a script that is called for each downloaded file. +For example: vdr -P "webvideo -p /path/to/script.sh" + +The script will be called with a single argument: the name (including +the path) of the downloaded file. + +An example script for transcoding the downloaded files to Ogg format +is included in the source distribution in examples/transcode2ogg.sh. + +"Watch on VDR" bookmarklet +-------------------------- + +It is possible to send the video you are watching on your web browser +to VDR and view or save it on your TV. + +1. Create a new bookmark on your web browser and paste the content of +examples/watchonvdr_bookmarklet.js or savevideo_bookmarklet.js as the +address of the new bookmark. If your VDR is running on a different +machine than your browser, put VDR's address into vdrserver variable +near the beginning of the bookmarklet. + +2. Set up the firewall of the VDR machine to allow incoming +connections on port 43280 only from the machine where you are running +your web browser. WARNING: Unrestricted access to port 43280 means +that anyone in the Internet can instruct your VDR to play videos. + +3. Run examples/bookmarklet_proxy.py on VDR machine. If the SVDRP port +differs from 2001 (the default changed to 6419 in VDR 1.7.15) use -s +option. + +4. When viewing a video on one of the supported sites click "watch on +VDR" bookmarklet. The video starts playing on VDR. diff --git a/README.webvi b/README.webvi new file mode 100644 index 0000000..326f7f4 --- /dev/null +++ b/README.webvi @@ -0,0 +1,138 @@ +webvi - command line web video downloader + +Copyright 2009-2012 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 websites 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 +--vfat generate Windows compatible filenames +-u URL, --url=URL Download video from URL and exit +-v, --verbose debug output + +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. x is menu index or video page URL. +stream x Play a media stream. x is menu index or video page URL. +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 menu item or an address of a video page (download and +stream commands). A plain number without a command follows a link +(like "select") or downloads a stream (like "download") if the item is +not a navigation link. + +The command line option --url runs a single download mode that +downloads a video from the the given URL and exits. URL must point to +a video page on one of the supported sites (see README). + +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. + +vfat + +Generate Windows compatible filenames. Allowed values: true, false. + +verbose + +Write debug output to stdin. Allowed values: true, false. + +Quality of the downloaded and streamed videos can be selected in video +site specific sections. Currently only YouTube module (section should +be called [www.youtube.com]) 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: 480x360 MP4 + 65: 480p WebM + 70: 720p MP4 + 75: 720p WebM + 80: 1080p MP4 diff --git a/cmake/modules/FindLibTidy.cmake b/cmake/modules/FindLibTidy.cmake new file mode 100644 index 0000000..071bed7 --- /dev/null +++ b/cmake/modules/FindLibTidy.cmake @@ -0,0 +1,73 @@ +# Try to find the HTML Tidy lib +# Once done this will define: +# +# LIBTIDY_FOUND - system has LIBTIDY +# LIBTIDY_INCLUDE_DIR - the LIBTIDY include directory +# LIBTIDY_LIBRARIES - The libraries needed to use LIBTIDY +# LIBTIDY_ULONG_VERSION_FOUND - To deal with source incompatible versions +# +# Copyright (c) 2007, Paulo Moura Guedes, <moura@kdewebdev.org> +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +INCLUDE(CheckCXXSourceCompiles) +INCLUDE(MacroBoolTo01) + +if (LIBTIDY_INCLUDE_DIR) + # Already in cache, be silent + set(LibTidy_FIND_QUIETLY TRUE) +endif (LIBTIDY_INCLUDE_DIR) + +FIND_PATH(LIBTIDY_INCLUDE_DIR tidy.h) + +if( NOT LIBTIDY_INCLUDE_DIR ) + find_path(LIBTIDY_INCLUDE_DIR tidy.h PATH_SUFFIXES tidy) + #now tidy.h was inside a tidy subdirectory so we need to + #add that to the include dir + set(LIBTIDY_INCLUDE_DIR ${LIBTIDY_INCLUDE_DIR}/tidy CACHE PATH "Libtidy include directory") +endif( NOT LIBTIDY_INCLUDE_DIR ) + + + +FIND_LIBRARY(LIBTIDY_LIBRARIES NAMES tidy) + +if (LIBTIDY_INCLUDE_DIR AND LIBTIDY_LIBRARIES) + set(LIBTIDY_FOUND TRUE) +endif (LIBTIDY_INCLUDE_DIR AND LIBTIDY_LIBRARIES) + + +if (LIBTIDY_FOUND) + if (NOT LibTidy_FIND_QUIETLY) + message(STATUS "Found Tidy: ${LIBTIDY_LIBRARIES}") + endif (NOT LibTidy_FIND_QUIETLY) + + SET(CHECK_TIDY_ULONG_SOURCE_CODE " +#include <${LIBTIDY_INCLUDE_DIR}/tidy.h> + +int main() +{ + ulong l; + TidyInputSource s; + s.sourceData = l; +} +") + + CHECK_CXX_SOURCE_COMPILES("${CHECK_TIDY_ULONG_SOURCE_CODE}" TIDY_ULONG_VERSION) + if(TIDY_ULONG_VERSION) + SET(LIBTIDY_ULONG_VERSION_FOUND TRUE) + else(TIDY_ULONG_VERSION) + SET(LIBTIDY_ULONG_VERSION_FOUND FALSE) + endif(TIDY_ULONG_VERSION) + + macro_bool_to_01(TIDY_ULONG_VERSION HAVE_TIDY_ULONG_VERSION) + +else (LIBTIDY_FOUND) + if (LibTidy_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find LIBTIDY") + else (LibTidy_FIND_REQUIRED) + message(STATUS "Could NOT find LIBTIDY") + endif (LibTidy_FIND_REQUIRED) +endif (LIBTIDY_FOUND) + +MARK_AS_ADVANCED(LIBTIDY_INCLUDE_DIR LIBTIDY_LIBRARIES) diff --git a/cmake/modules/MacroBoolTo01.cmake b/cmake/modules/MacroBoolTo01.cmake new file mode 100644 index 0000000..63b9852 --- /dev/null +++ b/cmake/modules/MacroBoolTo01.cmake @@ -0,0 +1,20 @@ +# MACRO_BOOL_TO_01( VAR RESULT0 ... RESULTN ) +# This macro evaluates its first argument +# and sets all the given vaiables either to 0 or 1 +# depending on the value of the first one + +# Copyright (c) 2006, Alexander Neundorf, <neundorf@kde.org> +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +MACRO(MACRO_BOOL_TO_01 FOUND_VAR ) + FOREACH (_current_VAR ${ARGN}) + IF(${FOUND_VAR}) + SET(${_current_VAR} 1) + ELSE(${FOUND_VAR}) + SET(${_current_VAR} 0) + ENDIF(${FOUND_VAR}) + ENDFOREACH(_current_VAR) +ENDMACRO(MACRO_BOOL_TO_01) diff --git a/examples/savevideo_bookmarklet.js b/examples/savevideo_bookmarklet.js new file mode 100644 index 0000000..ac16542 --- /dev/null +++ b/examples/savevideo_bookmarklet.js @@ -0,0 +1 @@ +javascript:(function(){ var vdrserver='127.0.0.1:43280'; var xmlhttp=new XMLHttpRequest(); xmlhttp.open("GET", "http://" + vdrserver + "/download?url=" + encodeURIComponent(location.href), true); xmlhttp.send(null); })();
\ No newline at end of file diff --git a/examples/transcode2ogg.sh b/examples/transcode2ogg.sh new file mode 100755 index 0000000..7e9fb27 --- /dev/null +++ b/examples/transcode2ogg.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# An example post-processing script for VDR plugin webvideo. +# +# Copyright: Antti Ajanki <antti.ajanki@iki.fi> +# License: GPL3 or later, see the file COPYING for the full license +# +# This script transcodes a video file using Ogg Theora and Vorbis +# codecs. The first parameter is the name of the video file. +# +# To setup this script to be called for every downloaded file, start +# the webvideo plugin with option -p. For example: +# +# vdr -P "webvideo -p /path/to/this/file/transcode2ogg.sh" + +fullsrcname=$1 +videodir=`dirname "$fullsrcname"` +srcfile=`basename "$fullsrcname"` +srcbasename=`echo "$srcfile" | sed 's/\.[^.]*$//'` +destname="$videodir/$srcbasename.ogg" + +nice -n 19 ffmpeg -i "$fullsrcname" -qscale 7 -vcodec libtheora -acodec libvorbis -ac 2 -y "$destname" + +if [ $? -eq 0 ]; then + rm -f "$fullsrcname" + exit 0 +else + exit 1 +fi diff --git a/examples/watchonvdr.sh b/examples/watchonvdr.sh new file mode 100755 index 0000000..5fe5e65 --- /dev/null +++ b/examples/watchonvdr.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# Plays a video on VDR. Give the video page address as parameter. +# +# Pre-requisites: svdrpsend.pl (from VDR) +# +# Example usage: +# watchonvdr.sh http://www.youtube.com/watch?v=n8qHOnlbvRY + +# Put your SVDRP host and port here +VDR_HOST=127.0.0.1 +VDR_PORT=2001 + +if [ "x$1" = "x" ]; then + echo "video page URL expected" + exit 1 +fi + +svdrpsend.pl -d $VDR_HOST -p $VDR_PORT "plug webvideo play $1" diff --git a/examples/watchonvdr_bookmarklet.js b/examples/watchonvdr_bookmarklet.js new file mode 100644 index 0000000..dc4961d --- /dev/null +++ b/examples/watchonvdr_bookmarklet.js @@ -0,0 +1 @@ +javascript:(function(){ var vdrserver='127.0.0.1:43280'; var xmlhttp=new XMLHttpRequest(); xmlhttp.open("GET", "http://" + vdrserver + "/play?url=" + encodeURIComponent(location.href), true); xmlhttp.send(null); })();
\ No newline at end of file diff --git a/examples/watchonvdr_proxy.py b/examples/watchonvdr_proxy.py new file mode 100644 index 0000000..be9afc2 --- /dev/null +++ b/examples/watchonvdr_proxy.py @@ -0,0 +1,99 @@ +#!/usr/bin/python + +# Proxy for relaying commands to play a video on VDR from a web +# browser to VDR. +# +# Listens for HTTP GET /play?url=XXX requests, where XXX is the address +# of the video page (not the address of the video stream), and converts +# them to webvideo plugin SVDRP commands. The bookmarklet in +# webvi_bookmarklet.js generates such requests. See README for the +# list of supported video sites. +# +# Antti Ajanki <antti.ajanki@iki.fi> + +import urllib +import socket +import os.path +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +from optparse import OptionParser +from urlparse import urlparse + +SVDRP_ADDRESS = ('', 2001) # default port is 6419 starting from VDR 1.7.15 + +class SVDRPRequestHandler(BaseHTTPRequestHandler): + def send(self, cmd): + self.sock.sendall(cmd) + self.sock.sendall('\r\n') + + def is_video_file(self, url): + ext = os.path.splitext(urlparse(url).path)[1] + return ext not in ('', '.htm', '.html') + + def do_GET(self): + if self.path.startswith('/play?url='): + videopage = urllib.unquote(self.path[len('/play?url='):]) + operation = "play" + + elif self.path.startswith('/download?url='): + videopage = urllib.unquote(self.path[len('/download?url='):]) + operation = "dwld" + + else: + self.send_response(404, 'Not found') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + return + + # Strip everything after the first linefeed to prevent + # SVDRP command injection. + videopage = videopage.split('\r', 1)[0].split('\n', 1)[0] + + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(10) + self.sock.connect(SVDRP_ADDRESS) + + # If this is a video file ask xineliboutput to play + # it. Otherwise assume it is a video page from one of + # the supported sites and let webvideo extract the + # video address from the page. + if self.is_video_file(videopage) and operation == "play": + self.send('plug xineliboutput pmda %s' % videopage) + else: + self.send('plug webvideo %s %s' % (operation, videopage) ) + self.send('quit') + while len(self.sock.recv(4096)) > 0: + pass + self.sock.close() + + self.send_response(204, 'OK') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + except socket.error, exc: + self.send_response(503, 'SVDRP connection error: %s' % exc) + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + except socket.timeout: + self.send_response(504, 'SVDRP timeout') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + + +def main(): + parser = OptionParser() + parser.add_option('-s', '--svdrpport', dest='svdrpport', + type='int', default=2001, help='set SVDRP port') + parser.add_option('-d', '--svdrpaddress', dest='svdrpaddress', + default='', help='set SVDRP address') + parser.add_option('-l', '--listen', dest='listenport', + type='int', default=43280, help='listen to connection on this port') + (options, args) = parser.parse_args() + + global SVDRP_ADDRESS + SVDRP_ADDRESS = (options.svdrpaddress, options.svdrpport) + + httpd = HTTPServer(('', options.listenport), SVDRPRequestHandler) + httpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/examples/webvi.conf b/examples/webvi.conf new file mode 100644 index 0000000..989aad2 --- /dev/null +++ b/examples/webvi.conf @@ -0,0 +1,28 @@ +[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/local/share/webvi/templates + +# If vfat is true, generate Windows compatible file names. +# +#vfat=false + +[www.youtube.com] + +# Limit the quality when streaming to make the playback smooth even if +# the network connection is slow. +stream-max-quality = 50 + +[youtu.be] + +stream-max-quality = 50 diff --git a/examples/webvi.plugin.conf b/examples/webvi.plugin.conf new file mode 100644 index 0000000..f8d0a96 --- /dev/null +++ b/examples/webvi.plugin.conf @@ -0,0 +1,11 @@ +[webvi] + +templatepath = /usr/local/share/webvi/templates + +[www.youtube.com] + +stream-max-quality = 50 + +[youtu.be] + +stream-max-quality = 50 diff --git a/src/libwebvi/CMakeLists.txt b/src/libwebvi/CMakeLists.txt new file mode 100644 index 0000000..6e4e155 --- /dev/null +++ b/src/libwebvi/CMakeLists.txt @@ -0,0 +1,50 @@ +SET(LIBWEBVI_SOURCES + libwebvi.c + mainmenu.c + webvicontext.c + request.c + link.c + linktemplates.c + linkextractor.c + menubuilder.c + pipecomponent.c + urlutils.c) + +SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") + +INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/src/libwebvi) + +ADD_LIBRARY(webvi SHARED ${LIBWEBVI_SOURCES}) +ADD_LIBRARY(webvistatic STATIC ${LIBWEBVI_SOURCES}) + +SET_TARGET_PROPERTIES(webvi PROPERTIES VERSION "${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}") +SET_TARGET_PROPERTIES(webvi PROPERTIES SOVERSION "${MAJOR_VERSION}.${MINOR_VERSION}") +SET_TARGET_PROPERTIES(webvi PROPERTIES COMPILE_FLAGS "-fvisibility=hidden") +SET_TARGET_PROPERTIES(webvistatic PROPERTIES OUTPUT_NAME webvi) + +ADD_DEFINITIONS(-DLIBWEBVI_LOG_DOMAIN="libwebvi") + +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall") + +# Required libraries +FIND_PACKAGE(LibXml2 REQUIRED) +FIND_PACKAGE(CURL REQUIRED) +FIND_PACKAGE(LibTidy REQUIRED) + +FIND_PACKAGE(PkgConfig) +PKG_CHECK_MODULES(GLIB REQUIRED glib-2.0) +ADD_DEFINITIONS(${GLIB_CFLAGS} ${GLIB_CFLAGS_OTHER}) +LINK_DIRECTORIES(${GLIB_LIBRARY_DIRS}) + +INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR}) +INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${GLIB_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${LIBTIDY_INCLUDE_DIRS}) + +ADD_DEFINITIONS(-DHAVE_TIDY_ULONG_VERSION=${HAVE_TIDY_ULONG_VERSION}) + +TARGET_LINK_LIBRARIES(webvi ${GLIB_LIBRARIES} ${LIBXML2_LIBRARIES} ${CURL_LIBRARIES} ${LIBTIDY_LIBRARIES}) + +# Installing +INSTALL(TARGETS webvi DESTINATION bin) +INSTALL(FILES libwebvi.h DESTINATION include) diff --git a/src/libwebvi/libwebvi.c b/src/libwebvi/libwebvi.c new file mode 100644 index 0000000..b3d030a --- /dev/null +++ b/src/libwebvi/libwebvi.c @@ -0,0 +1,338 @@ +/* + * libwebvi.c + * + * Copyright (c) 2010-2013 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 <stdarg.h> +#include <string.h> +#include <libxml/xmlversion.h> +#include "libwebvi.h" +#include "webvicontext.h" +#include "request.h" +#include "version.h" + +static const char *VERSION = "libwebvi/" LIBWEBVI_VERSION; + +struct WebviErrorMessage { + WebviResult code; + const char *message; +}; + +int webvi_global_init() { + LIBXML_TEST_VERSION + return 0; +} + +void webvi_cleanup() { + webvi_context_cleanup_all(); +} + +WebviCtx webvi_initialize_context(void) { + return webvi_context_initialize(); +} + +void webvi_cleanup_context(WebviCtx ctxhandle) { + webvi_context_cleanup(ctxhandle); +} + +const char* webvi_version(void) { + return VERSION; +} + +const char* webvi_strerror(WebviResult err) { + static struct WebviErrorMessage error_messages[] = + {{WEBVIERR_OK, "Succeeded"}, + {WEBVIERR_INVALID_HANDLE, "Invalid handle"}, + {WEBVIERR_INVALID_PARAMETER, "Invalid parameter"}, + {WEBVIERR_UNKNOWN_ERROR, "Internal error"}}; + + for (int i=0; i<(sizeof(error_messages)/sizeof(error_messages[0])); i++) { + if (err == error_messages[i].code) { + return error_messages[i].message; + } + } + + return "Internal error"; +} + +WebviResult webvi_set_config(WebviCtx ctxhandle, WebviConfig conf, ...) { + va_list argptr; + const char *p; + WebviResult res = WEBVIERR_OK; + + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + va_start(argptr, conf); + + switch (conf) { + case WEBVI_CONFIG_TEMPLATE_PATH: + p = va_arg(argptr, char *); + webvi_context_set_template_path(ctx, p); + break; + case WEBVI_CONFIG_DEBUG: + p = va_arg(argptr, char *); + webvi_context_set_debug(ctx, strcmp(p, "0") != 0); + break; + case WEBVI_CONFIG_TIMEOUT_CALLBACK: + // FIXME + // va_arg(argptr, long) + break; + case WEBVI_CONFIG_TIMEOUT_DATA: + // FIXME + // va_arg(argptr, long) + break; + default: + res = WEBVIERR_INVALID_PARAMETER; + }; + + va_end(argptr); + + return res; +} + +WebviHandle webvi_new_request(WebviCtx ctxhandle, const char *href) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVI_INVALID_HANDLE; + + WebviRequest *req = request_create(href, ctx); + return webvi_context_add_request(ctx, req); +} + +WebviResult webvi_start_request(WebviCtx ctxhandle, WebviHandle h) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + WebviRequest *req = webvi_context_get_request(ctx, h); + if (!req) + return WEBVIERR_INVALID_HANDLE; + + request_start(req); + + return WEBVIERR_OK; +} + +WebviResult webvi_stop_request(WebviCtx ctxhandle, WebviHandle h) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + WebviRequest *req = webvi_context_get_request(ctx, h); + if (!req) + return WEBVIERR_INVALID_HANDLE; + + request_stop(req); + + return WEBVIERR_OK; +} + +WebviResult webvi_delete_request(WebviCtx ctxhandle, WebviHandle h) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + WebviResult res = webvi_stop_request(ctxhandle, h); + if (res != WEBVIERR_OK) + return res; + + webvi_context_remove_request(ctx, h); + + return WEBVIERR_OK; +} + +WebviResult webvi_set_opt(WebviCtx ctxhandle, WebviHandle h, WebviOption opt, ...) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + WebviRequest *req = webvi_context_get_request(ctx, h); + if (!req) + return WEBVIERR_INVALID_HANDLE; + + va_list argptr; + WebviResult res = WEBVIERR_OK; + + va_start(argptr, opt); + + switch (opt) { + case WEBVIOPT_WRITEFUNC: + request_set_write_callback(req, va_arg(argptr, webvi_callback)); + break; + case WEBVIOPT_READFUNC: + request_set_read_callback(req, va_arg(argptr, webvi_callback)); + break; + case WEBVIOPT_WRITEDATA: + request_set_write_data(req, va_arg(argptr, void *)); + break; + case WEBVIOPT_READDATA: + request_set_read_data(req, va_arg(argptr, void *)); + break; + default: + res = WEBVIERR_INVALID_PARAMETER; + }; + + va_end(argptr); + + return res; +} + +WebviResult webvi_get_info(WebviCtx ctxhandle, WebviHandle h, WebviInfo info, ...) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + WebviRequest *req = webvi_context_get_request(ctx, h); + if (!req) + return WEBVIERR_INVALID_HANDLE; + + va_list argptr; + WebviResult res = WEBVIERR_OK; + + va_start(argptr, info); + + switch (info) { + case WEBVIINFO_URL: + { + char **output = va_arg(argptr, char **); + if (output) { + *output = NULL; + + const char *url = request_get_url(req); + if (url) { + *output = malloc(strlen(url)+1); + if (*output) { + strcpy(*output, url); + } + } + } + break; + } + + case WEBVIINFO_CONTENT_LENGTH: + { + // FIXME + long *content_length = va_arg(argptr, long *); + if (content_length) + *content_length = -1; + break; + } + + case WEBVIINFO_CONTENT_TYPE: + { + // FIXME + char **output = va_arg(argptr, char **); + if (output) { + *output = malloc(1); + **output = '\0'; + } + break; + } + + case WEBVIINFO_STREAM_TITLE: + { + // FIXME + char **output = va_arg(argptr, char **); + if (output) { + *output = malloc(1); + **output = '\0'; + } + break; + } + + default: + res = WEBVIERR_INVALID_PARAMETER; + }; + + va_end(argptr); + + return res; +} + +WebviResult webvi_fdset(WebviCtx ctxhandle, fd_set *readfd, fd_set *writefd, + fd_set *excfd, int *max_fd) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + return webvi_context_fdset(ctx, readfd, writefd, excfd, max_fd); +} + +WebviResult webvi_perform(WebviCtx ctxhandle, int sockfd, int ev_bitmask, + long *running_handles) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return WEBVIERR_INVALID_HANDLE; + + webvi_context_handle_socket_action(ctx, sockfd, ev_bitmask, running_handles); + + return WEBVIERR_OK; +} + +WebviMsg *webvi_get_message(WebviCtx ctxhandle, int *remaining_messages) { + WebviContext *ctx = get_context_by_handle(ctxhandle); + if (!ctx) + return NULL; + + return webvi_context_next_message(ctx, remaining_messages); +} + +int webvi_process_some(WebviCtx ctx, int timeout_seconds) { + fd_set readfds; + fd_set writefds; + fd_set excfds; + int maxfd; + int s; + WebviResult res; + struct timeval timeout; + long running_handles; + + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&excfds); + res = webvi_fdset(ctx, &readfds, &writefds, &excfds, &maxfd); + if (res != WEBVIERR_OK) { + return -1; + } + + timeout.tv_sec = timeout_seconds; + timeout.tv_usec = 0; + s = select(maxfd+1, &readfds, &writefds, NULL, &timeout); + + if (s == -1) { + // error + return -1; + } else if (s == 0) { + // timeout + webvi_perform(ctx, WEBVI_SELECT_TIMEOUT, WEBVI_SELECT_CHECK, &running_handles); + } else { + // handle one fd + for (int fd=0; fd<=maxfd; fd++) { + if (FD_ISSET(fd, &readfds)) { + webvi_perform(ctx, fd, WEBVI_SELECT_READ, &running_handles); + } else if (FD_ISSET(fd, &writefds)) { + webvi_perform(ctx, fd, WEBVI_SELECT_WRITE, &running_handles); + } else if (FD_ISSET(fd, &excfds)) { + webvi_perform(ctx, fd, WEBVI_SELECT_EXCEPTION, &running_handles); + } + } + } + + return running_handles; +} diff --git a/src/libwebvi/libwebvi.h b/src/libwebvi/libwebvi.h new file mode 100644 index 0000000..8efe953 --- /dev/null +++ b/src/libwebvi/libwebvi.h @@ -0,0 +1,370 @@ +/* + * libwebvi.h: C bindings for webvi Python module + * + * Copyright (c) 2010-2013 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> +#include <unistd.h> + +#if webvi_EXPORTS /* defined when building as shared library */ + #if defined _WIN32 || defined __CYGWIN__ + #define LIBWEBVI_DLL_EXPORT __declspec(dllexport) + #else + #if __GNUC__ >= 4 + #define LIBWEBVI_DLL_EXPORT __attribute__((__visibility__("default"))) + #endif + #endif +#endif + +#ifndef LIBWEBVI_DLL_EXPORT +#define LIBWEBVI_DLL_EXPORT +#endif + +typedef int WebviHandle; +typedef long WebviCtx; + +typedef ssize_t (*webvi_callback)(const char *, size_t, void *); +typedef void (*webvi_timeout_callback)(long, void *); + +#define WEBVI_INVALID_HANDLE -1 + +typedef enum { + WEBVIMSG_DONE +} WebviMsgType; + +typedef enum { + WEBVISTATE_NOT_FINISHED = 0, + WEBVISTATE_FINISHED_OK = 1, + WEBVISTATE_MEMORY_ALLOCATION_ERROR = 2, + WEBVISTATE_NOT_FOUND = 3, + WEBVISTATE_NETWORK_READ_ERROR = 4, + WEBVISTATE_IO_ERROR = 5, + WEBVISTATE_TIMEDOUT = 6, + WEBVISTATE_SUBPROCESS_FAILED = 7, + WEBVISTATE_INTERNAL_ERROR = 999, +} RequestState; + +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; + +#define WEBVI_SELECT_TIMEOUT -1 + +typedef enum { + WEBVI_SELECT_CHECK = 0, + WEBVI_SELECT_READ = 1, + WEBVI_SELECT_WRITE = 2, + WEBVI_SELECT_EXCEPTION = 4 +} WebviSelectBitmask; + +typedef enum { + WEBVI_CONFIG_TEMPLATE_PATH, + WEBVI_CONFIG_DEBUG, + WEBVI_CONFIG_TIMEOUT_CALLBACK, + WEBVI_CONFIG_TIMEOUT_DATA +} WebviConfig; + +typedef struct { + WebviMsgType msg; + WebviHandle handle; + RequestState status_code; + const char *data; +} WebviMsg; + +#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. + */ +LIBWEBVI_DLL_EXPORT 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. + */ +LIBWEBVI_DLL_EXPORT void webvi_cleanup(); + +/* + * 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. + */ +LIBWEBVI_DLL_EXPORT WebviCtx webvi_initialize_context(void); + +/* + * Free resources allocated by context ctx. The context can not be + * used anymore after a call to this function. + */ +LIBWEBVI_DLL_EXPORT void webvi_cleanup_context(WebviCtx ctx); + +/* + * Return the version of libwebvi as a string. The returned value + * points to a static buffer, and the caller should modify or not free() it. + */ +LIBWEBVI_DLL_EXPORT const char* webvi_version(void); + +/* + * Return a string describing an error code. The returned value points + * to a read-only buffer, and the caller should not modify or free() it. + */ +LIBWEBVI_DLL_EXPORT const char* webvi_strerror(WebviResult err); + +/* + * Set a new value for a global configuration option conf. + * + * Possible values and their meanings: + * + * WEBVI_CONFIG_TEMPLATE_PATH + * Set the base directory for the XSLT templates (char *) + * + * WEBVI_CONFIG_DEBUG + * If value is not "0", print debug output to stdin (char *) + * + * WEBVI_CONFIG_TIMEOUT_CALLBACK + * Set timeout callback function (webvi_timeout_callback) + * + * WEBVI_CONFIG_TIMEOUT_DATA + * Set user data which will passed as second argument of the timeout + * callback (void *) + * + * The strings (char * arguments) are copied to the library (the user + * can free their original copy). + */ +LIBWEBVI_DLL_EXPORT WebviResult webvi_set_config(WebviCtx ctx, WebviConfig conf, ...); + +/* + * Creates a new download request. + * + * href is a URI of the resource that should be downloaded. Typically, + * the reference has been acquired from a previously downloaded menu. + * A special constant "wvt://mainmenu" can be used to download the + * mainmenu. + * + * The return value is a handle to the newly created request. Value + * WEBVI_INVALID_HANDLE 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(). + */ +LIBWEBVI_DLL_EXPORT WebviHandle webvi_new_request(WebviCtx ctx, const char *href); + +/* + * Starts the transfer on request h. The transfer one or more sockets + * whose file descriptors are returned by webvi_fdset(). The actual + * transfer is done during webvi_perform() calls. + */ +LIBWEBVI_DLL_EXPORT WebviResult webvi_start_request(WebviCtx ctx, WebviHandle h); + +/* + * Requests that the transfer on request 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. + */ +LIBWEBVI_DLL_EXPORT WebviResult webvi_stop_request(WebviCtx ctx, WebviHandle h); + +/* + * Frees resources associated with request 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. + */ +LIBWEBVI_DLL_EXPORT WebviResult webvi_delete_request(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 + * function + * + * 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 function 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 + * function + * + * 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 *. + * + */ +LIBWEBVI_DLL_EXPORT 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. + * + */ +LIBWEBVI_DLL_EXPORT 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. + */ +LIBWEBVI_DLL_EXPORT 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 function. 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_CHECK which means that the state is + * checked internally. On return, running_handles will contain the + * number of still active file descriptors. + * + * If a timeout occurs before any file descriptor becomes ready, this + * function should be called with sockfd set to WEBVI_SELECT_TIMEOUT + * and ev_bitmask set to WEBVI_SELECT_CHECK. + */ +LIBWEBVI_DLL_EXPORT WebviResult webvi_perform(WebviCtx ctx, int sockfd, int ev_bitmask, long *running_handles); + + +LIBWEBVI_DLL_EXPORT int webvi_process_some(WebviCtx ctx, int timeout_seconds); + + +/* + * 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 not free the + * returned WebviMsg. The return value is NULL if there is no messages + * in the queue. + */ +LIBWEBVI_DLL_EXPORT WebviMsg *webvi_get_message(WebviCtx ctx, int *remaining_messages); + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/src/libwebvi/link.c b/src/libwebvi/link.c new file mode 100644 index 0000000..501e70a --- /dev/null +++ b/src/libwebvi/link.c @@ -0,0 +1,86 @@ +#include <stdlib.h> +#include <glib.h> +#include "link.h" + +struct Link { + gchar *href; + gchar *title; + LinkActionType type; +}; + +struct LinkAction { + LinkActionType type; + gchar *command; +}; + +struct Link *link_create(const char *href, const char *title, LinkActionType type) { + struct Link *self = malloc(sizeof(struct Link)); + self->href = g_strdup(href ? href : ""); + self->title = g_strdup(title ? title : ""); + self->type = type; + return self; +} + +const char *link_get_href(const struct Link *self) { + return self->href; +} + +const char *link_get_title(const struct Link *self) { + return self->title; +} + +LinkActionType link_get_type(const struct Link *self) { + return self->type; +} + +void link_delete(struct Link *self) { + g_free(self->href); + g_free(self->title); + free(self); +} + +void g_free_link(gpointer data) { + link_delete((struct Link *)data); +} + +struct LinkAction *link_action_create(LinkActionType type, const char *command) { + struct LinkAction *self = malloc(sizeof(struct LinkAction)); + self->type = type; + self->command = g_strdup(command ? command : ""); + return self; +} + +LinkActionType link_action_get_type(const struct LinkAction *self) { + return self->type; +} + +const char *link_action_get_command(const struct LinkAction *self) { + return self->command; +} + +void link_action_delete(struct LinkAction *self) { + if (self) { + g_free(self->command); + free(self); + } +} + +struct ActionTypeMessage { + LinkActionType type; + const char *message; +}; + +const char *link_action_type_to_string(LinkActionType atype) { + static struct ActionTypeMessage messages[] = + {{LINK_ACTION_PARSE, "regular link"}, + {LINK_ACTION_STREAM_LIBQUVI, "stream"}, + {LINK_ACTION_EXTERNAL_COMMAND, "external command"}}; + + for (int i=0; i<(sizeof(messages)/sizeof(messages[0])); i++) { + if (atype == messages[i].type) { + return messages[i].message; + } + } + + return "???"; +} diff --git a/src/libwebvi/link.h b/src/libwebvi/link.h new file mode 100644 index 0000000..0ddc05c --- /dev/null +++ b/src/libwebvi/link.h @@ -0,0 +1,45 @@ +/* + * link.h + * + * Copyright (c) 2013 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 __LINK_H +#define __LINK_H + +typedef enum { + LINK_ACTION_PARSE, + LINK_ACTION_STREAM_LIBQUVI, + LINK_ACTION_EXTERNAL_COMMAND +} LinkActionType; + +typedef struct Link Link; +typedef struct LinkAction LinkAction; + +struct Link *link_create(const char *href, const char *title, LinkActionType type); +const char *link_get_href(const struct Link *self); +const char *link_get_title(const struct Link *self); +LinkActionType link_get_type(const struct Link *self); +void link_delete(struct Link *self); +void g_free_link(gpointer link); + +struct LinkAction *link_action_create(LinkActionType type, const char *command); +LinkActionType link_action_get_type(const struct LinkAction *self); +const char *link_action_get_command(const struct LinkAction *self); +void link_action_delete(struct LinkAction *self); +const char *link_action_type_to_string(LinkActionType atype); + +#endif // __LINK_H diff --git a/src/libwebvi/linkextractor.c b/src/libwebvi/linkextractor.c new file mode 100644 index 0000000..d683df6 --- /dev/null +++ b/src/libwebvi/linkextractor.c @@ -0,0 +1,138 @@ +#include <string.h> +#ifdef HAVE_TIDY_ULONG_VERSION +#define __USE_MISC +#include <sys/types.h> +#undef __USE_MISC +#endif +#include <tidy/tidy.h> +#include <tidy/buffio.h> +#include "linkextractor.h" +#include "urlutils.h" + +#define MENU_HEADER "<?xml version=\"1.0\"?><wvmenu>" +#define MENU_FOOTER "</wvmenu>" + +struct LinkExtractor { + const LinkTemplates *link_templates; + TidyBuffer html_buffer; + gchar *baseurl; +}; + +static GPtrArray *extract_links(const LinkExtractor *self, TidyDoc tdoc); +static void free_link(gpointer p); +static void get_links_recursively(TidyDoc tdoc, + TidyNode node, + const LinkTemplates *link_templates, + const gchar *baseurl, + GPtrArray *links_found); +static void getTextContent(TidyDoc tdoc, TidyNode node, TidyBuffer* buf); + +LinkExtractor *link_extractor_create(const LinkTemplates *link_templates, const gchar *baseurl) { + LinkExtractor *extractor; + extractor = malloc(sizeof(LinkExtractor)); + memset(extractor, 0, sizeof(LinkExtractor)); + extractor->link_templates = link_templates; + tidyBufInit(&extractor->html_buffer); + extractor->baseurl = baseurl ? g_strdup(baseurl) : g_strdup(""); + return extractor; +} + +void link_extractor_delete(LinkExtractor *self) { + if (self) { + tidyBufFree(&self->html_buffer); + g_free(self->baseurl); + free(self); + } +} + +void link_extractor_append(LinkExtractor *self, const char *buf, size_t len) { + tidyBufAppend(&self->html_buffer, (void *)buf, len); +} + +GPtrArray *link_extractor_get_links(LinkExtractor *self) { + GPtrArray *links = NULL; + TidyDoc tdoc; + int err; + TidyBuffer errbuf; // swallow errors here instead of printing to stderr + + tdoc = tidyCreate(); + tidyOptSetBool(tdoc, TidyForceOutput, yes); + tidyOptSetInt(tdoc, TidyWrapLen, 4096); + tidyBufInit(&errbuf); + tidySetErrorBuffer(tdoc, &errbuf); + + err = tidyParseBuffer(tdoc, &self->html_buffer); + if (err >= 0) { + err = tidyCleanAndRepair(tdoc); + if ( err >= 0 ) { + links = extract_links(self, tdoc); + } + } + + tidyBufFree(&errbuf); + tidyRelease(tdoc); + + return links; +} + +GPtrArray *extract_links(const LinkExtractor *self, TidyDoc tdoc) { + GPtrArray *links = g_ptr_array_new_full(0, free_link); + TidyNode root = tidyGetBody(tdoc); + get_links_recursively(tdoc, root, self->link_templates, self->baseurl, links); + return links; +} + +void get_links_recursively(TidyDoc tdoc, TidyNode node, + const LinkTemplates *link_templates, + const gchar *baseurl, + GPtrArray *links_found) { + TidyNode child; + for (child = tidyGetChild(node); child; child = tidyGetNext(child)) { + if (tidyNodeIsA(child)) { + TidyAttr href_attr = tidyAttrGetById(child, TidyAttr_HREF); + ctmbstr href = tidyAttrValue(href_attr); + if (href && *href != '\0' && href[strlen(href)-1] != '#') { + gchar *absolute_href = relative_url_to_absolute(baseurl, href); + const LinkAction *action = \ + link_templates_get_action(link_templates, absolute_href); + if (action) { + TidyBuffer titlebuf; + tidyBufInit(&titlebuf); + getTextContent(tdoc, child, &titlebuf); + tidyBufPutByte(&titlebuf, '\0'); + gchar *title = g_strdup((const gchar*)titlebuf.bp); + g_strstrip(title); + LinkActionType type = link_action_get_type(action); + Link *link = link_create(absolute_href, title, type); + g_ptr_array_add(links_found, link); + g_free(title); + tidyBufFree(&titlebuf); + } + g_free(absolute_href); + } + } else { + TidyNodeType node_type = tidyNodeGetType(node); + if (node_type == TidyNode_Root || node_type == TidyNode_Start) { + get_links_recursively(tdoc, child, link_templates, baseurl, links_found); + } + } + } +} + +void getTextContent(TidyDoc tdoc, TidyNode node, TidyBuffer* buf) { + if (tidyNodeGetType(node) == TidyNode_Text) { + TidyBuffer content; + tidyBufInit(&content); + tidyNodeGetValue(tdoc, node, &content); + tidyBufAppend(buf, content.bp, content.size); + } else { + TidyNode child; + for (child = tidyGetChild(node); child; child = tidyGetNext(child)) { + getTextContent(tdoc, child, buf); + } + } +} + +void free_link(gpointer p) { + link_delete((Link *)p); +} diff --git a/src/libwebvi/linkextractor.h b/src/libwebvi/linkextractor.h new file mode 100644 index 0000000..770b62f --- /dev/null +++ b/src/libwebvi/linkextractor.h @@ -0,0 +1,34 @@ +/* + * linkextractor.h + * + * Copyright (c) 2013 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 __LINKEXTRACTOR_H +#define __LINKEXTRACTOR_H + +#include <glib.h> +#include "linktemplates.h" + +typedef struct LinkExtractor LinkExtractor; + +LinkExtractor *link_extractor_create(const LinkTemplates *link_templates, + const gchar *baseurl); +void link_extractor_delete(LinkExtractor *link_extractor); +void link_extractor_append(LinkExtractor *link_extractor, const char *buf, size_t len); +GPtrArray *link_extractor_get_links(LinkExtractor *link_extractor); + +#endif // __LINKEXTRACTOR_H diff --git a/src/libwebvi/linktemplates.c b/src/libwebvi/linktemplates.c new file mode 100644 index 0000000..a193df3 --- /dev/null +++ b/src/libwebvi/linktemplates.c @@ -0,0 +1,172 @@ +#include <string.h> +#include <glib.h> +#include <stdlib.h> +#include <stdio.h> +#include "linktemplates.h" +#include "link.h" + +#define STREAM_LIBQUVI_SELECTOR "stream:libquvi" +#define STREAM_LIBQUVI_SELECTOR_LEN strlen(STREAM_LIBQUVI_SELECTOR) +#define STREAM_SELECTOR "stream:" +#define STREAM_SELECTOR_LEN strlen(STREAM_SELECTOR) +#define EXT_CMD_SELECTOR "bin:" +#define EXT_CMD_SELECTOR_LEN strlen(EXT_CMD_SELECTOR) + +struct LinkTemplates { + GPtrArray *matcher; +}; + +struct LinkMatcher { + GRegex *pattern; + LinkAction *action; +}; + +static void free_link_matcher(gpointer link); +static struct LinkMatcher *parse_line(const char *line); +static LinkAction *parse_action(const gchar *actionstr); + +LinkTemplates *link_templates_create() { + LinkTemplates *config = malloc(sizeof(LinkTemplates)); + if (!config) + return NULL; + memset(config, 0, sizeof(LinkTemplates)); + config->matcher = g_ptr_array_new_with_free_func(free_link_matcher); + return config; +} + +void link_templates_delete(LinkTemplates *conf) { + g_ptr_array_free(conf->matcher, TRUE); + free(conf); +} + +void link_templates_load(LinkTemplates *conf, const char *filename) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + "Loading matchers from %s", filename); + FILE *file = fopen(filename, "r"); + if (!file) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Failed to read file %s", filename); + return; + } + + char line[1024]; + while (fgets(line, sizeof line, file)) { + struct LinkMatcher *link = parse_line(line); + if (link) { + g_ptr_array_add(conf->matcher, link); + } + } + + fclose(file); +} + +struct LinkMatcher *parse_line(const char *line) { + if (!line) + return NULL; + + const char *p = line; + while (*p == ' ') + p++; + + if (*p == '\0' || *p == '#' || *p == '\n' || *p == '\r') + return NULL; + + const char *end = line + strlen(line); + while ((end-1 >= p) && + (end[-1] == ' ' || end[-1] == '\r' || end[-1] == '\n' || end[-1] == '\t')) + end--; + + if (end <= p) + return NULL; + + const char *tab = memchr(p, '\t', end-p); + gchar *pattern; + LinkAction *action; + if (tab && tab < end) { + pattern = g_strndup(p, tab-p); + const char *cmdstart = tab+1; + gchar *action_field = g_strndup(cmdstart, end-cmdstart); + action = parse_action(action_field); + g_free(action_field); + } else { + pattern = g_strndup(p, end-p); + action = link_action_create(LINK_ACTION_PARSE, NULL); + } + + if (!action) { + g_free(pattern); + return NULL; + } + + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + "Compiling pattern %s %s %s", pattern, + link_action_type_to_string(link_action_get_type(action)), + link_action_get_command(action)); + + struct LinkMatcher *matcher = malloc(sizeof(struct LinkMatcher)); + GError *err = NULL; + matcher->pattern = g_regex_new(pattern, G_REGEX_OPTIMIZE, 0, &err); + matcher->action = action; + + if (err) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Error compiling pattern %s: %s", pattern, err->message); + g_error_free(err); + free_link_matcher(matcher); + matcher = NULL; + } + + g_free(pattern); + return matcher; +} + +LinkAction *parse_action(const gchar *actionstr) { + if (!actionstr) + return NULL; + + if (*actionstr == '\0') { + return link_action_create(LINK_ACTION_PARSE, NULL); + } else if (strcmp(actionstr, STREAM_LIBQUVI_SELECTOR) == 0) { + return link_action_create(LINK_ACTION_STREAM_LIBQUVI, NULL); + } else if (strncmp(actionstr, EXT_CMD_SELECTOR, EXT_CMD_SELECTOR_LEN) == 0) { + const gchar *command = actionstr + EXT_CMD_SELECTOR_LEN; + return link_action_create(LINK_ACTION_EXTERNAL_COMMAND, command); + } else if (strncmp(actionstr, STREAM_SELECTOR, STREAM_SELECTOR_LEN) == 0) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Unknown streamer %s in link template file", actionstr); + return NULL; + } else { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Invalid action %s in link template file", actionstr); + return NULL; + } +} + +const LinkAction *link_templates_get_action(const LinkTemplates *conf, + const char *url) +{ + for (int i=0; i<link_templates_size(conf); i++) { + struct LinkMatcher *matcher = g_ptr_array_index(conf->matcher, i); + if (g_regex_match(matcher->pattern, url, 0, NULL)) + { + return matcher->action; + } + } + + return NULL; +} + +int link_templates_size(const LinkTemplates *conf) { + return (int)conf->matcher->len; +} + +void free_link_matcher(gpointer ptr) { + if (ptr) { + struct LinkMatcher *matcher = (struct LinkMatcher *)ptr; + if (matcher->pattern) + g_regex_unref(matcher->pattern); + if (matcher->action) + link_action_delete(matcher->action); + free(matcher); + } +} diff --git a/src/libwebvi/linktemplates.h b/src/libwebvi/linktemplates.h new file mode 100644 index 0000000..1c184cc --- /dev/null +++ b/src/libwebvi/linktemplates.h @@ -0,0 +1,34 @@ +/* + * linktemplates.h + * + * Copyright (c) 2013 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 __LINKTEMPLATES_H +#define __LINKTEMPLATES_H + +#include "link.h" + +typedef struct LinkTemplates LinkTemplates; + +LinkTemplates *link_templates_create(); +void link_templates_delete(LinkTemplates *conf); +void link_templates_load(LinkTemplates *conf, const char *filename); +int link_templates_size(const LinkTemplates *conf); +const struct LinkAction *link_templates_get_action(const LinkTemplates *conf, + const char *url); + +#endif // __LINKTEMPLATES_H diff --git a/src/libwebvi/mainmenu.c b/src/libwebvi/mainmenu.c new file mode 100644 index 0000000..df08dce --- /dev/null +++ b/src/libwebvi/mainmenu.c @@ -0,0 +1,120 @@ +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <libxml/tree.h> +#include <libxml/parser.h> +#include "mainmenu.h" +#include "menubuilder.h" + +static GPtrArray *load_websites(const char *path); +static gint title_cmp(gconstpointer a, gconstpointer b); +static gchar *get_site_title(gchar *sitemenu, gsize sitemenu_len); + +char *build_mainmenu(const char *path) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "building main menu %s", path); + + MenuBuilder *menu_builder = menu_builder_create(); + menu_builder_set_title(menu_builder, "Select video source"); + + GPtrArray *websites = load_websites(path); + menu_builder_append_link_list(menu_builder, websites); + char *menu = menu_builder_to_string(menu_builder); + + g_ptr_array_free(websites, TRUE); + menu_builder_delete(menu_builder); + return menu; +} + +/* + * Load known websites from the given directory. + * + * Traverses each subdirectory looking for file called menu.xml. If + * found, reads the file to find site title. Returns an array of + * titles and wvt references. The caller must call g_ptr_array_free() + * on the returned array (the content of the array will be freed + * automatically). + */ +GPtrArray *load_websites(const char *path) { + GPtrArray *websites = g_ptr_array_new_with_free_func(g_free_link); + + GDir *dir = g_dir_open(path, 0, NULL); + if (!dir) + return websites; + + const gchar *dirname; + while ((dirname = g_dir_read_name(dir))) { + gchar *menudir = g_strconcat(path, "/", dirname, NULL); + if (g_file_test(menudir, G_FILE_TEST_IS_DIR)) { + gchar *menufile = g_strconcat(menudir, "/menu.xml", NULL); + + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "processing website menu %s", menufile); + + gchar *sitemenu = NULL; + gsize sitemenu_len; + if (g_file_get_contents(menufile, &sitemenu, &sitemenu_len, NULL)) { + gchar *title = get_site_title(sitemenu, sitemenu_len); + if (!title) { + title = g_strdup(dirname); + } + + gchar *href = g_strconcat("wvt://", menufile, NULL); + Link *menuitem = link_create(href, title, LINK_ACTION_PARSE); + g_ptr_array_add(websites, menuitem); + g_free(href); + g_free(title); + g_free(sitemenu); + } + + g_free(menufile); + } + + g_free(menudir); + } + + g_dir_close(dir); + + g_ptr_array_sort(websites, title_cmp); + + return websites; +} + +gint title_cmp(gconstpointer a, gconstpointer b) { + // a and b are pointers to Link pointers! + Link *link1 = *(Link **)a; + Link *link2 = *(Link **)b; + + return g_ascii_strcasecmp(link_get_title(link1), + link_get_title(link2)); +} + +/* + * Parse the contents of website menu.xml and return site's title. + */ +gchar *get_site_title(gchar *menuxml, gsize menuxml_len) { + gchar *title = NULL; + xmlDocPtr doc = xmlReadMemory(menuxml, menuxml_len, "", NULL, + XML_PARSE_NOWARNING | XML_PARSE_NONET); + if (!doc) + return NULL; + + xmlNode *root = xmlDocGetRootElement(doc); + xmlNode *node = root->children; + while (node) { + if (node->type == XML_ELEMENT_NODE && + xmlStrEqual(node->name, BAD_CAST "title")) { + xmlChar *xmltitle = xmlNodeGetContent(node); + if (xmltitle) { + title = g_strdup((gchar *)xmltitle); + xmlFree(xmltitle); + + break; + } + } + + node = node->next; + } + + xmlFreeDoc(doc); + + return title; +} diff --git a/src/libwebvi/mainmenu.h b/src/libwebvi/mainmenu.h new file mode 100644 index 0000000..b350a5f --- /dev/null +++ b/src/libwebvi/mainmenu.h @@ -0,0 +1,27 @@ +/* + * mainmenu.h + * + * Copyright (c) 2013 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 __MAINMENU_H +#define __MAINMENU_H + +#include <glib.h> + +char *build_mainmenu(const char *path); + +#endif // __MAINMENU_H diff --git a/src/libwebvi/menubuilder.c b/src/libwebvi/menubuilder.c new file mode 100644 index 0000000..d2b54ce --- /dev/null +++ b/src/libwebvi/menubuilder.c @@ -0,0 +1,99 @@ +#include <stdlib.h> +#include <string.h> +#include <libxml/tree.h> +#include "menubuilder.h" + +struct MenuBuilder { + xmlDocPtr doc; + xmlNodePtr root; + xmlNodePtr ul_node; + xmlNodePtr title_node; +}; + +static void add_link_to_menu(gpointer data, gpointer instance); + +MenuBuilder *menu_builder_create() { + MenuBuilder *self = malloc(sizeof(MenuBuilder)); + if (!self) + return NULL; + self->doc = xmlNewDoc(BAD_CAST "1.0"); + self->root = xmlNewNode(NULL, BAD_CAST "wvmenu"); + xmlDocSetRootElement(self->doc, self->root); + self->ul_node = xmlNewNode(NULL, BAD_CAST "ul"); + xmlAddChild(self->root, self->ul_node); + self->title_node = NULL; + return self; +} + +void menu_builder_set_title(MenuBuilder *self, const char *title) { + if (self->title_node) { + xmlUnlinkNode(self->title_node); + xmlFreeNode(self->title_node); + self->title_node = NULL; + } + + self->title_node = xmlNewNode(NULL, BAD_CAST "title"); + xmlNodeAddContent(self->title_node, BAD_CAST title); + + if (self->root->children) { + xmlAddPrevSibling(self->root->children, self->title_node); + } else { + xmlAddChild(self->root, self->title_node); + } +} + +char *menu_builder_to_string(MenuBuilder *self) { + xmlChar *buf; + int buflen; + char *menu; + + xmlDocDumpMemoryEnc(self->doc, &buf, &buflen, "UTF-8"); + menu = malloc(buflen+1); + strcpy(menu, (const char *)buf); + xmlFree(buf); + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "Menu:\n%s", menu); + return menu; +} + +void menu_builder_append_link_list(MenuBuilder *self, GPtrArray *links) { + g_ptr_array_foreach(links, add_link_to_menu, self); +} + +void add_link_to_menu(gpointer data, gpointer instance) { + MenuBuilder *menubuilder = (MenuBuilder *)instance; + Link *link = (Link *)data; + menu_builder_append_link(menubuilder, link); +} + +void menu_builder_append_link(MenuBuilder *self, const Link *link) { + const char *class; + if (link_get_type(link) == LINK_ACTION_STREAM_LIBQUVI) { + class = "stream"; + } else { + class = "webvi"; + } + menu_builder_append_link_plain(self, link_get_href(link), + link_get_title(link), class); +} + +void menu_builder_append_link_plain(MenuBuilder *self, const char *href, + const char *title, const char *class) +{ + xmlNodePtr li_node = xmlNewNode(NULL, BAD_CAST "li"); + xmlNodePtr a_node = xmlNewNode(NULL, BAD_CAST "a"); + if (title) + xmlNodeAddContent(a_node, BAD_CAST title); + if (href) + xmlNewProp(a_node, BAD_CAST "href", BAD_CAST href); + if (class) + xmlNewProp(a_node, BAD_CAST "class", BAD_CAST class); + xmlAddChild(li_node, a_node); + xmlAddChild(self->ul_node, li_node); +} + +void menu_builder_delete(MenuBuilder *self) { + if (self) { + xmlFreeDoc(self->doc); + free(self); + } +} diff --git a/src/libwebvi/menubuilder.h b/src/libwebvi/menubuilder.h new file mode 100644 index 0000000..37de6c2 --- /dev/null +++ b/src/libwebvi/menubuilder.h @@ -0,0 +1,37 @@ +/* + * menubuilder.h + * + * Copyright (c) 2013 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 __MENUBUILDER_H +#define __MENUBUILDER_H + +#include <glib.h> +#include "link.h" + +typedef struct MenuBuilder MenuBuilder; + +MenuBuilder *menu_builder_create(); +void menu_builder_set_title(MenuBuilder *self, const char *title); +char *menu_builder_to_string(MenuBuilder *self); +void menu_builder_append_link_plain(MenuBuilder *self, const char *href, + const char *title, const char *class); +void menu_builder_append_link(MenuBuilder *self, const Link *link); +void menu_builder_append_link_list(MenuBuilder *self, GPtrArray *links); +void menu_builder_delete(MenuBuilder *self); + +#endif // __MENUBUILDER_H diff --git a/src/libwebvi/pipecomponent.c b/src/libwebvi/pipecomponent.c new file mode 100644 index 0000000..a0743da --- /dev/null +++ b/src/libwebvi/pipecomponent.c @@ -0,0 +1,689 @@ +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <libxml/tree.h> +#include <libxml/parser.h> +#include "pipecomponent.h" +#include "menubuilder.h" +#include "linkextractor.h" +#include "mainmenu.h" +#include "webvicontext.h" +#include "version.h" + +#define WEBVID_USER_AGENT "libwebvi/" LIBWEBVI_VERSION " libcurl/" LIBCURL_VERSION + +#define INITIALIZE_PIPE(pipetype, process, finish, delete) \ + pipetype *self = malloc(sizeof(pipetype)); \ + memset(self, 0, sizeof(pipetype)); \ + pipe_component_initialize(&self->pipe_data, (process), (finish), (delete)) + +#define INITIALIZE_PIPE_WITH_FDSET(pipetype, process, finish, delete, fdset, handle_socket) \ + pipetype *self = malloc(sizeof(pipetype)); \ + memset(self, 0, sizeof(pipetype)); \ + pipe_component_initialize_fdset(&self->pipe_data, (process), (finish), (delete), (fdset), (handle_socket)) + +struct PipeDownloader { + PipeComponent pipe_data; + CURL *curl; + CURLM *curlmulti; +}; + +struct PipeLinkExtractor { + PipeComponent pipe_data; + LinkExtractor *link_extractor; +}; + +struct PipeCallbackWrapper { + PipeComponent pipe_data; + void *write_data; + void *finish_data; + ssize_t (*write_callback)(const char *, size_t, void *); + void (*finish_callback)(RequestState, void *); +}; + +struct PipeMainMenuDownloader { + PipeComponent pipe_data; + const WebviContext *context; /* borrowed reference */ +}; + +struct PipeExternalDownloader { + PipeComponent pipe_data; + gchar *url; + int fd; +}; + +struct PipeLocalFile { + PipeComponent pipe_data; + gchar *filename; + int fd; +}; + +struct PipeLibquvi { + PipeComponent pipe_data; + gchar *url; + GPid pid; + gint quvi_output; + xmlParserCtxtPtr parser; +}; + +static void pipe_component_delete(PipeComponent *self); + +static gboolean pipe_link_extractor_append(PipeComponent *instance, char *buf, size_t len); +static void pipe_link_extractor_finished(PipeComponent *instance, RequestState state); +static void pipe_link_extractor_delete(PipeComponent *instance); + +static gboolean pipe_callback_wrapper_process(PipeComponent *instance, + char *buf, size_t len); +static void pipe_callback_wrapper_finished(PipeComponent *instance, + RequestState state); +static void pipe_callback_wrapper_delete(PipeComponent *instance); + +static void pipe_downloader_finished(PipeComponent *instance, RequestState state); +static void pipe_downloader_delete(PipeComponent *instance); +static size_t curl_write_wrapper(char *ptr, size_t size, size_t nmemb, void *userdata); + +static void pipe_mainmenu_downloader_delete(PipeComponent *instance); + +static void pipe_local_file_fdset(PipeComponent *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *maxfd); +static gboolean pipe_local_file_handle_socket(PipeComponent *instance, + int fd, int bitmask); +static void pipe_local_file_delete(PipeComponent *instance); + +static void pipe_external_downloader_fdset(PipeComponent *instance, + fd_set *readfd, fd_set *writefd, + fd_set *excfd, int *maxfd); +static gboolean pipe_external_downloader_handle_socket( + PipeComponent *instance, int fd, int bitmask); +static void pipe_external_downloader_delete(PipeComponent *instance); + +static void pipe_libquvi_fdset(PipeComponent *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *maxfd); +static gboolean pipe_libquvi_handle_socket(PipeComponent *instance, + int fd, int bitmask); +static gboolean pipe_libquvi_parse(PipeComponent *instance, char *buf, size_t len); +static void pipe_libquvi_finished(PipeComponent *instance, RequestState state); +static xmlChar *quvi_xml_get_stream_url(xmlDoc *doc); +static xmlChar *quvi_xml_get_stream_title(xmlDoc *doc); +static void pipe_libquvi_delete(PipeComponent *instance); + +static CURL *start_curl(const char *url, CURLM *curlmulti, + PipeComponent *instance); +static void append_to_fdset(int fd, fd_set *fdset, int *maxfd); +static gboolean read_from_fd_to_pipe(PipeComponent *instance, + int *instance_fd_ptr, int fd, int bitmask); + + +void pipe_component_initialize(PipeComponent *self, + gboolean (*process_cb)(PipeComponent *, char *, size_t), + void (*done_cb)(PipeComponent *, RequestState), + void (*delete_cb)(PipeComponent *)) +{ + g_assert(self); + g_assert(delete_cb); + + memset(self, 0, sizeof(PipeComponent)); + self->process = process_cb; + self->finished = done_cb; + self->delete = delete_cb; + self->state = WEBVISTATE_NOT_FINISHED; +} + +void pipe_component_initialize_fdset(PipeComponent *self, + gboolean (*process_cb)(PipeComponent *, char *, size_t), + void (*done_cb)(PipeComponent *, RequestState), + void (*delete_cb)(PipeComponent *), + void (*fdset_cb)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *), + gboolean (*handle_socket_cb)(PipeComponent *, int, int)) +{ + pipe_component_initialize(self, process_cb, done_cb, delete_cb); + self->fdset = fdset_cb; + self->handle_socket = handle_socket_cb; +} + +void pipe_component_set_next(PipeComponent *self, PipeComponent *next) { + g_assert(!self->next); + self->next = next; +} + +void pipe_component_append(PipeComponent *self, char *buf, size_t length) { + if (self->state == WEBVISTATE_NOT_FINISHED) { + gboolean propagate = TRUE; + if (self->process) + propagate = self->process(self, buf, length); + if (propagate && self->next) + pipe_component_append(self->next, buf, length); + } +} + +void pipe_component_finished(PipeComponent *self, RequestState state) { + if (self->state == WEBVISTATE_NOT_FINISHED) { + self->state = state; + if (self->finished) + self->finished(self, state); + if (self->next) + pipe_component_finished(self->next, state); + } +} + +RequestState pipe_component_get_state(const PipeComponent *self) { + return self->state; +} + +void pipe_fdset(PipeComponent *head, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd) +{ + PipeComponent *component = head; + PipeComponent *next; + while (component) { + next = component->next; + if (component->fdset) { + component->fdset(component, readfd, writefd, excfd, max_fd); + } + component = next; + } +} + +gboolean pipe_handle_socket(PipeComponent *head, int sockfd, int ev_bitmask) { + PipeComponent *component = head; + PipeComponent *next; + while (component) { + next = component->next; + if (component->handle_socket) { + if (component->handle_socket(component, sockfd, ev_bitmask)) { + return TRUE; + } + } + component = next; + } + return FALSE; +} + +void pipe_component_delete(PipeComponent *self) { + if (self && self->delete) + self->delete(self); +} + +void pipe_delete_full(PipeComponent *head) { + PipeComponent *component = head; + PipeComponent *next; + while (component) { + next = component->next; + pipe_component_delete(component); + component = next; + } +} + + +/***** PipeLinkExtractor *****/ + + +PipeLinkExtractor *pipe_link_extractor_create( + const LinkTemplates *link_templates, const gchar *baseurl) +{ + INITIALIZE_PIPE(PipeLinkExtractor, pipe_link_extractor_append, + pipe_link_extractor_finished, pipe_link_extractor_delete); + self->link_extractor = link_extractor_create(link_templates, baseurl); + return self; +} + +void pipe_link_extractor_delete(PipeComponent *instance) { + PipeLinkExtractor *self = (PipeLinkExtractor *)instance; + link_extractor_delete(self->link_extractor); + free(self); +} + +gboolean pipe_link_extractor_append(PipeComponent *instance, char *buf, size_t len) { + PipeLinkExtractor *self = (PipeLinkExtractor *)instance; + link_extractor_append(self->link_extractor, buf, len); + return FALSE; +} + +void pipe_link_extractor_finished(PipeComponent *instance, + RequestState state) +{ + PipeLinkExtractor *self = (PipeLinkExtractor *)instance; + if (state == WEBVISTATE_FINISHED_OK) { + GPtrArray *links = link_extractor_get_links(self->link_extractor); + MenuBuilder *menu_builder = menu_builder_create(); + menu_builder_append_link_list(menu_builder, links); + char *menu = menu_builder_to_string(menu_builder); + if (self->pipe_data.next) { + pipe_component_append(self->pipe_data.next, menu, strlen(menu)); + pipe_component_finished(self->pipe_data.next, state); + } + menu_builder_delete(menu_builder); + free(menu); + g_ptr_array_free(links, TRUE); + } +} + + +/***** PipeDownloader *****/ + + +PipeDownloader *pipe_downloader_create(const char *url, CURLM *curlmulti) { + INITIALIZE_PIPE(PipeDownloader, NULL, pipe_downloader_finished, + pipe_downloader_delete); + self->curl = start_curl(url, curlmulti, (PipeComponent *)self); + if (!self->curl) { + pipe_downloader_delete((PipeComponent *)self); + return NULL; + } + self->curlmulti = curlmulti; + return self; +} + +void pipe_downloader_start(PipeDownloader *self) { + CURLMcode mcode = curl_multi_add_handle(self->curlmulti, self->curl); + if (mcode != CURLM_OK) { + pipe_component_finished(&self->pipe_data, WEBVISTATE_INTERNAL_ERROR); + } +} + +size_t curl_write_wrapper(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + PipeComponent *pipedata = (PipeComponent *)userdata; + if (pipe_component_get_state(pipedata) == WEBVISTATE_NOT_FINISHED) { + pipe_component_append(pipedata, ptr, size*nmemb); + return size*nmemb; + } else { + return 0; + } +} + +void pipe_downloader_finished(PipeComponent *instance, RequestState state) { + PipeDownloader *self = (PipeDownloader *)instance; + curl_multi_remove_handle(self->curlmulti, self->curl); +} + +void pipe_downloader_delete(PipeComponent *instance) { + PipeDownloader *self = (PipeDownloader *)instance; + if (self->pipe_data.state == WEBVISTATE_NOT_FINISHED) { + curl_multi_remove_handle(self->curlmulti, self->curl); + } + curl_easy_cleanup(self->curl); + free(instance); +} + + +/***** PipeMainMenuDownloader *****/ + + +PipeMainMenuDownloader *pipe_mainmenu_downloader_create(WebviContext *context) { + INITIALIZE_PIPE(PipeMainMenuDownloader, NULL, NULL, + pipe_mainmenu_downloader_delete); + self->context = context; + return self; +} + +void pipe_mainmenu_downloader_start(PipeMainMenuDownloader *self) { + char *mainmenu = build_mainmenu(webvi_context_get_template_path(self->context)); + if (!mainmenu) { + pipe_component_finished(&self->pipe_data, WEBVISTATE_INTERNAL_ERROR); + return; + } + + pipe_component_append(&self->pipe_data, mainmenu, strlen(mainmenu)); + pipe_component_finished(&self->pipe_data, WEBVISTATE_FINISHED_OK); + + g_free(mainmenu); +} + +void pipe_mainmenu_downloader_delete(PipeComponent *instance) { + PipeMainMenuDownloader *self = (PipeMainMenuDownloader *)instance; + free(self); +} + + +/***** PipeCallbackWrapper *****/ + + +PipeCallbackWrapper *pipe_callback_wrapper_create( + ssize_t (*write_callback)(const char *, size_t, void *), + void *writedata, + void (*finish_callback)(RequestState, void *), + void *finishdata) +{ + INITIALIZE_PIPE(PipeCallbackWrapper, + pipe_callback_wrapper_process, + pipe_callback_wrapper_finished, + pipe_callback_wrapper_delete); + self->write_data = writedata; + self->finish_data = finishdata; + self->write_callback = write_callback; + self->finish_callback = finish_callback; + return self; +} + +gboolean pipe_callback_wrapper_process(PipeComponent *instance, + char *buf, size_t len) +{ + PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance; + if (self->write_callback) + self->write_callback(buf, len, self->write_data); + return TRUE; +} + +void pipe_callback_wrapper_finished(PipeComponent *instance, + RequestState state) +{ + PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance; + if (self->finish_callback) + self->finish_callback(state, self->finish_data); +} + +void pipe_callback_wrapper_delete(PipeComponent *instance) { + PipeCallbackWrapper *self = (PipeCallbackWrapper *)instance; + free(self); +} + +PipeLocalFile *pipe_local_file_create(const gchar *filename) { + INITIALIZE_PIPE_WITH_FDSET(PipeLocalFile, NULL, NULL, + pipe_local_file_delete, + pipe_local_file_fdset, + pipe_local_file_handle_socket); + self->filename = g_strdup(filename); + self->fd = -1; + return self; +} + +void pipe_local_file_start(PipeLocalFile *self) { + if (!self->filename) { + pipe_component_finished((PipeComponent *)self, WEBVISTATE_NOT_FOUND); + } + + self->fd = open(self->filename, O_RDONLY); + if (self->fd == -1) { + pipe_component_finished((PipeComponent *)self, WEBVISTATE_NOT_FOUND); + } +} + +void pipe_local_file_fdset(PipeComponent *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *maxfd) +{ + PipeLocalFile *self = (PipeLocalFile *)instance; + append_to_fdset(self->fd, readfd, maxfd); +} + +gboolean pipe_local_file_handle_socket(PipeComponent *instance, int fd, int bitmask) { + PipeLocalFile *self = (PipeLocalFile *)instance; + return read_from_fd_to_pipe(instance, &self->fd, fd, bitmask); +} + +void pipe_local_file_delete(PipeComponent *instance) { + PipeLocalFile *self = (PipeLocalFile *)instance; + g_free(self->filename); + if (self->fd != -1) + close(self->fd); + free(self); +} + + +/***** PipeExternalDownloader *****/ + + +PipeExternalDownloader *pipe_external_downloader_create(const gchar *url, + const gchar *command) +{ + INITIALIZE_PIPE_WITH_FDSET(PipeExternalDownloader, NULL, NULL, + pipe_external_downloader_delete, + pipe_external_downloader_fdset, + pipe_external_downloader_handle_socket); + self->url = g_strdup(url); + self->fd = -1; + + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + "external downloader:\nurl: %s\n%s", url, command); + + return self; +} + +void pipe_external_downloader_start(PipeExternalDownloader *self) { + // FIXME + pipe_component_finished((PipeComponent *)self, WEBVISTATE_INTERNAL_ERROR); +} + +void pipe_external_downloader_fdset(PipeComponent *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *maxfd) +{ + PipeExternalDownloader *self = (PipeExternalDownloader *)instance; + append_to_fdset(self->fd, readfd, maxfd); +} + +gboolean pipe_external_downloader_handle_socket(PipeComponent *instance, + int fd, int bitmask) +{ + PipeExternalDownloader *self = (PipeExternalDownloader *)instance; + return read_from_fd_to_pipe(instance, &self->fd, fd, bitmask); +} + +void pipe_external_downloader_delete(PipeComponent *instance) { + PipeExternalDownloader *self = (PipeExternalDownloader *)instance; + if (self->fd != -1) + close(self->fd); + g_free(self->url); + g_free(self); +} + + +/***** PipeLibquvi *****/ + + +PipeLibquvi *pipe_libquvi_create(const gchar *url) { + INITIALIZE_PIPE_WITH_FDSET(PipeLibquvi, + pipe_libquvi_parse, + pipe_libquvi_finished, + pipe_libquvi_delete, + pipe_libquvi_fdset, + pipe_libquvi_handle_socket); + self->url = g_strdup(url); + self->pid = -1; + self->quvi_output = -1; + return self; +} + +void pipe_libquvi_start(PipeLibquvi *self) { + GError *error = NULL; + gchar *argv[] = {"quvi", "--xml", self->url}; + + g_spawn_async_with_pipes(NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, + NULL, NULL, &self->pid, NULL, &self->quvi_output, + NULL, &error); + if (error) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Calling quvi failed: %s", error->message); + pipe_component_finished((PipeComponent *)self, WEBVISTATE_SUBPROCESS_FAILED); + } +} + +void pipe_libquvi_fdset(PipeComponent *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *maxfd) +{ + PipeLibquvi *self = (PipeLibquvi *)instance; + append_to_fdset(self->quvi_output, readfd, maxfd); +} + +gboolean pipe_libquvi_handle_socket(PipeComponent *instance, + int fd, int bitmask) +{ + PipeLibquvi *self = (PipeLibquvi *)instance; + return read_from_fd_to_pipe(instance, &self->quvi_output, fd, bitmask); +} + +gboolean pipe_libquvi_parse(PipeComponent *instance, char *buf, size_t len) { + PipeLibquvi *self = (PipeLibquvi *)instance; + if (!self->parser) { + self->parser = xmlCreatePushParserCtxt(NULL, NULL, buf, len, "quvioutput.xml"); + g_assert(self->parser); + } else { + xmlParseChunk(self->parser, buf, len, 0); + } + + return FALSE; +} + +void pipe_libquvi_finished(PipeComponent *instance, RequestState state) { + if (state != WEBVISTATE_FINISHED_OK) { + pipe_component_finished(instance, state); + return; + } + + PipeLibquvi *self = (PipeLibquvi *)instance; + if (!self->parser) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "No output from quvi!"); + pipe_component_finished(instance, WEBVISTATE_SUBPROCESS_FAILED); + return; + } + + xmlParseChunk(self->parser, NULL, 0, 1); + xmlDoc *doc = self->parser->myDoc; + + xmlChar *dump; + int dumpLen; + xmlDocDumpMemory(doc, &dump, &dumpLen); + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "quvi output:\n%s", dump); + xmlFree(dump); + dump = NULL; + + xmlChar *encoded_url = quvi_xml_get_stream_url(doc); + if (!encoded_url) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "No url in quvi output!"); + pipe_component_finished(instance, WEBVISTATE_SUBPROCESS_FAILED); + return; + } + + /* URLs in quvi XML output are encoded by curl_easy_escape */ + char *url = curl_unescape((char *)encoded_url, strlen((char *)encoded_url)); + xmlChar *title = quvi_xml_get_stream_title(doc); + + MenuBuilder *menu_builder = menu_builder_create(); + menu_builder_set_title(menu_builder, (char *)title); + menu_builder_append_link_plain(menu_builder, url, (char *)title, NULL); + char *menu = menu_builder_to_string(menu_builder); + if (self->pipe_data.next) { + pipe_component_append(self->pipe_data.next, menu, strlen(menu)); + pipe_component_finished(self->pipe_data.next, state); + } + + free(menu); + menu_builder_delete(menu_builder); + xmlFree(title); + curl_free(url); + xmlFree(encoded_url); +} + +xmlChar *quvi_xml_get_stream_url(xmlDoc *doc) { + xmlNode *root = xmlDocGetRootElement(doc); + xmlNode *node = root->children; + while (node) { + if (xmlStrEqual(node->name, BAD_CAST "link")) { + xmlNode *link_child = node->children; + while (link_child) { + if (xmlStrEqual(link_child->name, BAD_CAST "url")) { + return xmlNodeGetContent(link_child); + } + link_child = link_child->next; + } + } + node = node->next; + } + + return NULL; +} + +xmlChar *quvi_xml_get_stream_title(xmlDoc *doc) { + xmlNode *root = xmlDocGetRootElement(doc); + xmlNode *node = root->children; + while (node) { + if (xmlStrEqual(node->name, BAD_CAST "page_title")) { + return xmlNodeGetContent(node); + } + node = node->next; + } + + return NULL; +} + +void pipe_libquvi_delete(PipeComponent *instance) { + PipeLibquvi *self = (PipeLibquvi *)instance; + if (self->quvi_output != -1) { + close(self->quvi_output); + } + if (self->pid != -1) { + g_spawn_close_pid(self->pid); + } + if (self->parser) { + xmlFreeParserCtxt(self->parser); + } + g_free(self->url); + free(self); +} + + +/***** Utility functions *****/ + + +CURL *start_curl(const char *url, CURLM *curlmulti, PipeComponent *instance) { + CURL *curl = curl_easy_init(); + if (!curl) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "curl initialization failed"); + return NULL; + } + + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "Downloading %s", url); + + curl_easy_setopt(curl, CURLOPT_USERAGENT, WEBVID_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_wrapper); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, instance); + if (url) + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_PRIVATE, instance); + + // FIXME: headers, cookies + + return curl; +} + +void append_to_fdset(int fd, fd_set *fdset, int *maxfd) { + if (fd != -1) { + FD_SET(fd, fdset); + if (fd > *maxfd) + *maxfd = fd; + } +} + +gboolean read_from_fd_to_pipe(PipeComponent *instance, int *instance_fd_ptr, + int fd, int bitmask) +{ + const int buflen = 4096; + char buf[buflen]; + ssize_t numbytes; + + gboolean owned_socket = (fd == -1) || (fd == *instance_fd_ptr); + gboolean read_operation = ((bitmask & WEBVI_SELECT_READ) != 0) || + (bitmask == WEBVI_SELECT_CHECK); + + if (owned_socket && read_operation) { + numbytes = read(fd, buf, buflen); + if (numbytes < 0) { + /* error */ + pipe_component_finished(instance, WEBVISTATE_IO_ERROR); + } else if (numbytes == 0) { + /* end of file */ + pipe_component_finished(instance, WEBVISTATE_FINISHED_OK); + close(fd); + *instance_fd_ptr = -1; + } else { + pipe_component_append(instance, buf, numbytes); + } + + return TRUE; + } else { + return FALSE; + } +} diff --git a/src/libwebvi/pipecomponent.h b/src/libwebvi/pipecomponent.h new file mode 100644 index 0000000..00a05ef --- /dev/null +++ b/src/libwebvi/pipecomponent.h @@ -0,0 +1,95 @@ +/* + * pipecomponent.h + * + * Copyright (c) 2013 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 __PIPECOMPONENT_H +#define __PIPECOMPONENT_H + +#include <stdlib.h> +#include <sys/select.h> +#include <glib.h> +#include <curl/curl.h> +#include "libwebvi.h" + +typedef struct PipeComponent { + struct PipeComponent *next; + RequestState state; + gboolean (*process)(struct PipeComponent *self, char *, size_t); + void (*finished)(struct PipeComponent *self, RequestState state); + void (*delete)(struct PipeComponent *self); + void (*fdset)(struct PipeComponent *self, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); + gboolean (*handle_socket)(struct PipeComponent *self, int fd, int ev_bitmask); +} PipeComponent; + +struct WebviContext; +struct LinkTemplates; + +typedef struct PipeDownloader PipeDownloader; +typedef struct PipeLinkExtractor PipeLinkExtractor; +typedef struct PipeCallbackWrapper PipeCallbackWrapper; +typedef struct PipeMainMenuDownloader PipeMainMenuDownloader; +typedef struct PipeLocalFile PipeLocalFile; +typedef struct PipeExternalDownloader PipeExternalDownloader; +typedef struct PipeLibquvi PipeLibquvi; + +void pipe_component_initialize(PipeComponent *self, + gboolean (*process_cb)(PipeComponent *, char *, size_t), + void (*done_cb)(PipeComponent *, RequestState state), + void (*delete_cb)(PipeComponent *)); +void pipe_component_initialize_fdset(PipeComponent *self, + gboolean (*process_cb)(PipeComponent *, char *, size_t), + void (*done_cb)(PipeComponent *, RequestState state), + void (*delete_cb)(PipeComponent *), + void (*fdset_cb)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *), + gboolean (*handle_socket_cb)(PipeComponent *, int, int)); +void pipe_component_append(PipeComponent *self, char *buf, size_t length); +void pipe_component_finished(PipeComponent *self, RequestState state); +void pipe_component_set_next(PipeComponent *self, PipeComponent *next); +RequestState pipe_component_get_state(const PipeComponent *self); +void pipe_fdset(PipeComponent *head, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); +gboolean pipe_handle_socket(PipeComponent *head, int sockfd, int ev_bitmask); +void pipe_delete_full(PipeComponent *head); + +PipeDownloader *pipe_downloader_create(const char *url, CURLM *curlmulti); +void pipe_downloader_start(PipeDownloader *self); + +PipeMainMenuDownloader *pipe_mainmenu_downloader_create(struct WebviContext *context); +void pipe_mainmenu_downloader_start(PipeMainMenuDownloader *self); + +PipeLinkExtractor *pipe_link_extractor_create( + const struct LinkTemplates *link_templates, const gchar *baseurl); + +PipeLocalFile *pipe_local_file_create(const gchar *filename); +void pipe_local_file_start(PipeLocalFile *self); + +PipeCallbackWrapper *pipe_callback_wrapper_create( + ssize_t (*write_callback)(const char *, size_t, void *), + void *writedata, + void (*finish_callback)(RequestState, void *), + void *finishdata); + +PipeExternalDownloader *pipe_external_downloader_create(const gchar *url, + const gchar *command); +void pipe_external_downloader_start(PipeExternalDownloader *self); + +PipeLibquvi *pipe_libquvi_create(const gchar *url); +void pipe_libquvi_start(PipeLibquvi *self); + +#endif // __PIPECOMPONENT_H diff --git a/src/libwebvi/request.c b/src/libwebvi/request.c new file mode 100644 index 0000000..af078c4 --- /dev/null +++ b/src/libwebvi/request.c @@ -0,0 +1,237 @@ +#include <glib.h> +#include <string.h> +#include "pipecomponent.h" +#include "webvicontext.h" +#include "request.h" +#include "link.h" + +struct WebviRequest { + PipeComponent *pipe_head; + gchar *url; + webvi_callback write_cb; + webvi_callback read_cb; + void *writedata; + void *readdata; + struct WebviContext *ctx; /* borrowed reference */ +}; + +struct RequestStateMessage { + RequestState state; + const char *message; +}; + +static PipeComponent *pipe_factory(const WebviRequest *self); +static void notify_pipe_finished(RequestState state, void *data); +static const char *pipe_state_to_message(RequestState state); +static gchar *wvt_to_local_file(const WebviContext *ctx, const char *wvt); +static PipeComponent *build_and_start_mainmenu_pipe(const WebviRequest *self); +static PipeComponent *build_and_start_local_pipe(const WebviRequest *self); +static PipeComponent *build_and_start_external_pipe(const WebviRequest *self, + const char *command); +static PipeComponent *build_and_start_libquvi_pipe(const WebviRequest *self); +static PipeComponent *build_and_start_menu_pipe(const WebviRequest *self); + +WebviRequest *request_create(const char *url, struct WebviContext *ctx) { + WebviRequest *req = malloc(sizeof(WebviRequest)); + memset(req, 0, sizeof(WebviRequest)); + req->url = g_strdup(url); + req->ctx = ctx; + return req; +} + +void request_delete(WebviRequest *self) { + if (self) { + request_stop(self); + pipe_delete_full(self->pipe_head); + g_free(self->url); + } +} + +gboolean request_start(WebviRequest *self) { + if (!self->pipe_head) { + self->pipe_head = pipe_factory(self); + if (!self->pipe_head) { + notify_pipe_finished(WEBVISTATE_NOT_FOUND, self); + } + } + return TRUE; +} + +void request_stop(WebviRequest *self) { + // FIXME +} + +PipeComponent *pipe_factory(const WebviRequest *self) { + PipeComponent *head; + if (strcmp(self->url, "wvt://mainmenu") == 0) { + head = build_and_start_mainmenu_pipe(self); + + } else if (strncmp(self->url, "wvt://", 6) == 0) { + head = build_and_start_local_pipe(self); + + } else { + const LinkAction *action = link_templates_get_action( + get_link_templates(self->ctx), self->url); + LinkActionType action_type = action ? link_action_get_type(action) : LINK_ACTION_PARSE; + if (action_type == LINK_ACTION_STREAM_LIBQUVI) { + head = build_and_start_libquvi_pipe(self); + } else if (action_type == LINK_ACTION_EXTERNAL_COMMAND) { + const char *command = link_action_get_command(action); + head = build_and_start_external_pipe(self, command); + } else { + head = build_and_start_menu_pipe(self); + } + } + + return head; +} + +PipeComponent *build_and_start_mainmenu_pipe(const WebviRequest *self) { + PipeMainMenuDownloader *p1 = pipe_mainmenu_downloader_create(self->ctx); + PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create( + self->read_cb, self->readdata, notify_pipe_finished, (void *)self); + pipe_component_set_next((PipeComponent *)p1, p2); + pipe_mainmenu_downloader_start(p1); + + return (PipeComponent *)p1; +} + +PipeComponent *build_and_start_local_pipe(const WebviRequest *self) { + gchar *filename = wvt_to_local_file(self->ctx, self->url); + PipeLocalFile *p1 = pipe_local_file_create(filename); + g_free(filename); + PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create( + self->read_cb, self->readdata, notify_pipe_finished, (void *)self); + pipe_component_set_next((PipeComponent *)p1, p2); + pipe_local_file_start(p1); + + return (PipeComponent *)p1; +} + +PipeComponent *build_and_start_external_pipe(const WebviRequest *self, const char *command) { + PipeExternalDownloader *p1 = pipe_external_downloader_create(self->url, command); + PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create( + self->read_cb, self->readdata, notify_pipe_finished, (void *)self); + pipe_component_set_next((PipeComponent *)p1, p2); + pipe_external_downloader_start(p1); + + return (PipeComponent *)p1; +} + +PipeComponent *build_and_start_libquvi_pipe(const WebviRequest *self) { + PipeLibquvi *p1 = pipe_libquvi_create(self->url); + PipeComponent *p2 = (PipeComponent *)pipe_callback_wrapper_create( + self->read_cb, self->readdata, notify_pipe_finished, (void *)self); + pipe_component_set_next((PipeComponent *)p1, p2); + pipe_libquvi_start(p1); + + return (PipeComponent *)p1; +} + +PipeComponent *build_and_start_menu_pipe(const WebviRequest *self) { + CURLM *curlmulti = webvi_context_get_curl_multi_handle(self->ctx); + PipeDownloader *p1 = pipe_downloader_create(self->url, curlmulti); + PipeComponent *p2 = (PipeComponent *)pipe_link_extractor_create( + get_link_templates(self->ctx), self->url); + PipeComponent *p3 = (PipeComponent *)pipe_callback_wrapper_create( + self->read_cb, self->readdata, notify_pipe_finished, (void *)self); + pipe_component_set_next((PipeComponent *)p1, p2); + pipe_component_set_next(p2, p3); + pipe_downloader_start(p1); + + return (PipeComponent *)p1; +} + +gchar *wvt_to_local_file(const WebviContext *ctx, const char *wvt) { + if (strncmp(wvt, "wvt://", 6) != 0) + return NULL; + + const gchar *template_path = webvi_context_get_template_path(ctx); + if (!template_path) + return NULL; // FIXME + + // FIXME: .. in paths + + gchar *filename = g_strdup(wvt+6); + if (filename[0] == '/') { + // absolute path + // The path must be located under the template directory + if (strncmp(filename, template_path, strlen(template_path)) != 0) { + g_log(LIBWEBVI_LOG_DOMAIN, G_LOG_LEVEL_WARNING, + "Invalid path in wvt:// url"); + g_free(filename); + return NULL; + } + } else { + // relative path, concatenate to template_path + gchar *absolute_path = g_strconcat(template_path, "/", filename, NULL); + g_free(filename); + filename = absolute_path; + } + + return filename; +} + +void request_fdset(WebviRequest *self, fd_set *readfds, + fd_set *writefds, fd_set *excfds, int *max_fd) +{ + if (self->pipe_head) { + pipe_fdset(self->pipe_head, readfds, writefds, excfds, max_fd); + } +} + +gboolean request_handle_socket(WebviRequest *self, int sockfd, int ev_bitmask) +{ + if (self->pipe_head) { + return pipe_handle_socket(self->pipe_head, sockfd, ev_bitmask); + } else { + return FALSE; + } +} + +void request_set_write_callback(WebviRequest *self, webvi_callback func) { + self->write_cb = func; +} + +void request_set_read_callback(WebviRequest *self, webvi_callback func) { + self->read_cb = func; +} + +void request_set_write_data(WebviRequest *self, void *data) { + self->writedata = data; +} + +void request_set_read_data(WebviRequest *self, void *data) { + self->readdata = data; +} + +const char *request_get_url(const WebviRequest *self) { + return self->url; +} + +void notify_pipe_finished(RequestState state, void *data) { + WebviRequest *req = (WebviRequest *)data; + const char *msg = pipe_state_to_message(state); + webvi_context_add_finished_message(req->ctx, req, state, msg); +} + +const char *pipe_state_to_message(RequestState state) { + static struct RequestStateMessage messages[] = + {{WEBVISTATE_NOT_FINISHED, "Not finished"}, + {WEBVISTATE_FINISHED_OK, "Success"}, + {WEBVISTATE_MEMORY_ALLOCATION_ERROR, "Out of memory"}, + {WEBVISTATE_NOT_FOUND, "Not found"}, + {WEBVISTATE_NETWORK_READ_ERROR, "Failed to receive data from the network"}, + {WEBVISTATE_IO_ERROR, "IO error"}, + {WEBVISTATE_TIMEDOUT, "Timedout"}, + {WEBVISTATE_SUBPROCESS_FAILED, "Failed to execute a subprocess"}, + {WEBVISTATE_INTERNAL_ERROR, "Internal error"}}; + + for (int i=0; i<(sizeof(messages)/sizeof(messages[0])); i++) { + if (state == messages[i].state) { + return messages[i].message; + } + } + + return "Internal error"; +} diff --git a/src/libwebvi/request.h b/src/libwebvi/request.h new file mode 100644 index 0000000..c52c91e --- /dev/null +++ b/src/libwebvi/request.h @@ -0,0 +1,42 @@ +/* + * request.h + * + * Copyright (c) 2013 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 __REQUEST_H +#define __REQUEST_H + +#include "pipecomponent.h" + +struct WebviContext; + +typedef struct WebviRequest WebviRequest; + +WebviRequest *request_create(const char *url, struct WebviContext *ctx); +void request_set_write_callback(WebviRequest *instance, webvi_callback func); +void request_set_read_callback(WebviRequest *instance, webvi_callback func); +void request_set_write_data(WebviRequest *instance, void *data); +void request_set_read_data(WebviRequest *instance, void *data); +const char *request_get_url(const WebviRequest *instance); +gboolean request_start(WebviRequest *instance); +void request_stop(WebviRequest *instance); +void request_fdset(WebviRequest *instance, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); +gboolean request_handle_socket(WebviRequest *instance, int sockfd, int ev_bitmask); +void request_delete(WebviRequest *instance); + +#endif // __REQUEST_H diff --git a/src/libwebvi/urlutils.c b/src/libwebvi/urlutils.c new file mode 100644 index 0000000..de64e06 --- /dev/null +++ b/src/libwebvi/urlutils.c @@ -0,0 +1,132 @@ +#include <string.h> +#include "urlutils.h" + +static const gchar *skip_scheme(const char *url); +static gboolean is_scheme_character(gchar c); + +gchar *relative_url_to_absolute(const gchar *baseurl, const gchar *href) { + gchar *absolute; + gchar *prefix; + const gchar *postfix = href; + if ((href[0] == '/') && (href[1] == '/')) { + gchar *scheme = url_scheme(baseurl); + prefix = g_strconcat(scheme, ":", NULL); + g_free(scheme); + } else if (href[0] == '/') { + prefix = url_root(baseurl); + if (g_str_has_suffix(prefix, "/")) { + postfix = href+1; + } + } else if (href[0] == '?') { + prefix = url_path_including_file(baseurl); + } else if (href[0] =='#') { + prefix = url_path_and_query(baseurl); + } else if (strstr(href, "://") == NULL) { + prefix = url_path_dirname(baseurl); + } else { + // href is absolute + prefix = NULL; + } + + if (prefix) { + absolute = g_strconcat(prefix, postfix, NULL); + g_free(prefix); + } else { + absolute = g_strdup(href); + } + + return absolute; +} + +gchar *url_scheme(const gchar *url) { + if (!url) + return NULL; + + const gchar *scheme_end = skip_scheme(url); + if (scheme_end == url) { + // no scheme + return g_strdup(""); + } else { + // scheme found + // Do not include :// in the return value + g_assert(scheme_end >= url+3); + return g_strndup(url, scheme_end-3 - url); + } +} + +gchar *url_root(const gchar *url) { + if (!url) + return NULL; + + const gchar *authority = skip_scheme(url); + size_t authority_len = strcspn(authority, "/?#"); + const gchar *authority_end = authority + authority_len; + gchar *root_without_slash = g_strndup(url, authority_end - url); + gchar *root = g_strconcat(root_without_slash, "/", NULL); + g_free(root_without_slash); + return root; +} + +gchar *url_path_including_file(const gchar *url) { + if (!url) + return NULL; + + const gchar *scheme_end = skip_scheme(url); + size_t path_len = strcspn(scheme_end, "?#"); + const gchar *end = scheme_end + path_len; + gchar *path = g_strndup(url, end - url); + if (memchr(scheme_end, '/', path_len) == NULL) { + gchar *path2 = g_strconcat(path, "/", NULL); + g_free(path); + path = path2; + } + + return path; +} + +gchar *url_path_dirname(const gchar *url) { + if (!url) + return NULL; + + const gchar *scheme_end = skip_scheme(url); + size_t path_len = strcspn(scheme_end, "?#"); + const gchar *p = scheme_end + path_len; + while ((p >= url) && (*p != '/')) { + p--; + } + + if (*p == '/') { + return g_strndup(url, (p+1) - url); + } else { + return g_strdup("/"); + } +} + +gchar *url_path_and_query(const gchar *url) { + if (!url) + return NULL; + + const gchar *scheme_end = skip_scheme(url); + size_t path_len = strcspn(scheme_end, "#"); + const gchar *end = scheme_end + path_len; + return g_strndup(url, end - url); +} + +const gchar *skip_scheme(const char *url) { + const gchar *c = url; + while (is_scheme_character(*c)) { + c++; + } + + if (strncmp(c, "://", 3) == 0) { + // scheme found + return c + 3; + } else { + // schemeless url + return url; + } +} + +gboolean is_scheme_character(gchar c) { + return g_ascii_isalnum(c) || (c == '+') || (c == '-') || (c == '.'); +} diff --git a/src/libwebvi/urlutils.h b/src/libwebvi/urlutils.h new file mode 100644 index 0000000..374a941 --- /dev/null +++ b/src/libwebvi/urlutils.h @@ -0,0 +1,32 @@ +/* + * urlutils.h + * + * Copyright (c) 2013 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 __URLUTILS_H +#define __URLUTILS_H + +#include <glib.h> + +gchar *relative_url_to_absolute(const gchar *baseurl, const gchar *href); +gchar *url_scheme(const gchar *baseurl); +gchar *url_root(const gchar *baseurl); +gchar *url_path_including_file(const gchar *baseurl); +gchar *url_path_dirname(const gchar *baseurl); +gchar *url_path_and_query(const gchar *baseurl); + +#endif // __URLUTILS_H diff --git a/src/libwebvi/webvicontext.c b/src/libwebvi/webvicontext.c new file mode 100644 index 0000000..4e33e94 --- /dev/null +++ b/src/libwebvi/webvicontext.c @@ -0,0 +1,409 @@ +#include <stdlib.h> +#include <string.h> +#include <glib/gprintf.h> +#include "webvicontext.h" +#include "libwebvi.h" +#include "request.h" + +#define DEFAULT_TEMPLATE_PATH "/etc/webvi/websites" +#define MAX_MESSAGE_LENGTH 128 + +struct WebviContext { + GTree *requests; + LinkTemplates *link_templates; + WebviHandle next_request; + CURLM *curl_multi_handle; + gchar *template_path; + GArray *finish_messages; + /* The value returned by the latest webvi_context_next_message() call */ + WebviMsg current_message; + bool debug; +}; + +typedef struct RequestAndHandle { + const WebviRequest *request; + WebviHandle handle; +} RequestAndHandle; + +typedef struct FoundFds { + fd_set *readfds; + fd_set *writefds; + fd_set *excfds; + int *max_fd; +} FoundFds; + +typedef struct SocketToHandle { + int sockfd; + int ev_bitmask; + gboolean handled; +} SocketToHandle; + +static WebviCtx handle_for_context(WebviContext *ctx); +static gint cmp_int(gconstpointer a, gconstpointer b, gpointer user_data); +static gboolean gather_fds(gpointer key, gpointer value, gpointer data); +static gboolean handle_request_socket(gpointer key, gpointer value, gpointer data); +static WebviHandle get_handle_for_request(WebviContext *ctx, const WebviRequest *req); +static gboolean search_by_request(gpointer key, gpointer value, gpointer data); +static void check_for_finished_curl(CURLM *multi_handle); +static RequestState curl_code_to_pipe_state(CURLcode curlcode); +static WebviResult curlmcode_to_webvierr(CURLMcode mcode); +static void webvi_log_handler(const gchar *log_domain, GLogLevelFlags log_level, + const gchar *message, gpointer user_data); +static void register_context(WebviCtx key, WebviContext *value); +static GTree *get_tls_contexts(); +static void webvi_context_delete(WebviContext *ctx); +static void free_tls_context(gpointer data); +static void free_context(gpointer data); +static void free_request(gpointer data); + +static GPrivate tls_contexts = G_PRIVATE_INIT(free_tls_context); + +WebviCtx webvi_context_initialize() { + WebviContext *ctx = malloc(sizeof(WebviContext)); + if (!ctx) { + return 0; + } + + memset(ctx, 0, sizeof(WebviContext)); + + ctx->requests = g_tree_new_full(cmp_int, NULL, NULL, free_request); + if (!ctx->requests) { + webvi_context_delete(ctx); + return 0; + } + + ctx->finish_messages = g_array_new(FALSE, TRUE, sizeof(WebviMsg)); + if (!ctx->finish_messages) { + webvi_context_delete(ctx); + return 0; + } + + ctx->next_request = 1; + + WebviCtx ctxhandle = handle_for_context(ctx); + register_context(ctxhandle, ctx); + + return ctxhandle; +} + +void register_context(WebviCtx ctxhandle, WebviContext *ctx) { + GTree *contexts = get_tls_contexts(); + g_tree_insert(contexts, GINT_TO_POINTER(ctxhandle), ctx); +} + +void webvi_context_cleanup(WebviCtx ctxhandle) { + GTree *contexts = get_tls_contexts(); + g_tree_remove(contexts, GINT_TO_POINTER(ctxhandle)); +} + +void webvi_context_set_debug(WebviContext *self, bool d) { + GLogFunc logfunc; + + self->debug = d; + if (self->debug) { + logfunc = webvi_log_handler; + } else { + logfunc = g_log_default_handler; + } + + g_log_set_handler(LIBWEBVI_LOG_DOMAIN, + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG, + logfunc, NULL); +} + +void webvi_log_handler(const gchar *log_domain, GLogLevelFlags log_level, + const gchar *message, gpointer user_data) +{ + g_fprintf(stderr, "%s: %s\n", log_domain, message); +} + +void webvi_context_set_template_path(WebviContext *self, const char *path) { + if (self->link_templates) { + link_templates_delete(self->link_templates); + self->link_templates = NULL; + } + if (self->template_path) { + g_free(self->template_path); + } + self->template_path = path ? g_strdup(path) : NULL; +} + +const char *webvi_context_get_template_path(const WebviContext *self) { + return self->template_path ? self->template_path : DEFAULT_TEMPLATE_PATH; +} + +const LinkTemplates *get_link_templates(WebviContext *self) { + if (!self->link_templates) { + self->link_templates = link_templates_create(); + if (self->link_templates) { + const gchar *path = webvi_context_get_template_path(self); + gchar *template_file = g_strconcat(path, "/links", NULL); + link_templates_load(self->link_templates, template_file); + g_free(template_file); + } + } + + return self->link_templates; +} + +WebviHandle webvi_context_add_request(WebviContext *self, WebviRequest *req) { + int h = self->next_request++; + g_tree_insert(self->requests, GINT_TO_POINTER(h), req); + return (WebviHandle)h; +} + +WebviRequest *webvi_context_get_request(WebviContext *self, WebviHandle h) { + return (WebviRequest *)g_tree_lookup(self->requests, GINT_TO_POINTER(h)); +} + +void webvi_context_remove_request(WebviContext *self, WebviHandle h) { + g_tree_remove(self->requests, GINT_TO_POINTER(h)); +} + +CURLM *webvi_context_get_curl_multi_handle(WebviContext *self) { + if (!self->curl_multi_handle) + self->curl_multi_handle = curl_multi_init(); + return self->curl_multi_handle; +} + +WebviCtx handle_for_context(WebviContext *ctx) { + return (WebviCtx)ctx; // FIXME +} + +WebviContext *get_context_by_handle(WebviCtx handle) { + GTree *contexts = get_tls_contexts(); + return g_tree_lookup(contexts, GINT_TO_POINTER(handle)); +} + +WebviResult webvi_context_fdset(WebviContext *ctx, fd_set *readfds, + fd_set *writefds, fd_set *excfds, int *max_fd) +{ + WebviResult res = WEBVIERR_OK; + *max_fd = -1; + + // curl sockets + CURLM *mhandle = webvi_context_get_curl_multi_handle(ctx); + if (mhandle) { + CURLMcode mcode = curl_multi_fdset(mhandle, readfds, writefds, excfds, max_fd); + res = curlmcode_to_webvierr(mcode); + } + + // non-curl fds + FoundFds fds; + fds.readfds = readfds; + fds.writefds = writefds; + fds.excfds = excfds; + fds.max_fd = max_fd; + g_tree_foreach(ctx->requests, gather_fds, &fds); + + return res; +} + +gboolean gather_fds(gpointer key, gpointer value, gpointer data) { + FoundFds *fds = (FoundFds *)data; + WebviRequest *req = (WebviRequest *)value; + request_fdset(req, fds->readfds, fds->writefds, fds->excfds, fds->max_fd); + return FALSE; +} + +void webvi_context_handle_socket_action( + WebviContext *ctx, int sockfd, int ev_bitmask, long *running_handles) +{ + SocketToHandle x; + x.sockfd = sockfd; + x.ev_bitmask = ev_bitmask; + x.handled = FALSE; + g_tree_foreach(ctx->requests, handle_request_socket, &x); + + int curl_handles = 0; + if (!x.handled) { + // sockfd belongs to curl + CURLM *multi_handle = webvi_context_get_curl_multi_handle(ctx); + if (multi_handle) { + curl_socket_t curl_socket; + int curl_mask = 0; + + if (sockfd == WEBVI_SELECT_TIMEOUT) { + curl_socket = CURL_SOCKET_TIMEOUT; + curl_mask = 0; + } else { + curl_socket = sockfd; + if ((ev_bitmask & WEBVI_SELECT_READ) != 0) + curl_mask |= CURL_CSELECT_IN; + if ((ev_bitmask & WEBVI_SELECT_WRITE) != 0) + curl_mask |= CURL_CSELECT_OUT; + if ((ev_bitmask & WEBVI_SELECT_EXCEPTION) != 0) + curl_mask |= CURL_CSELECT_ERR; + } + + curl_multi_socket_action(multi_handle, curl_socket, curl_mask, &curl_handles); + check_for_finished_curl(multi_handle); + } + } + + // FIXME: running_handles + *running_handles = curl_handles; +} + +gboolean handle_request_socket(gpointer key, gpointer value, gpointer data) { + WebviRequest *req = (WebviRequest *)value; + SocketToHandle *to_handle = (SocketToHandle *)data; + return request_handle_socket(req, to_handle->sockfd, to_handle->ev_bitmask); +} + +void check_for_finished_curl(CURLM *multi_handle) { + int num_messages; + CURLMsg *info; + while ((info = curl_multi_info_read(multi_handle, &num_messages))) { + if (info->msg == CURLMSG_DONE) { + char *instance; + if (curl_easy_getinfo(info->easy_handle, CURLINFO_PRIVATE, &instance) == CURLE_OK) { + PipeComponent *pipe = (PipeComponent *)instance; + pipe_component_finished(pipe, curl_code_to_pipe_state(info->data.result)); + } + } + } +} + +RequestState curl_code_to_pipe_state(CURLcode curlcode) { + switch (curlcode) { + case CURLE_OK: + return WEBVISTATE_FINISHED_OK; + + case CURLE_COULDNT_CONNECT: + case CURLE_TOO_MANY_REDIRECTS: + case CURLE_GOT_NOTHING: + case CURLE_RECV_ERROR: + return WEBVISTATE_NETWORK_READ_ERROR; + + case CURLE_REMOTE_FILE_NOT_FOUND: + return WEBVISTATE_NOT_FOUND; + + case CURLE_OPERATION_TIMEDOUT: + return WEBVISTATE_TIMEDOUT; + + default: + return WEBVISTATE_IO_ERROR; + } +} + +WebviResult curlmcode_to_webvierr(CURLMcode mcode) { + switch (mcode) { + case CURLM_OK: + return WEBVIERR_OK; + + case CURLM_BAD_HANDLE: + case CURLM_BAD_EASY_HANDLE: + return WEBVIERR_INVALID_PARAMETER; + + default: + return WEBVIERR_UNKNOWN_ERROR; + }; +} + +void webvi_context_add_finished_message(WebviContext *ctx, + const WebviRequest *req, + RequestState status_code, + const char *message_text) +{ + WebviHandle h = get_handle_for_request(ctx, req); + if (h != 0) { + GArray *messages = ctx->finish_messages; + WebviMsg msg; + msg.msg = WEBVIMSG_DONE; + msg.handle = h; + msg.status_code = status_code; + msg.data = message_text; + g_array_append_val(messages, msg); + } +} + +WebviHandle get_handle_for_request(WebviContext *ctx, const WebviRequest *req) { + RequestAndHandle query_and_result; + query_and_result.request = req; + query_and_result.handle = 0; + g_tree_foreach(ctx->requests, search_by_request, &query_and_result); + return query_and_result.handle; +} + +gboolean search_by_request(gpointer key, gpointer value, gpointer data) { + WebviHandle h = GPOINTER_TO_INT(key); + WebviRequest *req = value; + RequestAndHandle *query_and_result = data; + + if (query_and_result->request == req) { + query_and_result->handle = h; + return TRUE; /* stop traversal */ + } else { + return FALSE; + } +} + +WebviMsg *webvi_context_next_message(WebviContext *ctx, int *remaining_messages) { + guint len = ctx->finish_messages->len; + if (len > 0) { + if (remaining_messages) + *remaining_messages = (int)len-1; + + ctx->current_message = g_array_index(ctx->finish_messages, WebviMsg, 0); + g_array_remove_index(ctx->finish_messages, 0); + return &ctx->current_message; + } else { + if (remaining_messages) + *remaining_messages = 0; + return NULL; + } +} + +void webvi_context_delete(WebviContext *ctx) { + if (ctx) { + if (ctx->finish_messages) + g_array_free(ctx->finish_messages, TRUE); + if (ctx->curl_multi_handle) + curl_multi_cleanup(ctx->curl_multi_handle); + if (ctx->requests) + g_tree_unref(ctx->requests); + /* if (ctx->downloaders) */ + /* g_ptr_array_unref(ctx->downloaders); */ + if (ctx->link_templates) + link_templates_delete(ctx->link_templates); + g_free(ctx->template_path); + + free(ctx); + } +} + +GTree *get_tls_contexts() { + GTree *contexts = g_private_get(&tls_contexts); + if (!contexts) { + contexts = g_tree_new_full(cmp_int, NULL, NULL, free_context); + g_private_set(&tls_contexts, contexts); + } + + return contexts; +} + +void webvi_context_cleanup_all() { + /* This will cause free_tls_context to be called if tls_context was set */ + g_private_set(&tls_contexts, NULL); +} + +gint cmp_int(gconstpointer a, gconstpointer b, gpointer user_data) { + int aint = GPOINTER_TO_INT(a); + int bint = GPOINTER_TO_INT(b); + return aint - bint; +} + +void free_tls_context(gpointer data) { + if (data) { + g_tree_destroy((GTree *)data); + } +} + +void free_request(gpointer data) { + request_delete((WebviRequest *)data); +} + +void free_context(gpointer data) { + webvi_context_delete((WebviContext *)data); +} diff --git a/src/libwebvi/webvicontext.h b/src/libwebvi/webvicontext.h new file mode 100644 index 0000000..7fa0738 --- /dev/null +++ b/src/libwebvi/webvicontext.h @@ -0,0 +1,63 @@ +/* + * webvicontext.h + * + * Copyright (c) 2013 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 __WEBVICONTEXT_H +#define __WEBVICONTEXT_H + +#include <stdbool.h> +#include <curl/curl.h> +#include <glib.h> +#include "libwebvi.h" +#include "linktemplates.h" +#include "pipecomponent.h" + +typedef struct WebviContext WebviContext; +typedef struct WebviRequest WebviRequest; + +WebviContext *get_context_by_handle(WebviCtx handle); + +WebviCtx webvi_context_initialize(void); +void webvi_context_cleanup(WebviCtx ctxhandle); +void webvi_context_cleanup_all(); + +void webvi_context_set_debug(WebviContext *self, + bool d); +void webvi_context_set_template_path(WebviContext *self, + const char *path); +const char *webvi_context_get_template_path(const WebviContext *self); +const LinkTemplates *get_link_templates(WebviContext *self); +CURLM *webvi_context_get_curl_multi_handle(WebviContext *self); + +WebviHandle webvi_context_add_request(WebviContext *self, WebviRequest *req); +void webvi_context_remove_request(WebviContext *self, WebviHandle h); +WebviRequest *webvi_context_get_request(WebviContext *self, WebviHandle h); + +WebviResult webvi_context_fdset(WebviContext *ctx, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); +void webvi_context_handle_socket_action( + WebviContext *ctx, int sockfd, int ev_bitmask, long *running_handles); + +void webvi_context_add_finished_message(WebviContext *messages, + const WebviRequest *req, + RequestState status_code, + const char *message_text); +WebviMsg *webvi_context_next_message(WebviContext *ctx, + int *remaining_messages); + +#endif // __WEBVICONTEXT_H diff --git a/src/pywebvi/pywebvi.py b/src/pywebvi/pywebvi.py new file mode 100644 index 0000000..b3df416 --- /dev/null +++ b/src/pywebvi/pywebvi.py @@ -0,0 +1,248 @@ +from ctypes import * +import ctypes.util +import weakref + +_WEBVIERR_OK = 0 + +_WEBVI_INVALID_HANDLE = -1 + +_WEBVIOPT_WRITEFUNC = 0 +_WEBVIOPT_READFUNC = 1 +_WEBVIOPT_WRITEDATA = 2 +_WEBVIOPT_READDATA = 3 + +_WEBVIINFO_URL = 0 +_WEBVIINFO_CONTENT_LENGTH = 1 +_WEBVIINFO_CONTENT_TYPE = 2 +_WEBVIINFO_STREAM_TITLE = 3 + +_WEBVI_CONFIG_TEMPLATE_PATH = 0 +_WEBVI_CONFIG_DEBUG = 1 +_WEBVI_CONFIG_TIMEOUT_CALLBACK = 2 +_WEBVI_CONFIG_TIMEOUT_DATA = 3 + +class WebviState: + NOT_FINISHED = 0 + FINISHED_OK = 1 + NOT_FOUND = 2 + NETWORK_READ_ERROR = 3 + IO_ERROR = 4 + TIMEDOUT = 5 + INTERNAL_ERROR = 999 + +class WebviMsg(Structure): + _fields_ = [("msg", c_int), + ("handle", c_int), + ("status_code", c_int), + ("data", c_char_p)] + +class WeakRequestRef(weakref.ref): + pass + +class WebviError(Exception): + pass + +def raise_if_webvi_result_not_ok(value): + if value != _WEBVIERR_OK: + raise WebviError('libwebvi function returned error code %d: %s' % + (value, strerror(value))) + return value + +def raise_if_request_not_ok(value): + if value == -1: + raise WebviError('libwebvi request initialization failed') + return value + +_libc = CDLL(ctypes.util.find_library("c")) +_libc.free.argtypes = [c_void_p] +_libc.free.restype = None + +libwebvi = CDLL("libwebvi.so") +libwebvi.webvi_global_init() + +libwebvi.webvi_initialize_context.argtypes = [] +libwebvi.webvi_initialize_context.restype = c_long +libwebvi.webvi_version.argtypes = [] +libwebvi.webvi_version.restype = c_char_p +libwebvi.webvi_strerror.argtypes = [c_int] +libwebvi.webvi_strerror.restype = c_char_p +libwebvi.webvi_new_request.argtypes = [c_long, c_char_p] +libwebvi.webvi_new_request.restype = raise_if_request_not_ok +libwebvi.webvi_delete_request.argtypes = [c_long, c_int] +libwebvi.webvi_delete_request.restype = raise_if_webvi_result_not_ok +libwebvi.webvi_process_some.argtypes = [c_long, c_int] +libwebvi.webvi_process_some.restype = c_int +libwebvi.webvi_get_message.argtypes = [c_long, POINTER(c_int)] +libwebvi.webvi_get_message.restype = POINTER(WebviMsg) +libwebvi.webvi_start_request.argtypes = [c_long, c_int] +libwebvi.webvi_start_request.restype = raise_if_webvi_result_not_ok +libwebvi.webvi_stop_request.argtypes = [c_long, c_int] +libwebvi.webvi_stop_request.restype = raise_if_webvi_result_not_ok + +WEBVICALLBACK = CFUNCTYPE(c_ssize_t, c_char_p, c_size_t, c_void_p) +TIMEOUTFUNC = CFUNCTYPE(None, c_long, c_void_p) + +def version(): + return string_at(libwebvi.webvi_version()) + +def strerror(err): + return string_at(libwebvi.webvi_strerror(err)) + +class WebviContext: + def __init__(self): + self.handle = libwebvi.webvi_initialize_context() + self._requests = {} + self.timeout_callback = None + + def __del__(self): + libwebvi.webvi_cleanup_context(self.handle) + self.handle = None + + def set_template_path(self, path): + if self.handle is None: + return + + set_config = libwebvi.webvi_set_config + set_config.argtypes = [c_long, c_int, c_char_p] + set_config.restype = raise_if_webvi_result_not_ok + set_config(self.handle, _WEBVI_CONFIG_TEMPLATE_PATH, path) + + def set_debug(self, enabled): + if self.handle is None: + return + + set_config = libwebvi.webvi_set_config + set_config.argtypes = [c_long, c_int, c_char_p] + set_config.restype = raise_if_webvi_result_not_ok + if enabled: + debug = "1" + else: + debug = "0" + set_config(self.handle, _WEBVI_CONFIG_DEBUG, debug) + + def set_timeout_callback(self, cb): + def callback_wrapper(timeout, userdata): + return cb(timeout) + + set_config = libwebvi.webvi_set_config + set_config.argtypes = [c_long, c_int, TIMEOUTFUNC] + set_config.restype = raise_if_webvi_result_not_ok + self.timeout_callback = TIMEOUTFUNC(callback_wrapper) + set_config(self.handle, _WEBVI_CONFIG_TIMEOUT_CALLBACK, + self.timeout_callback) + + def process_some(self, timeout_seconds=0): + return libwebvi.webvi_process_some(self.handle, int(timeout_seconds)) + + def get_finished_request(self): + remaining = c_int(0) + msg_ptr = libwebvi.webvi_get_message(self.handle, byref(remaining)) + + if not msg_ptr or msg_ptr.contents.msg != 0: + return None + + reqhandle = msg_ptr.contents.handle + if reqhandle not in self._requests: + return None + + request = self._requests[reqhandle]() + if request is None: + return None + + return (request, + msg_ptr.contents.status_code, + string_at(msg_ptr.contents.data)) + + def register_request(self, request): + def unregister_request(ref): + del self._requests[ref.handle] + libwebvi.webvi_delete_request(self.handle, ref.handle) + + ref = WeakRequestRef(request, unregister_request) + ref.handle = request.handle + self._requests[request.handle] = ref + + +class WebviRequest: + def __init__(self, context, href): + self.context = context + self.read_callback = None + self.handle = libwebvi.webvi_new_request(context.handle, href) + if self.handle == _WEBVI_INVALID_HANDLE: + raise WebviError('Initializing request failed') + self.context.register_request(self) + + def start(self): + libwebvi.webvi_start_request(self.context.handle, self.handle) + + def stop(self): + libwebvi.webvi_stop_request(self.context.handle, self.handle) + + def set_read_callback(self, cb): + def callback_wrapper(buf, length, userdata): + return cb(string_at(buf, length)) + + set_opt = libwebvi.webvi_set_opt + set_opt.argstypes = [c_long, c_int, c_int, WEBVICALLBACK] + set_opt.restype = raise_if_webvi_result_not_ok + # Must keep a reference to the callback! + self.read_callback = WEBVICALLBACK(callback_wrapper) + set_opt(self.context.handle, self.handle, + _WEBVIOPT_READFUNC, self.read_callback) + + + # def set_write_callback(self, cb): + # def callback_wrapper(buf, length, userdata): + # return cb(string_at(buf, length)) + + # set_opt = libwebvi.webvi_set_opt + # set_opt.argstypes = [c_long, c_int, c_int, WEBVICALLBACK] + # set_opt.restype = raise_if_webvi_result_not_ok + # set_opt(self.context.handle, self.handle, _WEBVIOPT_WRITEFUNC, + # WEBVICALLBACK(callback_wrapper)) + + def get_url(self): + get_info = libwebvi.webvi_get_info + get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)] + get_info.restype = raise_if_webvi_result_not_ok + + output = c_char_p() + get_info(self.context.handle, self.handle, + _WEBVIINFO_URL, pointer(output)) + url = string_at(output) + _libc.free(output) + return url + + def get_content_length(self): + get_info = libwebvi.webvi_get_info + get_info.argtypes = [c_long, c_int, c_int, POINTER(c_long)] + get_info.restype = raise_if_webvi_result_not_ok + + output = c_long(0) + get_info(self.context.handle, self.handle, + _WEBVIINFO_CONTENT_LENGTH, pointer(output)) + return output.value + + def get_content_type(self): + get_info = libwebvi.webvi_get_info + get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)] + get_info.restype = raise_if_webvi_result_not_ok + + output = c_char_p() + get_info(self.context.handle, self.handle, + _WEBVIINFO_CONTENT_TYPE, pointer(output)) + content_type = string_at(output) + _libc.free(output) + return content_type + + def get_stream_title(self): + get_info = libwebvi.webvi_get_info + get_info.argtypes = [c_long, c_int, c_int, POINTER(c_char_p)] + get_info.restype = raise_if_webvi_result_not_ok + + output = c_char_p() + get_info(self.context.handle, self.handle, + _WEBVIINFO_STREAM_TITLE, pointer(output)) + title = string_at(output) + _libc.free(output) + return title diff --git a/src/version.h.in b/src/version.h.in new file mode 100644 index 0000000..5b7da4e --- /dev/null +++ b/src/version.h.in @@ -0,0 +1 @@ +#define LIBWEBVI_VERSION "@MAJOR_VERSION@.@MINOR_VERSION@.@PATCH_VERSION@" diff --git a/src/webvicli/webvi b/src/webvicli/webvi new file mode 100755 index 0000000..cb98b5e --- /dev/null +++ b/src/webvicli/webvi @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# webvi - starter script for webvicli +# +# Copyright (c) 2010-2013 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..5de29c9 --- /dev/null +++ b/src/webvicli/webvicli/client.py @@ -0,0 +1,825 @@ +#!/usr/bin/env python + +# client.py - webvi command line client +# +# Copyright (c) 2009-2013 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 cmd +import mimetypes +import select +import os.path +import subprocess +import time +import re +import datetime +import urllib +import shlex +import shutil +import tempfile +import libxml2 +from pywebvi import WebviContext, WebviRequest, WebviState +from optparse import OptionParser +from ConfigParser import RawConfigParser +from urlparse import urlparse +from StringIO import StringIO +from . import menu + +VERSION = '0.5.0' + +# Default options +DEFAULT_PLAYERS = ['mplayer -cache-min 10 "%s"', + 'vlc --play-and-exit --file-caching 5000 "%s"', + 'totem "%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') +mimetypes.add_type('video/webm', '.webm') + +def safe_filename(name, vfat): + """Sanitize a filename. If vfat is False, replace '/' with '_', if + vfat is True, replace also other characters that are illegal on + VFAT. Remove dots from the beginning of the filename.""" + if vfat: + excludechars = r'[\\"*/:<>?|]' + else: + excludechars = r'[/]' + + res = re.sub(excludechars, '_', name) + res = res.lstrip('.') + res = res.encode(sys.getfilesystemencoding(), 'ignore') + + return res + +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 guess_video_extension(mimetype, url): + """Return extension for a video at url with a given mimetype. + + This assumes that the target is a video stream and therefore ignores + mimetype if it is text/plain, which some incorrectly configured servers + return as the mimetype. + """ + ext = mimetypes.guess_extension(mimetype) + if (ext is None) or (mimetype == 'text/plain'): + lastcomponent = re.split(r'[?#]', url, 1)[0].split('/')[-1] + i = lastcomponent.rfind('.') + if i == -1: + ext = '' + else: + ext = lastcomponent[i:] + return ext + +def dl_progress(count, blockSize, totalSize): + if totalSize == -1: + return + percent = int(count*blockSize*100/totalSize) + sys.stdout.write("\r%d% %" % percent) + sys.stdout.flush() + +def next_available_file_name(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) + +class StringIOCallback(StringIO): + def write_and_return_length(self, buf): + self.write(buf) + return len(buf) + + +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, vfatfilenames): + self.streamplayers = streamplayers + self.history = [] + self.history_pointer = 0 + self.quality_limits = {'download': downloadlimits, + 'stream': streamlimits} + self.vfatfilenames = vfatfilenames + self.alarm = None + self.webvi = WebviContext() + self.webvi.set_timeout_callback(self.update_timeout) + + def set_debug(self, enabled): + self.webvi.set_debug(enabled) + + def set_template_path(self, path): + self.webvi.set_template_path(path) + + def update_timeout(self, timeout_ms, data): + if timeout_ms < 0: + self.alarm = None + else: + now = datetime.datetime.now() + self.alarm = now + datetime.timedelta(milliseconds=timeout_ms) + + 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 = get_content_unicode(node) + elif node.name == 'ul': + li_node = node.children + while li_node: + if li_node.name == 'li': + menuitem = self.parse_link(li_node) + menupage.add(menuitem) + li_node = li_node.next + + # 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 + is_stream = False + child = node.children + while child: + if child.name == 'a': + label = get_content_unicode(child) + ref = child.prop('href') + is_stream = child.prop('class') != 'webvi' + child = child.next + return menu.MenuItemLink(label, ref, is_stream) + + def parse_textfield(self, node): + label = '' + name = node.prop('name') + child = node.children + while child: + if child.name == 'label': + label = 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 = 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 = get_content_unicode(child) + elif child.name == 'item': + items.append(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 + encoding = 'utf-8' + child = node.children + while child: + if child.name == 'label': + label = get_content_unicode(child) + elif child.name == 'submission': + submission = get_content_unicode(child) + enc = child.hasProp('encoding') + if enc is not None: + encoding = get_content_unicode(enc) + child = child.next + return menu.MenuItemSubmitButton(label, submission, queryitems, encoding) + + def execute_webvi(self, request): + """Call self.webvi.process_some until request is finished.""" + while True: + if self.alarm is None: + timeout = 10 + else: + delta = self.alarm - datetime.datetime.now() + if delta < datetime.timedelta(0): + timeout = 10 + self.alarm = None + else: + timeout = delta.microseconds/1000000.0 + delta.seconds + + self.webvi.process_some(timeout) + finished = self.webvi.get_finished_request() + if finished is not None and finished[0] == request: + return (finished[1], finished[2]) + + def getmenu(self, ref): + dlbuffer = StringIOCallback() + request = WebviRequest(self.webvi, ref) + request.set_read_callback(dlbuffer.write_and_return_length) + request.start() + status, err = self.execute_webvi(request) + del request + + if status != WebviState.FINISHED_OK: + 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): + streamurl, streamtitle = self.get_stream_url_and_title(stream) + if streamurl is None: + return True + + self.download_stream(streamurl, streamtitle) + return True + + def get_stream_url_and_title(self, stream): + dlbuffer = StringIOCallback() + request = WebviRequest(self.webvi, stream) + request.set_read_callback(dlbuffer.write_and_return_length) + request.start() + status, err = self.execute_webvi(request) + del request + + if status != WebviState.FINISHED_OK: + print 'Download failed:', err + return (None, None) + + menu = self.parse_page(dlbuffer.getvalue()) + if menu is None or len(menu) == 0: + print 'Failed to parse menu' + return (None, None) + + return (menu[0].activate(), menu[0].label) + + def download_stream(self, url, title): + try: + (tmpfilename, headers) = \ + urllib.urlretrieve(url, reporthook=dl_progress) + print + except urllib.ContentTooShortError, exc: + print 'Got too few bytes, connection may have been interrupted' + headers = {} + tmpfile, tmpfilename = tempfile.mkstemp() + tmpfile.write(exc.content) + tmpfile.close() + + # rename the tempfile to final name + contenttype = headers.get('Content-Type', 'video') + ext = guess_video_extension(contenttype, url) + safename = safe_filename(title, self.vfatfilenames) + destfilename = next_available_file_name(safename, ext) + shutil.move(tmpfilename, destfilename) + print 'Saved to %s' % destfilename + + def play_stream(self, ref): + streamurl = self.get_stream_url(ref) + if streamurl == '': + print 'Did not find URL' + return False + + if streamurl.startswith('wvt://'): + print 'Streaming not supported, try downloading' + return False + + # Found url, now find a working media player + for player in self.streamplayers: + if '%s' not in player: + player = player + ' %s' + + playcmd = shlex.split(player) + + # Hack for playing from fifo in VLC + if 'vlc' in playcmd[0] and streamurl.startswith('file://'): + realurl = 'stream://' + streamurl[len('file://'):] + else: + realurl = streamurl + + try: + playcmd[playcmd.index('%s')] = realurl + except ValueError: + print 'Can\'t substitute URL in', player + continue + + try: + print 'Trying player: ' + ' '.join(playcmd) + retcode = subprocess.call(playcmd) + if retcode > 0: + print 'Player failed with returncode', retcode + # else: + # # After the player has finished, the library + # # generates a read event on a control socket. When + # # the client calls perform on the socket the + # # library removes temporary files. + # readfds, writefds = webvi.api.fdset()[1:3] + # readyread, readywrite, readyexc = \ + # select.select(readfds, writefds, [], 0.1) + # for fd in readyread: + # webvi.api.perform(fd, WebviSelectBitmask.READ) + # for fd in readywrite: + # webvi.api.perform(fd, WebviSelectBitmask.WRITE) + + 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') + + request = WebviRequest(self.webvi, ref) + + dlbuffer = StringIOCallback() + request.set_read_callback(dlbuffer.write_and_return_length) + request.start() + status, err = self.execute_webvi(request) + del request + + if status != WebviState.FINISHED_OK: + print 'Download failed:', err + return '' + + return dlbuffer.getvalue() + + 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) + menuitem = self._get_numbered_item(int(arg)) + if getattr(menuitem, 'is_stream', False): + return 'download ' + arg + else: + return 'select ' + arg + except ValueError: + return arg + + def onecmd(self, c): + try: + return cmd.Cmd.onecmd(self, c) + except Exception: + import traceback + print 'Exception occurred while handling command "' + c + '"' + print traceback.format_exc() + return False + + def emptyline(self): + pass + + def display_menu(self, menupage): + if menupage is not None: + enc = self.stdout.encoding or 'UTF-8' + self.stdout.write(unicode(menupage).encode(enc, 'replace')) + self.stdout.flush() + + 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) + self.stdout.flush() + 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)) + self.stdout.flush() + else: + menupage = self.client.get_current_menu() + self.display_menu(menupage) + return False + + def do_download(self, arg): + """download x +Download a stream to a file. x can be an integer referring to a +downloadable item (item without brackets) in the current menu or an +URL of a video page. + """ + stream = None + try: + menuitem = self._get_numbered_item(int(arg)) + if menuitem is not None: + stream = menuitem.activate() + except (ValueError, AttributeError): + pass + + if stream is None and arg.find('://') != -1: + stream = arg + + if stream is not None: + self.client.download(stream) + else: + self.stdout.write('Not a stream\n') + self.stdout.flush() + return False + + def do_stream(self, arg): + """stream x +Play a stream. x can be an integer referring to a downloadable item +(item without brackets) in the current menu or an URL of a video page. + """ + stream = None + try: + menuitem = self._get_numbered_item(int(arg)) + if menuitem is not None: + stream = menuitem.activate() + except (ValueError, AttributeError): + pass + + if stream is None and arg.find('://') != -1: + stream = arg + + if stream is not None: + self.client.play_stream(stream) + else: + self.stdout.write('Not a stream\n') + self.stdout.flush() + 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) + self.stdout.flush() + return False + + def do_menu(self, arg): + """Get back to the main menu.""" + status, statusmsg, menupage = self.client.getmenu('wvt://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)) + self.stdout.flush() + 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'): + if opt in ['vfat', 'verbose']: + try: + options[opt] = cfgprs.getboolean(sec, opt) + except ValueError: + print 'Invalid config: %s = %s' % (opt, val) + + # convert verbose to integer + if opt == 'verbose': + if options['verbose']: + options['verbose'] = 1 + else: + options['verbose'] = 0 + + else: + options[opt] = val + + else: + sitename = urlparse(sec).netloc + if sitename == '': + sitename = sec + + 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) + parser.add_option('-v', '--verbose', action='store_const', const=1, + dest='verbose', help='debug output', default=0) + parser.add_option('--vfat', action='store_true', + dest='vfat', default=False, + help='generate Windows compatible filenames') + parser.add_option('-u', '--url', type='string', + dest='url', + help='Download video from URL and exit', + metavar='URL', default=None) + cmdlineopt = parser.parse_args(cmdlineargs)[0] + + if cmdlineopt.templatepath is not None: + options['templatepath'] = cmdlineopt.templatepath + if cmdlineopt.verbose > 0: + options['verbose'] = cmdlineopt.verbose + if cmdlineopt.vfat: + options['vfat'] = cmdlineopt.vfat + if cmdlineopt.url: + options['url'] = cmdlineopt.url + + 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) + + client = WVClient(player_list(options), + options.get('download-limits', {}), + options.get('stream-limits', {}), + options.get('vfat', False)) + + if options.has_key('verbose'): + client.set_debug(options['verbose']) + if options.has_key('templatepath'): + client.set_template_path(options['templatepath']) + + if options.has_key('url'): + stream = options['url'] + if not client.download(stream): + # FIXME: more helpful error message if URL is not a + # supported site + sys.exit(1) + + sys.exit(0) + + shell = WVShell(client) + 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..509ba2c --- /dev/null +++ b/src/webvicli/webvicli/menu.py @@ -0,0 +1,177 @@ +# menu.py - menu elements for webvicli +# +# Copyright (c) 2009-2012 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, is_stream): + self.label = label + if type(ref) == unicode: + self.ref = ref.encode('utf-8') + else: + self.ref = ref + self.is_stream = is_stream + + def __str__(self): + res = self.label + if not self.is_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 or 'utf-8') + 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, encoding): + self.label = label + if type(baseurl) == unicode: + self.baseurl = baseurl.encode('utf-8') + else: + self.baseurl = baseurl + self.subitems = subitems + self.encoding = encoding + + 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(): + try: + parts.append('subst=%s,%s' % \ + (urllib.quote(key.encode(self.encoding, 'ignore')), + urllib.quote(val.encode(self.encoding, 'ignore')))) + except LookupError: + pass + + return baseurl + '&'.join(parts) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..6166f6f --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,44 @@ +ENABLE_TESTING() + +SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") + +FIND_PROGRAM(GTESTER_BIN gtester) +IF(NOT GTESTER_BIN) + MESSAGE(FATAL_ERROR "Binary 'gtester' is required for testing!") +ENDIF() + +INCLUDE_DIRECTORIES(. ../src/libwebvi) + +FIND_PACKAGE(LibXml2 REQUIRED) +FIND_PACKAGE(CURL REQUIRED) +FIND_PACKAGE(LibTidy REQUIRED) + +FIND_PACKAGE(PkgConfig) +PKG_CHECK_MODULES(GLIB REQUIRED glib-2.0) +ADD_DEFINITIONS(${GLIB_CFLAGS} ${GLIB_CFLAGS_OTHER}) +LINK_DIRECTORIES(${GLIB_LIBRARY_DIRS}) + +INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR}) +INCLUDE_DIRECTORIES(${GLIB_INCLUDE_DIRS}) + +ADD_DEFINITIONS( -DTEST_DATA_DIR="${CMAKE_SOURCE_DIR}/tests/data") + +ADD_EXECUTABLE(libwebvi_tests + libwebvi_tests.c + context_tests.c + linktemplates_tests.c + linkextractor_tests.c + menubuilder_tests.c + pipe_tests.c + urlutils_tests.c) +TARGET_LINK_LIBRARIES(libwebvi_tests + webvistatic + ${GLIB_LIBRARIES} + ${LIBXML2_LIBRARIES} + ${CURL_LIBRARIES} + ${LIBTIDY_LIBRARIES}) + +ADD_TEST(unittests ${GTESTER_BIN} -k -o testresults.xml ${CMAKE_CURRENT_BINARY_DIR}/libwebvi_tests) +SET_TESTS_PROPERTIES(unittests PROPERTIES PASS_REGULAR_EXPRESSION "PASS:") +SET_TESTS_PROPERTIES(unittests PROPERTIES FAIL_REGULAR_EXPRESSION "ERROR:") + diff --git a/tests/context_tests.c b/tests/context_tests.c new file mode 100644 index 0000000..43bf572 --- /dev/null +++ b/tests/context_tests.c @@ -0,0 +1,54 @@ +#include "context_tests.h" +#include "request.h" + +void context_fixture_setup(ContextFixture *fixture, + gconstpointer test_data) +{ + fixture->handle = webvi_context_initialize(); + g_assert(fixture->handle != 0); + fixture->context = get_context_by_handle(fixture->handle); + g_assert(fixture->context != NULL); +} + +void context_fixture_teardown(ContextFixture *fixture, + gconstpointer test_data) +{ + g_assert(fixture->handle != 0); + g_assert(fixture->context != NULL); + webvi_context_cleanup(fixture->handle); + fixture->handle = 0; + fixture->context = NULL; +} + +void test_context_create(void) { + WebviCtx ctx = webvi_initialize_context(); + g_assert(ctx != 0); + WebviCtx ctx2 = webvi_initialize_context(); + g_assert(ctx != ctx2); + + webvi_context_cleanup(ctx2); + webvi_context_cleanup(ctx); +} + +void test_context_template_path(ContextFixture *fixture, + gconstpointer test_data) { + const char *tpath = "testpath"; + webvi_context_set_template_path(fixture->context, tpath); + const char *output_path = webvi_context_get_template_path(fixture->context); + g_assert_cmpstr(output_path, ==, tpath); +} + +void test_context_request_processing(ContextFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + WebviRequest *mock_request = request_create("testuri", fixture->context); + + WebviHandle h = webvi_context_add_request(fixture->context, mock_request); + g_assert(h != WEBVI_INVALID_HANDLE); + WebviRequest *req = webvi_context_get_request(fixture->context, h); + g_assert(req == mock_request); + + webvi_context_remove_request(fixture->context, h); + + req = webvi_context_get_request(fixture->context, h); + g_assert(req == NULL); +} diff --git a/tests/context_tests.h b/tests/context_tests.h new file mode 100644 index 0000000..1a6907c --- /dev/null +++ b/tests/context_tests.h @@ -0,0 +1,23 @@ +#ifndef CONTEXT_TESTS_H +#define CONTEXT_TESTS_H + +#include <glib.h> +#include "webvicontext.h" + +typedef struct { + WebviCtx handle; + struct WebviContext *context; +} ContextFixture; + +void context_fixture_setup(ContextFixture *fixture, + gconstpointer test_data); +void context_fixture_teardown(ContextFixture *fixture, + gconstpointer test_data); + +void test_context_create(void); +void test_context_template_path(ContextFixture *fixture, + gconstpointer test_data); +void test_context_request_processing(ContextFixture *fixture, + gconstpointer test_data); + +#endif // CONTEXT_TESTS_H diff --git a/tests/data/links b/tests/data/links new file mode 100644 index 0000000..0482470 --- /dev/null +++ b/tests/data/links @@ -0,0 +1,5 @@ +### Link configuration file for tests ### +# Regular link +http://example\.com/test/ +# Video link +http://example.com/video/ bin:test_command diff --git a/tests/data/websites/www.youtube.com/menu.xml b/tests/data/websites/www.youtube.com/menu.xml new file mode 100644 index 0000000..15f1a0e --- /dev/null +++ b/tests/data/websites/www.youtube.com/menu.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<wvmenu> + <title>YouTube</title> + <description>Video sharing service on which users worldwide can upload their videos</description> + + <link> + <label>Search</label> + <ref>wvt:///www.youtube.com/search.xml</ref> + </link> + + <link> + <label>Categories</label> + <ref>http://www.youtube.com/channels?feature=guide</ref> + </link> +</wvmenu> diff --git a/tests/libwebvi_tests.c b/tests/libwebvi_tests.c new file mode 100644 index 0000000..f754157 --- /dev/null +++ b/tests/libwebvi_tests.c @@ -0,0 +1,106 @@ +#include <glib.h> +#include "context_tests.h" +#include "linktemplates_tests.h" +#include "linkextractor_tests.h" +#include "menubuilder_tests.h" +#include "pipe_tests.h" +#include "urlutils_tests.h" + +int main(int argc, char** argv) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/context/create", test_context_create); + g_test_add("/context/templatepath", ContextFixture, 0, context_fixture_setup, + test_context_template_path, context_fixture_teardown); + g_test_add("/context/request", ContextFixture, 0, context_fixture_setup, + test_context_request_processing, context_fixture_teardown); + + g_test_add("/linktemplates/load", LinkTemplatesFixture, 0, + link_templates_fixture_setup, test_link_templates_load, + link_templates_fixture_teardown); + g_test_add("/linktemplates/get", LinkTemplatesFixture, 0, + link_templates_fixture_setup, test_link_templates_get, + link_templates_fixture_teardown); + + g_test_add("/linkextractor/extract", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_extract, + link_extractor_fixture_teardown); + g_test_add("/linkextractor/unrecognized", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_unrecognized_link, + link_extractor_fixture_teardown); + g_test_add("/linkextractor/append", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_append, + link_extractor_fixture_teardown); + g_test_add("/linkextractor/invalidHtml", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_invalid_html, + link_extractor_fixture_teardown); + g_test_add("/linkextractor/relativeURL", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_relative_urls, + link_extractor_fixture_teardown); + g_test_add("/linkextractor/html_title", LinkExtractorFixture, 0, + link_extractor_fixture_setup, test_link_extractor_html_title, + link_extractor_fixture_teardown); + + g_test_add_func("/menubuilder/mainmenu", test_mainmenu); + g_test_add("/menubuilder/title", MenuBuilderFixture, 0, + menu_builder_fixture_setup, test_menu_builder_title, + menu_builder_fixture_teardown); + g_test_add("/menubuilder/links", MenuBuilderFixture, 0, + menu_builder_fixture_setup, test_menu_builder_append_links, + menu_builder_fixture_teardown); + g_test_add("/menubuilder/encoding", MenuBuilderFixture, 0, + menu_builder_fixture_setup, test_menu_builder_link_title_encoding, + menu_builder_fixture_teardown); + + g_test_add_func("/pipe/one_component", test_pipe_one_component); + g_test_add_func("/pipe/two_components", test_pipe_two_components); + g_test_add_func("/pipe/failing_component", test_pipe_failing_component); + g_test_add_func("/pipe/append_after_finish", + test_pipe_not_appending_after_finished); + g_test_add_func("/pipe/state_change_after_finish", + test_pipe_state_not_chaning_after_finished); + g_test_add_func("/pipe/fdset", test_pipe_fdset); + g_test_add_func("/pipe/delete_all", test_pipe_delete_all); + + g_test_add_func("/urlutils/scheme", test_url_scheme); + g_test_add_func("/urlutils/scheme_no_scheme", test_url_scheme_no_scheme); + g_test_add_func("/urlutils/scheme_double", test_url_scheme_double_scheme); + g_test_add_func("/urlutils/scheme_invalid_characters", + test_url_scheme_invalid_characters); + g_test_add_func("/urlutils/root", test_url_root); + g_test_add_func("/urlutils/root_path", test_url_root_full_path); + g_test_add_func("/urlutils/root_query", test_url_root_terminated_by_query); + g_test_add_func("/urlutils/path", test_url_path); + g_test_add_func("/urlutils/path_slash", test_url_path_ends_in_slash); + g_test_add_func("/urlutils/path_query", test_url_path_query); + g_test_add_func("/urlutils/path_fragment", test_url_path_fragment); + g_test_add_func("/urlutils/path_no_path", test_url_path_no_path); + g_test_add_func("/urlutils/path_no_server", test_url_path_no_server); + g_test_add_func("/urlutils/path_no_scheme", test_url_path_no_scheme); + g_test_add_func("/urlutils/dirname", test_url_path_dirname); + g_test_add_func("/urlutils/dirname_no_file", test_url_path_dirname_no_file); + g_test_add_func("/urlutils/dirname_query", test_url_path_dirname_query); + g_test_add_func("/urlutils/dirname_no_server", + test_url_path_dirname_no_server); + g_test_add_func("/urlutils/query", test_url_path_and_query); + g_test_add_func("/urlutils/query_no_query", + test_url_path_and_query_no_query); + g_test_add_func("/urlutils/query_double_query", + test_url_path_and_query_double_query); + g_test_add_func("/urlutils/query_fragment", test_url_path_and_query_fragment); + g_test_add_func("/urlutils/query_no_path", test_url_path_and_query_no_path); + g_test_add_func("/urlutils/rel2abs", test_url_rel2abs_file); + g_test_add_func("/urlutils/rel2abs_root", test_url_rel2abs_root); + g_test_add_func("/urlutils/rel2abs_query", test_url_rel2abs_query); + g_test_add_func("/urlutils/rel2abs_double_query", + test_url_rel2abs_double_query); + g_test_add_func("/urlutils/rel2abs_append_query", + test_url_rel2abs_append_query); + g_test_add_func("/urlutils/rel2abs_fragment", test_url_rel2abs_fragment); + g_test_add_func("/urlutils/rel2abs_append_fragment", + test_url_rel2abs_append_fragment); + g_test_add_func("/urlutils/rel2abs_scheme", test_url_rel2abs_scheme); + + return g_test_run(); +} diff --git a/tests/linkextractor_tests.c b/tests/linkextractor_tests.c new file mode 100644 index 0000000..2aa9166 --- /dev/null +++ b/tests/linkextractor_tests.c @@ -0,0 +1,158 @@ +#include <string.h> +#include "linkextractor_tests.h" + +#define LINK_TEMPLATE_PATH TEST_DATA_DIR "/links" + +#define BASEURL "http://example.com/index.html" + +#define HTML1_HREF "http://example.com/test/link" +#define HTML1_TITLE "Test link" +#define HTML1 "<html><body>" \ + "<a href=\"" HTML1_HREF "\">" HTML1_TITLE "</a>" \ + "</body></html>" + +#define HTML2_HREF "http://example.com/test/link" +#define HTML2_TITLE "Test link" +#define HTML2 "<html><body>" \ + "<a href=\"" HTML2_HREF "\">" HTML2_TITLE "</a>" \ + "<a href=\"http://example.com/index.html\">This is an unrecognized link</a>" \ + "</body></html>" + +#define HTML3_HEADER "<html><body>" +#define HTML3_HREF "http://example.com/test/link" +#define HTML3_TITLE "Test link" +#define HTML3_BODY "<a href=\"" HTML3_HREF "\">" HTML3_TITLE "</a>" +#define HTML3_FOOTER "</body></html>" +#define HTML_INVALID "<a hrefxxx=0</a>" + +#define HTML4_HREF "/test/link" +#define HTML4_HREF_ABSOLUTE "http://example.com" HTML4_HREF +#define HTML4_TITLE "Relative link" +#define HTML4 "<html><body>" \ + "<a href=\"" HTML4_HREF "\">" HTML4_TITLE "</a>" \ + "</body></html>" + +#define HTML5_HREF "http://example.com/test/link" +#define HTML5_TITLE "Test link" +#define HTML5 "<html><body>" \ + "<a href=\"" HTML5_HREF "\"><span><b> Test</b></span> <span>link</span></a>" \ + "</body></html>" + + +void link_extractor_fixture_setup(LinkExtractorFixture *fixture, + gconstpointer test_data) { + fixture->templates = link_templates_create(); + g_assert(fixture->templates != NULL); + link_templates_load(fixture->templates, LINK_TEMPLATE_PATH); + + fixture->extractor = link_extractor_create(fixture->templates, BASEURL); + g_assert(fixture->extractor != NULL); +} + +void link_extractor_fixture_teardown(LinkExtractorFixture *fixture, + gconstpointer test_data) { + link_extractor_delete(fixture->extractor); + link_templates_delete(fixture->templates); +} + +void test_link_extractor_extract(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML1, strlen(HTML1)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 1); + const struct Link *link = g_ptr_array_index(links, 0); + const char *href = link_get_href(link); + g_assert(href); + g_assert(strcmp(href, HTML1_HREF) == 0); + const char *title = link_get_title(link); + g_assert(title); + g_assert(strcmp(title, HTML1_TITLE) == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_unrecognized_link(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML2, strlen(HTML2)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 1); + const struct Link *link = g_ptr_array_index(links, 0); + const char *href = link_get_href(link); + g_assert(href); + g_assert(strcmp(href, HTML2_HREF) == 0); + const char *title = link_get_title(link); + g_assert(title); + g_assert(strcmp(title, HTML2_TITLE) == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_append(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML3_HEADER, strlen(HTML3_HEADER)); + link_extractor_append(fixture->extractor, HTML3_BODY, strlen(HTML3_BODY)); + link_extractor_append(fixture->extractor, HTML3_FOOTER, strlen(HTML3_FOOTER)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 1); + const struct Link *link = g_ptr_array_index(links, 0); + const char *href = link_get_href(link); + g_assert(href); + g_assert(strcmp(href, HTML3_HREF) == 0); + const char *title = link_get_title(link); + g_assert(title); + g_assert(strcmp(title, HTML3_TITLE) == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_invalid_html(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML_INVALID, strlen(HTML_INVALID)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_relative_urls(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML4, strlen(HTML4)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 1); + const struct Link *link = g_ptr_array_index(links, 0); + const char *href = link_get_href(link); + g_assert(href); + g_assert(strcmp(href, HTML4_HREF_ABSOLUTE) == 0); + const char *title = link_get_title(link); + g_assert(title); + g_assert(strcmp(title, HTML4_TITLE) == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_html_title(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links; + link_extractor_append(fixture->extractor, HTML5, strlen(HTML5)); + links = link_extractor_get_links(fixture->extractor); + g_assert(links); + g_assert(links->len == 1); + const struct Link *link = g_ptr_array_index(links, 0); + const char *href = link_get_href(link); + g_assert(href); + g_assert(strcmp(href, HTML5_HREF) == 0); + const char *title = link_get_title(link); + g_assert(title); + g_assert(strcmp(title, HTML5_TITLE) == 0); + g_ptr_array_free(links, TRUE); +} + +void test_link_extractor_xml(LinkExtractorFixture *fixture, + gconstpointer test_data) { + +} diff --git a/tests/linkextractor_tests.h b/tests/linkextractor_tests.h new file mode 100644 index 0000000..49d2c7f --- /dev/null +++ b/tests/linkextractor_tests.h @@ -0,0 +1,34 @@ +#ifndef LINK_EXTRACTOR_TESTS_H +#define LINK_EXTRACTOR_TESTS_H + +#include "linkextractor.h" +#include "linktemplates_tests.h" + +typedef struct { + struct LinkTemplates *templates; + struct LinkExtractor *extractor; +} LinkExtractorFixture; + +void link_extractor_fixture_setup(LinkExtractorFixture *fixture, + gconstpointer test_data); +void link_extractor_fixture_teardown(LinkExtractorFixture *fixture, + gconstpointer test_data); + +void test_link_extractor_extract(LinkExtractorFixture *fixture, + gconstpointer test_data); +void test_link_extractor_unrecognized_link(LinkExtractorFixture *fixture, + gconstpointer test_data); +void test_link_extractor_append(LinkExtractorFixture *fixture, + gconstpointer test_data); +void test_link_extractor_invalid_html(LinkExtractorFixture *fixture, + gconstpointer test_data); +void test_link_extractor_relative_urls(LinkExtractorFixture *fixture, + gconstpointer test_data); +void test_link_extractor_html_title(LinkExtractorFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data); + +void test_link_extractor_xml(LinkExtractorFixture *fixture, + gconstpointer test_data); + + +#endif // LINK_EXTRACTOR_TESTS_H diff --git a/tests/linktemplates_tests.c b/tests/linktemplates_tests.c new file mode 100644 index 0000000..bc15e15 --- /dev/null +++ b/tests/linktemplates_tests.c @@ -0,0 +1,46 @@ +#include "linktemplates_tests.h" + +#define LINK_TEMPLATES_TEST_FILE TEST_DATA_DIR "/links" +#define TEST_REGULAR_LINK "http://example.com/test/testpage.html" +#define TEST_IGNORED_LINK "http://example.com/ignoredpage.html" +#define TEST_STREAM_LINK "http://example.com/video/videopage.html" + +void link_templates_fixture_setup(LinkTemplatesFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) +{ + fixture->templates = link_templates_create(); + g_assert(fixture->templates != NULL); + link_templates_load(fixture->templates, LINK_TEMPLATES_TEST_FILE); +} + +void link_templates_fixture_teardown(LinkTemplatesFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) +{ + link_templates_delete(fixture->templates); + fixture->templates = NULL; +} + +void test_link_templates_load(LinkTemplatesFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) +{ + g_assert(link_templates_size(fixture->templates) >= 1); +} + +void test_link_templates_get(LinkTemplatesFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) +{ + const struct LinkAction *action; + action = link_templates_get_action(fixture->templates, TEST_REGULAR_LINK); + g_assert(action); + g_assert(link_action_get_type(action) == LINK_ACTION_PARSE); + + action = link_templates_get_action(fixture->templates, TEST_IGNORED_LINK); + g_assert(!action); + + action = link_templates_get_action(fixture->templates, TEST_STREAM_LINK); + g_assert(action); + g_assert(link_action_get_type(action) == LINK_ACTION_EXTERNAL_COMMAND); + const char *command = link_action_get_command(action); + g_assert(command); + g_assert(strcmp(command, "test_command") == 0); +} diff --git a/tests/linktemplates_tests.h b/tests/linktemplates_tests.h new file mode 100644 index 0000000..d718d61 --- /dev/null +++ b/tests/linktemplates_tests.h @@ -0,0 +1,23 @@ +#ifndef LINK_TEMPLATES_TESTS_H +#define LINK_TEMPLATES_TESTS_H + +#include <unistd.h> +#include <glib.h> +#include "linktemplates.h" + +typedef struct { + struct LinkTemplates *templates; +} LinkTemplatesFixture; + + +void link_templates_fixture_setup(LinkTemplatesFixture *fixture, + gconstpointer test_data); +void link_templates_fixture_teardown(LinkTemplatesFixture *fixture, + gconstpointer test_data); + +void test_link_templates_load(LinkTemplatesFixture *fixture, + gconstpointer test_data); +void test_link_templates_get(LinkTemplatesFixture *fixture, + gconstpointer test_data); + +#endif // LINK_TEMPLATES_TESTS_H diff --git a/tests/menubuilder_tests.c b/tests/menubuilder_tests.c new file mode 100644 index 0000000..a34f470 --- /dev/null +++ b/tests/menubuilder_tests.c @@ -0,0 +1,155 @@ +#include <string.h> +#include <stdlib.h> +#include <stdbool.h> +#include <libxml/parser.h> +#include <libxml/tree.h> +#include "menubuilder_tests.h" +#include "link.h" +#include "mainmenu.h" + +#define TEST_TITLE "Test menu" +#define LINK1_HREF "http://example.com/test/link" +#define LINK1_TITLE "Test link" +#define LINK2_HREF "http://example.com/video/00001" +#define LINK2_TITLE "Test stream" +#define LINK3_HREF "http://example.com/test/link3" +#define LINK3_TITLE "(large > small) & {50% of huge?}" +#define LINK3_TITLE_ENCODED "(large > small) & {50% of huge?}" + +#define TITLE_TAG "<h1>" +#define LINK_TAG "<a " + +static bool contains_substring(const char *buffer, const char *substr); +static int count_nonoverlapping_substrings(const char *buffer, const char *substr); +static void free_link(gpointer data); + +void menu_builder_fixture_setup(MenuBuilderFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + fixture->menu_builder = menu_builder_create(); + g_assert(fixture->menu_builder != NULL); +} + +void menu_builder_fixture_teardown(MenuBuilderFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + menu_builder_delete(fixture->menu_builder); + fixture->menu_builder = NULL; +} + +void test_menu_builder_title(MenuBuilderFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + menu_builder_set_title(fixture->menu_builder, TEST_TITLE); + char *menu = menu_builder_to_string(fixture->menu_builder); + g_assert(menu != NULL); + g_assert(contains_substring(menu, TEST_TITLE)); + int numlinks = count_nonoverlapping_substrings(menu, LINK_TAG); + g_assert(numlinks == 0); + free(menu); +} + +void test_menu_builder_append_links(MenuBuilderFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links = g_ptr_array_new_with_free_func(free_link); + g_assert(links != NULL); + g_ptr_array_add(links, link_create(LINK1_HREF, LINK1_TITLE, LINK_ACTION_PARSE)); + g_ptr_array_add(links, link_create(LINK2_HREF, LINK2_TITLE, LINK_ACTION_EXTERNAL_COMMAND)); + + menu_builder_append_link_list(fixture->menu_builder, links); + char *menu = menu_builder_to_string(fixture->menu_builder); + g_assert(menu != NULL); + int numlinks = count_nonoverlapping_substrings(menu, LINK_TAG); + g_assert(numlinks == 2); + g_assert(contains_substring(menu, LINK1_HREF)); + g_assert(contains_substring(menu, LINK1_TITLE)); + g_assert(contains_substring(menu, LINK2_HREF)); + g_assert(contains_substring(menu, LINK2_TITLE)); + + free(menu); + g_ptr_array_free(links, TRUE); +} + +void test_menu_builder_link_title_encoding(MenuBuilderFixture *fixture, + G_GNUC_UNUSED gconstpointer test_data) { + GPtrArray *links = g_ptr_array_new_with_free_func(free_link); + g_ptr_array_add(links, link_create(LINK3_HREF, LINK3_TITLE, LINK_ACTION_PARSE)); + + menu_builder_append_link_list(fixture->menu_builder, links); + char *menu = menu_builder_to_string(fixture->menu_builder); + g_assert(menu != NULL); + g_assert(contains_substring(menu, LINK3_TITLE_ENCODED)); + + free(menu); + g_ptr_array_free(links, TRUE); +} + +void test_mainmenu() { + char *menu = build_mainmenu(TEST_DATA_DIR "/websites"); + g_assert(menu); + + xmlDoc *doc = xmlReadMemory(menu, strlen(menu), "mainmenu.xml", NULL, 0); + g_assert(doc); + xmlNode *root = xmlDocGetRootElement(doc); + g_assert(root); + g_assert(xmlStrEqual(root->name, BAD_CAST "wvmenu")); + + unsigned int title_count = 0; + unsigned int link_count = 0; + unsigned int ul_count = 0; + xmlNode *node; + for (node = root->children; node; node = node->next) { + if (xmlStrEqual(node->name, BAD_CAST "title")) { + title_count++; + + } else if (xmlStrEqual(node->name, BAD_CAST "ul")) { + ul_count++; + + xmlNode *li_node; + for (li_node = node->children; li_node; li_node = li_node->next) { + g_assert(xmlStrEqual(li_node->name, BAD_CAST "li")); + + xmlNode *a_node; + for (a_node = li_node->children; a_node; a_node = a_node->next) { + g_assert(xmlStrEqual(a_node->name, BAD_CAST "a")); + g_assert(xmlHasProp(a_node, BAD_CAST "href")); + link_count++; + } + } + + } else if (node->type != XML_TEXT_NODE) { + g_assert_not_reached(); + } + } + + g_assert(title_count == 1); + g_assert(ul_count == 1); + g_assert(link_count >= 1); + + xmlFreeDoc(doc); + free(menu); +} + +int count_nonoverlapping_substrings(const char *buffer, const char *substr) { + if (!buffer || !substr) + return 0; + + int count = 0; + int substrlen = strlen(substr); + const char *p = buffer; + + while ((p = strstr(p, substr))) { + p += substrlen; + count++; + } + + return count; +} + +bool contains_substring(const char *buffer, const char *substr) { + if (!buffer || !substr) + return false; + + return strstr(buffer, substr) != NULL; +} + +void free_link(gpointer data) { + link_delete((struct Link *)data); +} diff --git a/tests/menubuilder_tests.h b/tests/menubuilder_tests.h new file mode 100644 index 0000000..8373224 --- /dev/null +++ b/tests/menubuilder_tests.h @@ -0,0 +1,26 @@ +#ifndef MENU_BUILDER_TESTS_H +#define MENU_BUILDER_TESTS_H + +#include <glib.h> +#include "menubuilder.h" + +typedef struct { + struct MenuBuilder *menu_builder; +} MenuBuilderFixture; + + +void menu_builder_fixture_setup(MenuBuilderFixture *fixture, + gconstpointer test_data); +void menu_builder_fixture_teardown(MenuBuilderFixture *fixture, + gconstpointer test_data); + +void test_menu_builder_title(MenuBuilderFixture *fixture, + gconstpointer test_data); +void test_menu_builder_append_links(MenuBuilderFixture *fixture, + gconstpointer test_data); +void test_menu_builder_link_title_encoding(MenuBuilderFixture *fixture, + gconstpointer test_data); + +void test_mainmenu(); + +#endif // MENU_BUILDER_TESTS_H diff --git a/tests/pipe_tests.c b/tests/pipe_tests.c new file mode 100644 index 0000000..0dd15ba --- /dev/null +++ b/tests/pipe_tests.c @@ -0,0 +1,246 @@ +#include <stdlib.h> +#include <string.h> +#include <glib.h> +#include "pipecomponent.h" +#include "pipe_tests.h" + +#define TEST_STRING "ABCDEF" +#define TEST_FD1 1 +#define TEST_FD2 2 + +typedef struct CountingPipe { + PipeComponent p; + size_t bytes; + gboolean finished; + gboolean failed; +} CountingPipe; + +static gboolean delete1_called; +static gboolean delete2_called; + +static CountingPipe *testpipe_create( + gboolean (*process)(PipeComponent *, char *, size_t), + void (*finished)(PipeComponent *, RequestState state), + void (*delete)(PipeComponent *)); +static CountingPipe *testpipe_create_fdset( + gboolean (*process)(PipeComponent *, char *, size_t), + void (*finished)(PipeComponent *, RequestState state), + void (*delete)(PipeComponent *), + void (*fdset)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *), + gboolean (*handle_socket)(PipeComponent *, int, int)); +static gboolean testpipe_forward(PipeComponent *component, char *buf, size_t len); +static gboolean testpipe_fail(PipeComponent *component, char *buf, size_t len); +static void testpipe_fdset1(PipeComponent *self, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); +static void testpipe_fdset2(PipeComponent *self, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd); +static void testpipe_finished(PipeComponent *component, RequestState state); +static void testpipe_delete(PipeComponent *component); +static void testpipe_delete1(PipeComponent *component); +static void testpipe_delete2(PipeComponent *component); + +void test_pipe_one_component() { + CountingPipe *pipe = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + + pipe_component_append(&pipe->p, TEST_STRING, strlen(TEST_STRING)); + g_assert(!pipe->finished); + g_assert(!pipe->failed); + g_assert(pipe_component_get_state(&pipe->p) == WEBVISTATE_NOT_FINISHED); + g_assert(pipe->bytes == strlen(TEST_STRING)); + + pipe_component_finished(&pipe->p, WEBVISTATE_FINISHED_OK); + g_assert(pipe->finished); + g_assert(!pipe->failed); + g_assert(pipe_component_get_state(&pipe->p) == WEBVISTATE_FINISHED_OK); + g_assert(pipe->bytes == strlen(TEST_STRING)); + + pipe_delete_full(&pipe->p); +} + +void test_pipe_not_appending_after_finished() { + CountingPipe *pipe = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + + pipe_component_append(&pipe->p, TEST_STRING, strlen(TEST_STRING)); + g_assert(pipe->bytes == strlen(TEST_STRING)); + + pipe_component_finished(&pipe->p, WEBVISTATE_FINISHED_OK); + + pipe_component_append(&pipe->p, TEST_STRING, strlen(TEST_STRING)); + g_assert(pipe->bytes == strlen(TEST_STRING)); + + pipe_delete_full(&pipe->p); +} + +void test_pipe_state_not_chaning_after_finished() { + CountingPipe *pipe = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + + pipe_component_append(&pipe->p, TEST_STRING, strlen(TEST_STRING)); + g_assert(pipe_component_get_state(&pipe->p) == WEBVISTATE_NOT_FINISHED); + + pipe_component_finished(&pipe->p, WEBVISTATE_INTERNAL_ERROR); + g_assert(pipe_component_get_state(&pipe->p) == WEBVISTATE_INTERNAL_ERROR); + + pipe_component_finished(&pipe->p, WEBVISTATE_FINISHED_OK); + g_assert(pipe_component_get_state(&pipe->p) == WEBVISTATE_INTERNAL_ERROR); + + pipe_delete_full(&pipe->p); +} + +void test_pipe_two_components() { + CountingPipe *first = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + CountingPipe *second = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + pipe_component_set_next(&first->p, &second->p); + + pipe_component_append(&first->p, TEST_STRING, strlen(TEST_STRING)); + pipe_component_finished(&first->p, WEBVISTATE_FINISHED_OK); + + g_assert(first->finished); + g_assert(!first->failed); + g_assert(first->bytes == strlen(TEST_STRING)); + g_assert(second->finished); + g_assert(!second->failed); + g_assert(second->bytes == strlen(TEST_STRING)); + + pipe_delete_full(&first->p); +} + +void test_pipe_failing_component() { + CountingPipe *first = + testpipe_create(testpipe_fail, testpipe_finished, testpipe_delete); + CountingPipe *second = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete); + pipe_component_set_next(&first->p, &second->p); + + pipe_component_append(&first->p, TEST_STRING, strlen(TEST_STRING)); + pipe_component_finished(&first->p, WEBVISTATE_FINISHED_OK); + + g_assert(first->failed); + g_assert(second->finished); + g_assert(second->failed); + g_assert(second->bytes == 0); + + pipe_delete_full(&first->p); +} + +void test_pipe_delete_all() { + CountingPipe *first = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete1); + CountingPipe *second = + testpipe_create(testpipe_forward, testpipe_finished, testpipe_delete2); + pipe_component_set_next(&first->p, &second->p); + + delete1_called = FALSE; + delete2_called = FALSE; + + pipe_delete_full(&first->p); + + g_assert(delete1_called); + g_assert(delete2_called); +} + +void test_pipe_fdset() { + fd_set readfds; + fd_set writefds; + fd_set excfds; + int maxfd = 0; + CountingPipe *first = + testpipe_create_fdset(testpipe_forward, testpipe_finished, + testpipe_delete, testpipe_fdset1, NULL); + CountingPipe *second = + testpipe_create_fdset(testpipe_forward, testpipe_finished, + testpipe_delete, testpipe_fdset2, NULL); + pipe_component_set_next(&first->p, &second->p); + + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&excfds); + pipe_fdset(&first->p, &readfds, &writefds, &excfds, &maxfd); + g_assert(FD_ISSET(TEST_FD1, &readfds)); + g_assert(FD_ISSET(TEST_FD2, &readfds)); + + pipe_delete_full(&first->p); +} + +CountingPipe *testpipe_create( + gboolean (*process)(PipeComponent *, char *, size_t), + void (*finished)(PipeComponent *, RequestState state), + void (*delete)(PipeComponent *)) +{ + CountingPipe *pipe = malloc(sizeof(CountingPipe)); + g_assert(pipe); + memset(pipe, 0, sizeof(CountingPipe)); + pipe_component_initialize(&pipe->p, process, finished, delete); + return pipe; +} + +CountingPipe *testpipe_create_fdset( + gboolean (*process)(PipeComponent *, char *, size_t), + void (*finished)(PipeComponent *, RequestState state), + void (*delete)(PipeComponent *), + void (*fdset)(PipeComponent *, fd_set *, fd_set *, fd_set *, int *), + gboolean (*handle_socket)(PipeComponent *, int, int)) +{ + CountingPipe *pipe = malloc(sizeof(CountingPipe)); + g_assert(pipe); + memset(pipe, 0, sizeof(CountingPipe)); + pipe_component_initialize_fdset(&pipe->p, process, finished, delete, + fdset, handle_socket); + return pipe; +} + +gboolean testpipe_forward(PipeComponent *component, char *buf, size_t len) { + CountingPipe *self = (CountingPipe *)component; + self->bytes += len; + return TRUE; +} + +gboolean testpipe_fail(PipeComponent *component, char *buf, size_t len) { + CountingPipe *self = (CountingPipe *)component; + pipe_component_finished(component, WEBVISTATE_INTERNAL_ERROR); + return TRUE; +} + +void testpipe_fdset1(PipeComponent *self, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd) +{ + FD_SET(TEST_FD1, readfd); + if (TEST_FD1 > *max_fd) + *max_fd = TEST_FD1; +} + +void testpipe_fdset2(PipeComponent *self, fd_set *readfd, + fd_set *writefd, fd_set *excfd, int *max_fd) +{ + FD_SET(TEST_FD2, readfd); + if (TEST_FD2 > *max_fd) + *max_fd = TEST_FD2; +} + +void testpipe_finished(PipeComponent *component, RequestState state) { + CountingPipe *self = (CountingPipe *)component; + self->finished = TRUE; + if (state != WEBVISTATE_FINISHED_OK) + self->failed = TRUE; +} + +void testpipe_delete(PipeComponent *component) { + CountingPipe *self = (CountingPipe *)component; + free(self); +} + +void testpipe_delete1(PipeComponent *component) { + CountingPipe *self = (CountingPipe *)component; + delete1_called = TRUE; + free(self); +} + +void testpipe_delete2(PipeComponent *component) { + CountingPipe *self = (CountingPipe *)component; + delete2_called = TRUE; + free(self); +} diff --git a/tests/pipe_tests.h b/tests/pipe_tests.h new file mode 100644 index 0000000..4ed02c2 --- /dev/null +++ b/tests/pipe_tests.h @@ -0,0 +1,12 @@ +#ifndef PIPE_TESTS_H +#define PIPE_TESTS_H + +void test_pipe_one_component(); +void test_pipe_two_components(); +void test_pipe_failing_component(); +void test_pipe_not_appending_after_finished(); +void test_pipe_state_not_chaning_after_finished(); +void test_pipe_delete_all(); +void test_pipe_fdset(); + +#endif // PIPE_TESTS_H diff --git a/tests/urlutils_tests.c b/tests/urlutils_tests.c new file mode 100644 index 0000000..da5f9e1 --- /dev/null +++ b/tests/urlutils_tests.c @@ -0,0 +1,227 @@ +#include "urlutils_tests.h" +#include "urlutils.h" + +void test_url_scheme() { + gchar *prefix = url_scheme("http://example.com/"); + g_assert(prefix); + g_assert(strcmp(prefix, "http") == 0); + g_free(prefix); +} + +void test_url_scheme_no_scheme() { + gchar *prefix = url_scheme("example.com/"); + g_assert(prefix); + g_assert(strcmp(prefix, "") == 0); + g_free(prefix); +} + +void test_url_scheme_double_scheme() { + gchar *prefix = url_scheme("ftp://http://example.com/"); + g_assert(prefix); + g_assert(strcmp(prefix, "ftp") == 0); + g_free(prefix); +} + +void test_url_scheme_invalid_characters() { + gchar *prefix = url_scheme("invalid/http://example.com/"); + g_assert(prefix); + g_assert(strcmp(prefix, "") == 0); + g_free(prefix); +} + +void test_url_root() { + gchar *prefix = url_root("http://example.com/"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/") == 0); + g_free(prefix); +} + +void test_url_root_full_path() { + gchar *prefix = url_root("http://example.com/path/to/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/") == 0); + g_free(prefix); +} + +void test_url_root_terminated_by_query() { + gchar *prefix = url_root("http://example.com?query=path"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/") == 0); + g_free(prefix); +} + +void test_url_path() { + gchar *prefix = url_path_including_file("http://example.com/path/to/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html") == 0); + g_free(prefix); +} + +void test_url_path_ends_in_slash() { + gchar *prefix = url_path_including_file("http://example.com/path/"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/") == 0); + g_free(prefix); +} + +void test_url_path_query() { + gchar *prefix = url_path_including_file("http://example.com/path/to/file.html?foo=bar"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html") == 0); + g_free(prefix); +} + +void test_url_path_fragment() { + gchar *prefix = url_path_including_file("http://example.com/path/to/file.html#frag"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html") == 0); + g_free(prefix); +} + +void test_url_path_no_path() { + gchar *prefix = url_path_including_file("http://example.com"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/") == 0); + g_free(prefix); +} + +void test_url_path_no_server() { + gchar *prefix = url_path_including_file("http:///path/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "http:///path/file.html") == 0); + g_free(prefix); +} + +void test_url_path_no_scheme() { + gchar *prefix = url_path_including_file("example.com/path/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "example.com/path/file.html") == 0); + g_free(prefix); +} + +void test_url_path_dirname() { + gchar *prefix = url_path_dirname("http://example.com/path/to/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/") == 0); + g_free(prefix); +} + +void test_url_path_dirname_no_file() { + gchar *prefix = url_path_dirname("http://example.com/path/to/"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/") == 0); + g_free(prefix); +} + +void test_url_path_dirname_query() { + gchar *prefix = url_path_dirname("http://example.com/path/to/file.html?foo=bar"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/") == 0); + g_free(prefix); +} + +void test_url_path_dirname_no_server() { + gchar *prefix = url_path_dirname("/path/to/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "/path/to/") == 0); + g_free(prefix); +} + +void test_url_path_and_query() { + gchar *prefix = url_path_and_query("http://example.com/path/to/file.html?foo=1&bar=2"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html?foo=1&bar=2") == 0); + g_free(prefix); +} + +void test_url_path_and_query_no_query() { + gchar *prefix = url_path_and_query("http://example.com/path/to/file.html"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html") == 0); + g_free(prefix); +} + +void test_url_path_and_query_double_query() { + gchar *prefix = url_path_and_query("http://example.com/path/to/file.html?foo=1?bar=2"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html?foo=1?bar=2") == 0); + g_free(prefix); +} + +void test_url_path_and_query_fragment() { + gchar *prefix = url_path_and_query("http://example.com/path/to/file.html?foo=1&bar=2#frag"); + g_assert(prefix); + g_assert(strcmp(prefix, "http://example.com/path/to/file.html?foo=1&bar=2") == 0); + g_free(prefix); +} + +void test_url_path_and_query_no_path() { + gchar *prefix = url_path_and_query("?foo=1&bar=2"); + g_assert(prefix); + g_assert(strcmp(prefix, "?foo=1&bar=2") == 0); + g_free(prefix); +} + +void test_url_rel2abs_file() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html", + "file.html"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/file.html") == 0); + g_free(abs); +} + +void test_url_rel2abs_root() { + gchar *abs = relative_url_to_absolute("http://example.com/path/index.html", + "/another/path/file.html"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/another/path/file.html") == 0); + g_free(abs); +} + +void test_url_rel2abs_query() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html?foo=1", + "?bar=2"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/index.html?bar=2") == 0); + g_free(abs); +} + +void test_url_rel2abs_double_query() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html?foo=1?bar=2", + "?baz=3"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/index.html?baz=3") == 0); + g_free(abs); +} + +void test_url_rel2abs_append_query() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html", + "?bar=2"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/index.html?bar=2") == 0); + g_free(abs); +} + +void test_url_rel2abs_fragment() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html#frag", + "#bar"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/index.html#bar") == 0); + g_free(abs); +} + +void test_url_rel2abs_append_fragment() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html", + "#bar"); + g_assert(abs); + g_assert(strcmp(abs, "http://example.com/index.html#bar") == 0); + g_free(abs); +} + +void test_url_rel2abs_scheme() { + gchar *abs = relative_url_to_absolute("http://example.com/index.html", + "//server.org/path2/file2"); + g_assert(abs); + g_assert(strcmp(abs, "http://server.org/path2/file2") == 0); + g_free(abs); +} diff --git a/tests/urlutils_tests.h b/tests/urlutils_tests.h new file mode 100644 index 0000000..e4c796c --- /dev/null +++ b/tests/urlutils_tests.h @@ -0,0 +1,38 @@ +#ifndef URLUTILS_TESTS_H +#define URLUTILS_TESTS_H + +#include <glib.h> + +void test_url_scheme(); +void test_url_scheme_no_scheme(); +void test_url_scheme_double_scheme(); +void test_url_scheme_invalid_characters(); +void test_url_root(); +void test_url_root_full_path(); +void test_url_root_terminated_by_query(); +void test_url_path(); +void test_url_path_ends_in_slash(); +void test_url_path_query(); +void test_url_path_fragment(); +void test_url_path_no_path(); +void test_url_path_no_server(); +void test_url_path_no_scheme(); +void test_url_path_dirname(); +void test_url_path_dirname_no_file(); +void test_url_path_dirname_query(); +void test_url_path_dirname_no_server(); +void test_url_path_and_query(); +void test_url_path_and_query_no_query(); +void test_url_path_and_query_double_query(); +void test_url_path_and_query_fragment(); +void test_url_path_and_query_no_path(); +void test_url_rel2abs_file(); +void test_url_rel2abs_root(); +void test_url_rel2abs_query(); +void test_url_rel2abs_double_query(); +void test_url_rel2abs_append_query(); +void test_url_rel2abs_fragment(); +void test_url_rel2abs_append_fragment(); +void test_url_rel2abs_scheme(); + +#endif // URLUTILS_TESTS_H diff --git a/websites/links b/websites/links new file mode 100644 index 0000000..21d1fbc --- /dev/null +++ b/websites/links @@ -0,0 +1,68 @@ +### Youtube ### + +# category +http://www\.youtube\.com/channels/ +# channel +http://www\.youtube\.com/channel/ +# user +http://www\.youtube\.com/user/ +# search results +http://www\.youtube\.com/results\? +# video +http://www\.youtube\.com/watch\? stream:libquvi + +### Metacafe ### + +# category +http://www\.metacafe\.com/videos/ +# search results +###http://www\.metacafe\.com/topics/ +# video +http://www\.metacafe\.com/watch/ stream:libquvi + +### www.ruutu.fi ### + +# series page +http://www\.ruutu\.fi/ohjelmat/.+?/$ +# search results +http://www\.ruutu\.fi/hakutulokset/ +# video +http://www\.ruutu\.fi/ohjelmat/.+?/.+ stream:libquvi + +### Vimeo ### + +# channel +http://vimeo\.com/channels/.+?/videos +# category +http://vimeo\.com/categories/[^/]+$ +# group +http://vimeo\.com/groups/[^/]+$ +# user +http://vimeo.com/[a-zA-Z][a-zA-Z0-9]+$ +# video +http://vimeo\.com/[0-9]+$ stream:libquvi +http://vimeo\.com/.*/[0-9]+$ stream:libquvi + +### Dailymotion ### + +# channel +http://www.dailymotion.com/en/channel/ +http://www.dailymotion.com/channel/ +# video +http://www.dailymotion.com/video/ stream:libquvi + + +### imdb ### + +# video +http://www.imdb.com/video/ stream:libquvi + + +### areena.yle.fi ### + +# category +http://areena\.yle\.fi/(?:tv|radio)/?.*/kaikki.json? bin:areena-category.py +# search results +http://areena.yle.fi/.json bin:areena-category.py +# video +http://areena\.yle\.fi/(?:tv|radio)/[0-9]+$ stream:yle-dl diff --git a/websites/www.metacafe.com/menu.xml b/websites/www.metacafe.com/menu.xml new file mode 100644 index 0000000..bdb2ebe --- /dev/null +++ b/websites/www.metacafe.com/menu.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<wvmenu> + <title>Metacafe</title> + <description>We’re the first and only entertainment destination solely dedicated to showcasing the best short-form videos from the world of Movies, Video Games, TV, Music and Sports – programmed for today’s young male Entertainment Drivers.</description> + + <ul> + <li><a href="http://www.metacafe.com/videos/" class="webvi">Categories</a></li> + </ul> +</wvmenu> diff --git a/websites/www.youtube.com/menu.xml b/websites/www.youtube.com/menu.xml new file mode 100644 index 0000000..d060772 --- /dev/null +++ b/websites/www.youtube.com/menu.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<wvmenu> + <title>YouTube</title> + <description>Video sharing service on which users worldwide can upload their videos</description> + + <ul> + <li><a href="wvt://www.youtube.com/search.xml" class="webvi">Search</a></li> + <li><a href="http://www.youtube.com/channels?feature=guide" class="webvi">Categories</a></li> + </ul> +</wvmenu> diff --git a/websites/www.youtube.com/search.xml b/websites/www.youtube.com/search.xml new file mode 100644 index 0000000..46cca93 --- /dev/null +++ b/websites/www.youtube.com/search.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<wvmenu> + <title>Youtube Search</title> + + <textfield name="q"> + <label>Search terms</label> + </textfield> + + <itemlist name="uploaded"> + <label>Uploaded</label> + <item value="">Anytime</item> + <item value="d">Today</item> + <item value="w">This week</item> + <item value="m">This month</item> + </itemlist> + + <itemlist name="duration"> + <label>Duration</label> + <item value="">All</item> + <item value="short">Short</item> + <item value="long">Long</item> + </itemlist> + + <button> + <label>Search</label> + <submission>http://www.youtube.com/results?search_query={q}&oq={q}&uploaded={uploaded}&search_duration={duration}</submission> + </button> +</wvmenu> |