summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntti Ajanki <antti.ajanki@iki.fi>2013-08-06 09:07:49 +0300
committerAntti Ajanki <antti.ajanki@iki.fi>2013-08-06 09:07:49 +0300
commit7c81286a59639e139ac7e947378be24410701a5e (patch)
tree88e43b758dc2330e8711ebae80eee0039cc57322
downloadvdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.gz
vdr-plugin-webvideo-7c81286a59639e139ac7e947378be24410701a5e.tar.bz2
import to vdr-developer repo
-rw-r--r--CMakeLists.txt19
-rw-r--r--COPYING701
-rw-r--r--HISTORY281
-rw-r--r--README41
-rw-r--r--README.vdrplugin233
-rw-r--r--README.webvi138
-rw-r--r--cmake/modules/FindLibTidy.cmake73
-rw-r--r--cmake/modules/MacroBoolTo01.cmake20
-rw-r--r--examples/savevideo_bookmarklet.js1
-rwxr-xr-xexamples/transcode2ogg.sh29
-rwxr-xr-xexamples/watchonvdr.sh19
-rw-r--r--examples/watchonvdr_bookmarklet.js1
-rw-r--r--examples/watchonvdr_proxy.py99
-rw-r--r--examples/webvi.conf28
-rw-r--r--examples/webvi.plugin.conf11
-rw-r--r--src/libwebvi/CMakeLists.txt50
-rw-r--r--src/libwebvi/libwebvi.c338
-rw-r--r--src/libwebvi/libwebvi.h370
-rw-r--r--src/libwebvi/link.c86
-rw-r--r--src/libwebvi/link.h45
-rw-r--r--src/libwebvi/linkextractor.c138
-rw-r--r--src/libwebvi/linkextractor.h34
-rw-r--r--src/libwebvi/linktemplates.c172
-rw-r--r--src/libwebvi/linktemplates.h34
-rw-r--r--src/libwebvi/mainmenu.c120
-rw-r--r--src/libwebvi/mainmenu.h27
-rw-r--r--src/libwebvi/menubuilder.c99
-rw-r--r--src/libwebvi/menubuilder.h37
-rw-r--r--src/libwebvi/pipecomponent.c689
-rw-r--r--src/libwebvi/pipecomponent.h95
-rw-r--r--src/libwebvi/request.c237
-rw-r--r--src/libwebvi/request.h42
-rw-r--r--src/libwebvi/urlutils.c132
-rw-r--r--src/libwebvi/urlutils.h32
-rw-r--r--src/libwebvi/webvicontext.c409
-rw-r--r--src/libwebvi/webvicontext.h63
-rw-r--r--src/pywebvi/pywebvi.py248
-rw-r--r--src/version.h.in1
-rwxr-xr-xsrc/webvicli/webvi22
-rw-r--r--src/webvicli/webvicli/__init__.py1
-rw-r--r--src/webvicli/webvicli/client.py825
-rw-r--r--src/webvicli/webvicli/menu.py177
-rw-r--r--tests/CMakeLists.txt44
-rw-r--r--tests/context_tests.c54
-rw-r--r--tests/context_tests.h23
-rw-r--r--tests/data/links5
-rw-r--r--tests/data/websites/www.youtube.com/menu.xml16
-rw-r--r--tests/libwebvi_tests.c106
-rw-r--r--tests/linkextractor_tests.c158
-rw-r--r--tests/linkextractor_tests.h34
-rw-r--r--tests/linktemplates_tests.c46
-rw-r--r--tests/linktemplates_tests.h23
-rw-r--r--tests/menubuilder_tests.c155
-rw-r--r--tests/menubuilder_tests.h26
-rw-r--r--tests/pipe_tests.c246
-rw-r--r--tests/pipe_tests.h12
-rw-r--r--tests/urlutils_tests.c227
-rw-r--r--tests/urlutils_tests.h38
-rw-r--r--websites/links68
-rw-r--r--websites/www.metacafe.com/menu.xml10
-rw-r--r--websites/www.youtube.com/menu.xml11
-rw-r--r--websites/www.youtube.com/search.xml29
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)
+
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..4d82ab0
--- /dev/null
+++ b/COPYING
@@ -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.
diff --git a/HISTORY b/HISTORY
new file mode 100644
index 0000000..6941e29
--- /dev/null
+++ b/HISTORY
@@ -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.
diff --git a/README b/README
new file mode 100644
index 0000000..dc0fe4b
--- /dev/null
+++ b/README
@@ -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 &gt; small) &amp; {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}&amp;oq={q}&amp;uploaded={uploaded}&amp;search_duration={duration}</submission>
+ </button>
+</wvmenu>